因爲泛型存在某種不肯定的類型,所以不多直接運用於拿來即用的泛型類,它更常常以泛型接口的面目出現。例如幾種基本的容器類型Set、Map、List都被定義爲接口interface,像HashSet、TreeMap、LinkedList等等只是實現了對應容器接口的具體類罷了。泛型的用途各式各樣,近的不說,遠的如數組工具Arrays的sort方法,它在排序時用到的比較器Comparator就是個泛型接口。別看Comparator.java的源碼洋洋灑灑數百行,其實它的精華部分僅僅下列寥寥數行:html
//數組排序須要的比較器主要代碼,可見它是個泛型接口 public interface Comparator<T> { int compare(T o1, T o2); }
固然系統提供的泛型接口不止是Comparator一個,從Java8開始,又新增了好幾個系統自帶的泛型接口,它們的適用範圍各有千秋,接下來便分別加以介紹。java
一、斷言接口Predicate
以前介紹方法引用的時候,要求從一個字符串數組中挑選出符合條件的元素生成新數組,爲此定義一個過濾器接口StringFilter,該接口聲明瞭字符串匹配方法isMatch,而後再利用該過濾器編寫字符串數組的篩選方法,進而由外部經過Lambda表達式或者方法引用來進行過濾。但是StringFilter這個過濾器只能用於篩選字符串,不能用來篩選其它數據類型。若想讓它支持全部類型的數據篩選,勢必要把數據類型空泛化,Java8推出的斷言接口Predicate正是這種用於匹配校驗的泛型接口。
在詳細說明Predicate以前,先定義一個蘋果類Apple,本文的幾個泛型接口都準備拿蘋果類練手,它的類定義代碼以下所示:數組
//定義一個蘋果類 public class Apple { private String name; // 名稱 private String color; // 顏色 private Double weight; // 重量 private Double price; // 價格 public Apple(String name, String color, Double weight, Double price) { this.name = name; this.color = color; this.weight = weight; this.price = price; } // 爲節省篇幅,此處省略每一個成員屬性的get/set方法 // 獲取該蘋果的詳細描述文字 public String toString() { return String.format("\n(name=%s,color=%s,weight=%f,price=%f)", name, color, weight, price); } // 判斷是否紅蘋果 public boolean isRedApple() { return this.color.toLowerCase().equals("red"); } }
接着構建一個填入若干蘋果信息的初始清單,幾種泛型接口準備對蘋果清單磨刀霍霍,清單數據的構建代碼示例以下:app
// 獲取默認的蘋果清單 private static List<Apple> getAppleList() { // 數組工具Arrays的asList方法能夠把一系列元素直接賦值給清單對象 List<Apple> appleList = Arrays.asList( new Apple("紅蘋果", "RED", 150d, 10d), new Apple("大蘋果", "green", 250d, 10d), new Apple("紅蘋果", "red", 300d, 10d), new Apple("大蘋果", "yellow", 200d, 10d), new Apple("紅蘋果", "green", 100d, 10d), new Apple("大蘋果", "Red", 250d, 10d)); return appleList; }
而後當前的主角——斷言接口終於登場了,別看「斷言」二字彷佛很嚇人,其實它的關鍵代碼也只有如下幾行,真正有用的就是校驗方法test:ide
public interface Predicate<T> { boolean test(T t); }
再定義一個清單過濾的泛型方法,輸入原始清單和斷言實例,輸出篩選後符合條件的新清單。過濾方法的處理邏輯很簡單,僅僅要求遍歷清單的全部元素,一旦經過斷言實例的test方法檢驗,就把該元素添加到新的清單。具體的過濾代碼以下所示:函數
// 利用系統自帶的斷言接口Predicate,對某個清單裏的元素進行過濾 private static <T> List<T> filterByPredicate(List<T> list, Predicate<T> p) { List<T> result = new ArrayList<T>(); for (T t : list) { if (p.test(t)) { // 若是知足斷言的測試條件,則把該元素添加到新的清單 result.add(t); } } return result; }
終於輪到外部調用剛纔的過濾方法了,如今要求從原始的蘋果清單中挑出全部的紅蘋果,爲了更直觀地理解泛型接口的運用,先經過匿名內部類方式來表達Predicate實例。此時的調用代碼是下面這樣的:工具
// 測試系統自帶的斷言接口Predicate private static void testPredicate() { List<Apple> appleList = getAppleList(); // 第一種調用方式:匿名內部類實現Predicate。挑出全部的紅蘋果 List<Apple> redAppleList = filterByPredicate(appleList, new Predicate<Apple>() { @Override public boolean test(Apple t) { return t.isRedApple(); } }); System.out.println("紅蘋果清單:" + redAppleList.toString()); }
運行上述的測試代碼,從輸出的日誌信息可知,經過斷言接口正確篩選到了紅蘋果清單:測試
紅蘋果清單:[ (name=紅蘋果,color=RED,weight=150.000000,price=10.000000), (name=紅蘋果,color=red,weight=300.000000,price=10.000000), (name=大蘋果,color=Red,weight=250.000000,price=10.000000)]
顯然匿名內部類的實現代碼過於冗長,改寫爲Lambda表達式的話僅有如下一行代碼:this
// 第二種調用方式:Lambda表達式實現Predicate List<Apple> redAppleList = filterByPredicate(appleList, t -> t.isRedApple());
或者採起方法引用的形式,也只需下列的一行代碼:編碼
// 第三種調用方式:經過方法引用實現Predicate List<Apple> redAppleList = filterByPredicate(appleList, Apple::isRedApple);
除了挑選紅蘋果,還能夠挑選大個的蘋果,好比要挑出全部重量大於半斤的蘋果,則採起Lambda表達式的的調用代碼見下:
// Lambda表達式實現Predicate。挑出全部重量大於半斤的蘋果 List<Apple> heavyAppleList = filterByPredicate(appleList, t -> t.getWeight() >= 250); System.out.println("重蘋果清單:" + heavyAppleList.toString());
以上的代碼演示結果,充分說明了斷言接口徹底適用於過濾判斷及篩選操做。
二、消費接口Consumer
斷言接口只進行邏輯判斷,不涉及到數據修改,若要修改清單裏的元素,就用到了另外一個消費接口Consumer。譬以下館子消費,把肚子撐大了;又如去超市消費,手上多了裝滿商品的購物袋;所以消費行爲理應伴隨着某些屬性的變動,變大或變小,變多或變少。Consumer一樣屬於泛型接口,它的核心代碼也只有如下區區幾行:
public interface Consumer<T> { void accept(T t); }
接着將消費接口做用於清單對象,意圖修改清單元素的某些屬性,那麼得定義泛型方法modifyByConsumer,根據輸入的清單數據和消費實例,從而對清單執行指定的消費行爲。詳細的修改方法示例以下:
// 利用系統自帶的消費接口Consumer,對某個清單裏的元素進行修改 private static <T> void modifyByConsumer(List<T> list, Consumer<T> c) { for (T t : list) { // 根據輸入的消費指令接受變動,所謂消費,通俗地說,就是女人花錢打扮本身。 // 下面的t既是輸入參數,又容許修改。 c.accept(t); // 若是t是String類型,那麼accept方法不能真正修改字符串 } }
消費行爲仍然拿蘋果清單小試牛刀,外部調用modifyByConsumer方法之時,傳入的消費實例要給蘋果名稱加上「好吃」二字。下面即是具體的調用代碼例子,其中一塊列出了匿名內部類與Lambda表達式這兩種寫法:
// 測試系統自帶的消費接口Consumer private static void testConsumer() { List<Apple> appleList = getAppleList(); // 第一種調用方式:匿名內部類實現Consumer。在蘋果名稱後面加上「好吃」二字 modifyByConsumer(appleList, new Consumer<Apple>() { @Override public void accept(Apple t) { t.setName(t.getName() + "好吃"); } }); // 第二種調用方式:Lambda表達式實現Consumer modifyByConsumer(appleList, t -> t.setName(t.getName() + "好吃")); System.out.println("好吃的蘋果清單" + appleList.toString()); }
運行上面的調用代碼,可見輸入的日誌記錄果真給蘋果名稱補充了兩遍「好吃」:
好吃的蘋果清單[ (name=紅蘋果好吃好吃,color=RED,weight=150.000000,price=10.000000), (name=大蘋果好吃好吃,color=green,weight=250.000000,price=10.000000), (name=紅蘋果好吃好吃,color=red,weight=300.000000,price=10.000000), (name=大蘋果好吃好吃,color=yellow,weight=200.000000,price=10.000000), (name=紅蘋果好吃好吃,color=green,weight=100.000000,price=10.000000), (name=大蘋果好吃好吃,color=Red,weight=250.000000,price=10.000000)]
不過單獨使用消費接口的話,只能把清單裏的每一個元素所有修改過去,不加甄別的作法顯然太粗暴了。更好的辦法是挑出符合條件的元素再作變動,如此一來就得聯合運用斷言接口與消費接口,先經過斷言接口Predicate篩選目標元素,再經過消費接口Consumer處理目標元素。因而結合兩種泛型接口的泛型方法就變成了如下這般代碼:
// 聯合運用Predicate和Consumer,可篩選出某些元素並給它們整容 private static <T> void selectAndModify(List<T> list, Predicate<T> p, Consumer<T> c) { for (T t : list) { if (p.test(t)) { // 若是知足斷言的條件要求, c.accept(t); // 就把該元素送去美容院整容。 } } }
針對特定的記錄再做調整,正是實際業務場景中的常見作法。好比現有一堆蘋果,由於每一個蘋果的質量良莠不齊,因此要對蘋果分類訂價。通常的蘋果每公斤賣10塊錢,若是是紅彤彤的蘋果,則單價提升50%;若是蘋果個頭很大(重量大於半斤),則單價也提升50%;又紅又大的蘋果想都不要想確定特別吃香,算下來它的單價足足是通常蘋果的1.5*1.5=2.25倍了。那麼調整蘋果訂價的代碼邏輯就得前後調用兩次selectAndModify方法,第一次用來調整紅蘋果的價格,第二次用來調整大蘋果的價格,完整的價格調整代碼以下所示:
// 聯合測試斷言接口Predicate和消費接口Consumer private static void testPredicateAndConsumer() { List<Apple> appleList = getAppleList(); // 若是是紅蘋果,就漲價五成 selectAndModify(appleList, t -> t.isRedApple(), t -> t.setPrice(t.getPrice() * 1.5)); // 若是重量大於半斤,再漲價五成 selectAndModify(appleList, t -> t.getWeight() >= 250, t -> t.setPrice(t.getPrice() * 1.5)); System.out.println("漲價後的蘋果清單:" + appleList.toString()); }
運行以上的價格調整代碼,從如下輸出的日誌結果可知,每一個蘋果的單價都通過計算從新改過了:
漲價後的蘋果清單:[ (name=紅蘋果,color=RED,weight=150.000000,price=15.000000), (name=大蘋果,color=green,weight=250.000000,price=15.000000), (name=紅蘋果,color=red,weight=300.000000,price=22.500000), (name=大蘋果,color=yellow,weight=200.000000,price=10.000000), (name=紅蘋果,color=green,weight=100.000000,price=10.000000), (name=大蘋果,color=Red,weight=250.000000,price=22.500000)]
三、函數接口Function
剛纔聯合斷言接口和消費接口,順利實現了修改部分元素的功能,然而這種作法存在問題,就是直接在原清單上面進行修改,一方面破壞了原始數據,另外一方面仍未抽取到新清單。因而Java又設計了泛型的函數接口Function,且看它的泛型接口定義代碼:
public interface Function<T, R> { R apply(T t); }
從Function的定義代碼可知,該接口不但支持輸入某個泛型變量,也支持返回另外一個泛型變量。這樣的話,把輸入參數同輸出參數區分開,就避免了兩者的數據處理操做發生干擾。據此可編寫新的泛型方法recycleByFunction,該方法輸入原始清單和函數實例,輸出處理後的新清單,從而知足了數據抽取的功能需求。詳細的方法代碼示例以下:
// 利用系統自帶的函數接口Function,把全部元素進行處理後加到新的清單裏面 private static <T, R> List<R> recycleByFunction(List<T> list, Function<T, R> f) { List<R> result = new ArrayList<R>(); for (T t : list) { R r = f.apply(t); // 把原始材料t加工一番後輸出成品r result.add(r); // 把成品r添加到新的清單 } return result; }
接下來由外部去調用新定義的recycleByFunction方法,照舊採起匿名內部類與Lambda表達式同時進行編碼,輪番對紅蘋果和大蘋果漲價,修改後的調用代碼例子見下:
// 測試系統自帶的函數接口Function private static void testFunction() { List<Apple> appleList = getAppleList(); List<Apple> appleRecentList; // 第一種調用方式:匿名內部類實現Function。把漲價後的蘋果放到新的清單之中 appleRecentList = recycleByFunction(appleList, new Function<Apple, Apple>() { @Override public Apple apply(Apple t) { Apple apple = new Apple(t.getName(), t.getColor(), t.getWeight(), t.getPrice()); if (apple.isRedApple()) { // 若是是紅蘋果,就漲價五成 apple.setPrice(apple.getPrice() * 1.5); } if (apple.getWeight() >= 250) { // 若是重量大於半斤,再漲價五成 apple.setPrice(apple.getPrice() * 1.5); } return apple; } }); // 第二種調用方式:Lambda表達式實現Function appleRecentList = recycleByFunction(appleList, t -> { Apple apple = new Apple(t.getName(), t.getColor(), t.getWeight(), t.getPrice()); if (apple.isRedApple()) { // 若是是紅蘋果,就漲價五成 apple.setPrice(apple.getPrice() * 1.5); } if (apple.getWeight() >= 250) { // 若是重量大於半斤,再漲價五成 apple.setPrice(apple.getPrice() * 1.5); } return apple; }); System.out.println("漲價後的新蘋果清單:" + appleRecentList.toString()); }
注意到上面的例子代碼中,函數接口的入參類型爲Apple,而出參類型也爲Apple。假設出參類型不是Apple,而是別的類型如String,那該當若何?其實很簡單,只要函數接口的返回參數改爲其它類型就行了。譬如如今無需返回蘋果的完整清單,只需返回蘋果的名稱清單,則調用代碼可調整爲下面這樣:
// 返回的清單類型可能與原清單類型不一樣,好比只返回蘋果名稱 List<String> colorList = recycleByFunction(appleList, t -> t.getName() + "(" + t.getColor() + ")"); System.out.println("帶顏色的蘋果名稱清單:" + colorList.toString());
運行以上的調整代碼,果真打印了以下的蘋果名稱清單日誌:
帶顏色的蘋果名稱清單:[紅蘋果(RED), 大蘋果(green), 紅蘋果(red), 大蘋果(yellow), 紅蘋果(green), 大蘋果(Red)]
更多Java技術文章參見《Java開發筆記(序)章節目錄》