本文翻譯自Getting Started with Google Guava這本書,如有翻譯不足的地方請指出。
在這一章,我們開始注意到使用Guava進行編寫代碼會更加簡單。我們將看看如何使用Guava當中的接口和類可以幫助我們,通過應用行之有效的模式,以使我們的代碼更容易維護以及健壯。
在本章中我們將包含一下幾點:
- Function接口:這說明在java編程當中可以引入函數式編程。同時也說明了如何使用Function接口以及最好的使用方式。
- Functions類:Functions類包含一些實用的方法來操作Fucntion接口的實例。
- Predicate接口:這個接口是評估一個對象是否滿足一定條件,如果滿足則返回true。
- Predicates類:這個類是對於Predicate接口的指南類,它實現了Predicate接口並且非常實用的靜態方法。
- Supplier接口:這個接口可以提供一個對象通過給定的類型。我們也可以看到通過各種各樣的方式來創建對象。
- Suppliers類:這個類是Suppliers接口的默認實現類。
使用Function接口
函數式編程強調使用函數,以實現其目標與不斷變化的狀態。這與大多數開發者熟悉的改變狀態的編程方式形成對比。Function接口讓我們在java代碼當中引入函數式編程成為可能。
Function接口當中只有2個方法:
public interface Function<F,T> { T apply(F input); boolean equals(Object object); }
我們不會具體的使用equals方法來判斷A對象與B對象是否相等,只會調用apply方法來比較A對象與B對象是否相等。apply方法接受一個參數並且返回一個對象。一個好的功能實現應該沒有副作用,這意味着當一個對象傳入到apply方法當中后應該是保持不變的。下面是一個接受Date對象並且返回Date格式化后字符串的例子:
public class DateFormatFunction implements Function<Date,String> { @Override public String apply(Date input) { SimpleDateFormat dateFormat = new SimpleDateFormat("dd/mm/yyyy"); return dateFormat.format(input); } }
在這個例子當中,我們可以清楚看到Date對象正在通過SimpleDateFormat類轉換成我們期望格式的字符串。雖然這個例子可能過於簡單,但是它演示了Function接口的作用,轉換一個對象並且隱藏了實現的細節。通過這個例子我們可以使用實現了Function接口的類,我們也可以使用匿名類來實現。看看下面的例子:
Function<Date,String> function = new Function<Date, String>() { @Override public String apply( Date input) { return new SimpleDateFormat("dd/mm/yyyy").format(input); } };
這2個例子沒什么不同。一個是簡單的實現了Function接口,另一個是匿名類。實現了Function接口的類的優點是,你可以使用依賴注入來傳遞一個函數接口到一個協作的類中,使得代碼高內聚。
使用Function接口的參考
這是一個很好的機會討論在你的代碼中使用匿名類來引入Function接口。在java當前的狀態中,我們沒有閉包特性。當JAVA8發布后將會改變,現在java的回答就是使用匿名類。當你使用匿名類來充當閉包的時候,語法是相當的繁重。如果使用太頻繁,會使你的代碼很難跟蹤和維護。事實上,對上面的例子分析,這個例子只是為了演示Function是如何使用,我們不能獲取到更多的好處。例如,下面代碼來實現這個功能會更好。
public String formatDate(Date input) { return new SimpleDateFormat("dd/mm/yyyy").format(input); }
現在比較一下這兩個例子,我們會發現后面的更加容易讀懂。當我們使用Function取決於你想在什么地方進行轉換。如果你有一個類里面包含一個Date實例變量和一個返回日期轉換成期望字符串的方法,你可能更好的執行后面的例子。然而,如果你有一個Date對象的集合並且想獲取這些Date的字符串形式的list,使用Function接口可能是一個不錯的方法。這里的要點是,你不能因為可以就放棄使用Function匿名實例在你的代碼里。看你的代碼,你是否從函數編程中獲得了好處。第四章Guava Collections和第六章Guava Cache我們會看些實用的例子。
使用Functions類
Functions類包含一些實用的方法來操作Fucntion接口的實例。在本節當中,我們將會講其中的兩個方法。
使用Functions.forMap方法
forMap方法接受一個Map<K,V>的參數並且返回一個Function<K,V>實例,執行apply方法會在map當中進行查找。例如,考慮下面的類代表美國的一個州。
public class State { private String name; private String code; private Set<City> mainCities = new HashSet<City>();
//省去getter和setter方法 }
現在你有一個名為stateMap的Map<String, State>,他的key就是州名的縮寫。現在我們可以創建一個通過州代碼來查找的函數,你只需要下面幾步:
Function<String,State> lookup = Functions.forMap(stateMap); //Would return State object for NewYork lookup.apply("NY");
使用Functions.forMap方法有一個警告。如果傳入的key在map當中不存在會拋出IllegalArgumentException異常。然而,有一個重載的forMap方法增加一個默認值參數,如果key沒找到會返回默認值。通過使用Function接口來執行state的查找,你可以很容易的改變這個實現。當我們使用Splitter對象來創建一個map或者使用Guava collection包中其他的一些方法來創建map,總之我們可以在我們代碼當中借住Guava的力量。
使用Functions.compose方法
假設你現在有一個代表city的類,代碼如下:
public class City { private String name; private String zipCode; private int population;
//省去getter和setter方法 public String toString() { return name; } }
考慮下面的情形:你要創建一個Function實例,傳入State對象返回當中mainCities逗號分割的字符串。代碼如下:
public class StateToCityString implements Function<State,String> { @Override public String apply(State input) { return Joiner.on(",").join(input.getMainCities()); } }
更進一步來說。你希望只有一個Function實例通過傳入State的名稱縮寫來返回這State當中mainCities逗號分割的字符串。Guava提高了一個很好的方法來解決這種情況,Functions.compose方法接受兩個Function實例作為參數,並且返回這兩個Function組合后的Function。所以我們可以使用上面的兩個Function來舉一個例子:
Function<String,State> lookup = Functions.forMap(stateMap); Function<State, String> stateFunction = new StateToCityString(); Function<String,String> composed = Functions.compose(stateFunction ,lookup);
現在調用composed.apply("NY")方法將會返回:"Albany,Buffalo,NewYorkCity"
花一分鍾時間來看下方法的調用順序。composed接受一個“NY”參數並且調用lookup.apply()方法,從lookup.apply()方法中返回的值傳入了stateFunction.apply()方法當中並且返回執行結果。可以理解為第二個參數的輸入參數就是composed.apply方法的輸入參數,第一個參數的輸出就是composed.apply 方法的輸出。如果不使用composed 方法,在前面的例子將如下所示:
String cities = stateFunction.apply(lookup.apply("NY"));
使用Predicate接口
Predicate接口與Function接口的功能相似。像Function接口一樣,Predicate接口也有2個方法:
public interface Predicate<T> { boolean apply(T input) boolean equals(Object object) }
Function接口的情況是,我們不會去詳細的講解equals方法。apply方法會返回對輸入斷定后的結果。在Fucntion接口使用的地方來轉換對象,Predicate接口則用於過濾對象。Predicates類的使用與Functions類一樣。當一個簡單方法能實現的不使用Predicates類。同時,Predicate接口沒有任何副作用。在下一章,會講到Collections,我們會看到Predicate的最佳實踐。
Predicate接口的例子
這是Predicate接口的一個簡單的例子,我們使用上面例子當中的City類。我們定義一個Predicate來判斷這個城市是否有最小人口。
public class PopulationPredicate implements Predicate<City> { @Override public boolean apply(City input) { return input.getPopulation() <= 500000; } }
在這個例子當中,我們簡單的檢查了City對象當中的人口屬性,當人口屬性小於等於500000的時候會返回true。通常,你可以將Predicate的實現定義成匿名類來過濾集合當中的每一個元素。Predicate接口和Function接口是如此的相似,許多情況下使用Fucntion接口的時候同樣可以使用Predicate接口。
使用Predicates類
Predicates類是包含一些實用的方法來操作Predicate接口的實例。Predicates類提供了一些非常有用的方法,從布爾值條件中得到期望的值,也可以使用“and”和“or”方法來連接不同的Predicate實例作為條件,並且如果提供“not”方法一個Predicate實例返回的值是false則“not”方法返回true,反之亦然。同樣也有Predicates.compose方法,但是他接受一個Predicate實例和一個Function實例,並且返回Predicate執行后的值,把Function執行后的值當中它的參數。讓我們看些例子,我們會更好的理解如何在代碼中使用Predicates類。先看個特殊的例子,假設我們有下面兩個類的實例。
public class TemperateClimatePredicate implements Predicate<City> { @Override public boolean apply(City input) { return input.getClimate().equals(Climate.TEMPERATE); } }
public class LowRainfallPredicate implements Predicate<City> { @Override public boolean apply(City input) { return input.getAverageRainfall() < 45.7; } }
值得重申,通常我們會使用匿名類,但為清楚起見,我們將使用具體類。
使用Predicates.and方法
Predicates.and方法接受多個Predicate對象並且返回一個Predicate對象,因此調用返回的Predicate對象的apply方法當所有Predicate對象的apply方法都返回true的時候會返回true。如果其中一個Predicate對象返回false,其他的Predicate對象的執行就會停止。例如,假如我們只允許城市人口小於500,000並且年降雨量小於45.7英寸的。
Predicate smallAndDry = Predicates.and(smallPopulationPredicate, lowRainFallPredicate);
下面是Predicates.and方法的簽名:
Predicates.and(Iterable<Predicate<T>> predicates);
Predicates.and(Predicate<T> ...predicates);
使用Predicates.or方法
Predicates.or方法接受多個Predicate對象並且返回一個Predicate對象,如果當中有一個Predicate對象的apply方法返回true則總方法就返回true。
如果有一個Predicate實例返回true,就不會繼續執行。例如,假設我們想包含城市人口小於等於500,000或者有溫帶氣候的城市。
Predicate smallTemperate = Predicates.or(smallPopulationPredicate, temperateClimatePredicate);
下面是Predicates.or方法的簽名:
Predicates.or(Iterable<Predicate<T>> predicates);
Predicates.or(Predicate<T> ...predicates);
使用Predicates.not方法
Predicates.not接受一個Predicate實例並且執行這個Predicate實例的邏輯否的功能。加入我們想獲得人口大於500,000的城市。使用這個方法可以代替重寫一個Predicate實例:
Predicate largeCityPredicate = Predicates.not(smallPopulationPredicate);
使用Predicates.compose方法
Predicates.compose接受一個Function實例和一個Predicate實例作為參數,並且從Fucntion實例當中返回的對象傳入到Predicate對象當中並且進行評估。在下面的例子當中,我們創建一個新的Predicate對象:
public class SouthwestOrMidwestRegionPredicate implements Predicate<State> { @Override public boolean apply(State input) { return input.getRegion().equals(Region.MIDWEST) || input.getRegion().equals(Region.SOUTHWEST); } }
下一步,我們將使用原來的Function實例lookup來創建一個State對象,並且使用上面例子的Predicate實例來評估下這個State是否在MIDWEST或者SOUTHWEST:
Predicate<String> predicate = Predicates.compose(southwestOrMidwestRegionPredicate,lookup);
使用Supplier接口
Supplier接口只有一個方法如下:
public interface Supplier<T> { T get(); }
get方法返回泛型T的實例。Supplier接口可以幫助我們實現幾個典型的創建模式。當get方法被調用,我們可以返回相同的實例或者每次調用都返回新的實例。Supplier也可以讓你靈活選擇是否當get方法調用的時候才創建實例。並且Supplier是個接口,單元測試也會更簡單,相對於其他方法創建的對象,如靜態工廠方法。 總之,供應Supplier接口的強大之處在於它抽象的復雜性和對象如何需要創建的細節,讓開發人員自由地在他覺得任何方式創建一個對象時最好的方法。讓我們看看如何Supplier接口。
一個Supplier接口的例子
下面代碼是Supplier接口的例子:
public class ComposedPredicateSupplier implements Supplier<Predicate<String>> { @Override public Predicate<String> get() { City city = new City("Austin,TX","12345",250000, Climate.SUB_ TROPICAL, 45.3); State state = new State("Texas","TX", Sets.newHashSet(city), Region.SOUTHWEST); City city1 = new City("New York,NY","12345",2000000,Climate.TEMPERATE, 48.7); State state1 = new State("New York","NY",Sets.newHashSet(city1), Region.NORTHEAST); Map<String,State> stateMap = Maps.newHashMap(); stateMap.put(state.getCode(),state); stateMap.put(state1.getCode(),state1); Function<String,State> mf = Functions.forMap(stateMap); return Predicates.compose(new RegionPredicate(), mf); } }
在這個例子當中,我們看到使用Functions.forMap關鍵一個Function實例可以通過State的縮寫來查找State,和使用Predicate實例來評估在那些地方是否有這個State。然后將Function實例和Predicate實例作為參數傳入到Predicates.compose方法當中並且返回期望的Predicate實例。我們使用了兩個靜態方法,Maps.newHashMap() 和Sets. newHashSet(),這兩個都可以在Guava的包當中找到我們下一章會講到。現在我們每次調用都會返回新的實例。我們也可以把創建Predicate實例的工作放到ComposedPredicateSupplier 的構造方法中來進行,當每次調用get的時候返回相同的實例。接着往下看,Guava提供了更簡單的選擇。
一個Suppliers類
正如我們所期望的Guava,有一個Suppliers類的靜態方法來操作Supplier實例。在前面的例子,每次調用get方法都會返回一個新的實例。如果我們想改變我們的實現並且每次返回相同的實例,Suppliers給我們一些可選項。
使用Suppliers.memoize方法
Suppliers.memoize方法返回一個包裝了委托實現的Supplier實例。當第一調用get方法,會被調用真實的Supplier實例的get方法。memoize方法返回被包裝后的Supplier實例。包裝后的Supplier實例會緩存調用返回的結果。后面的調用get方法會返回緩存的實例。我們可以這樣使用Suppliers.memoize方法:
Supplier<Predicate<String>> wrapped = Suppliers.memoize(composedPredicateSupplier);
只增加一行代碼我們就可以返回相同的實例。
使用Suppliers.memoizeWithExpiration方法
Suppliers.memoizeWithExpiration方法與memoize方法工作相同,只不過緩存的對象超過了時間就會返回真實Supplier實例get方法返回的值,在給定的時間當中緩存並且返回
Supplier包裝對象。注意這個實例的緩存不是物理緩存,包裝后的Supplier對象當中有真實Supplier對象的值。 例如:
Supplier<Predicate<String>> wrapped = Suppliers.memoizeWithExpiration(composedPredicateSupplier,10L,TimeUnit.MINUTES);
這里我們包裝了Supplier並且設置了超時時間為10分鍾。對於ComposedPredicateSupplier來說沒什么不同,但是Supplier返回的對象可能不同,可能從數據庫當中恢復,例如memoizeWithExpiration方法會非常有用。
通過依賴注入來使用Supplier接口是強有力的組合。然而,如果你使用Guice(google的依賴注入框架),它包含了Provider<T>接口提供了跟
Supplier<T>接口相同的功能。當然,如果你想利用緩存這個特性你可以使用Supplier接口。
概要
我們看到可以使用Function接口和Predicate接口來在java編程當中添加一些函數方便的功能。Function接口提高給我們轉換對象和Predicate接口可以給我們一個強大的過濾機。Functions和Predicates類也幫助我們寫代碼更加簡單。Suppliers通過提供必要的協作對象,而完全隱藏了這些對象創建的細節。 使用依賴注入框架spring或者guice,這些接口將允許我們無縫地通過簡單地提供不同的實現改變我們的程序的行為。下一章我們講Guava的重點,Collections。