Java開發筆記(七十)Java8新增的幾種泛型接口

因爲泛型存在某種不肯定的類型,所以不多直接運用於拿來即用的泛型類,它更常常以泛型接口的面目出現。例如幾種基本的容器類型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開發筆記(序)章節目錄

相關文章
相關標籤/搜索