Java開發筆記(六十三)雙冒號標記的方法引用

前面介紹瞭如何本身定義函數式接口,本文接續函數式接口的實現原理,闡述它在數組處理中的實際應用。數組工具Arrays提供了sort方法用於數組元素排序,但是並未提供更豐富的數組加工操做,好比從某個字符串數組中挑選符合條件的字符串並造成新的數組。如今就讓咱們從零開始,利用函數式接口實現數組元素篩選的功能。
首先要定義一個字符串的過濾器接口,該接口內部聲明瞭一個用於字符串匹配的抽象方法,由此構成了以下所示的函數式接口代碼:html

//定義字符串的過濾接口
public interface StringFilter {

	// 聲明一個輸入參數只有源字符串的抽象方法
	public boolean isMatch(String str);
}

接着編寫一個字符串處理工具類,在工具類裏面定義一個字符串數組的篩選方法select,該方法的輸入參數包括原始數組和過濾器實例,方法內部根據過濾器的isMatch函數判斷每一個字符串是否符合篩選條件,並把全部符合條件的字符串從新生成新數組。按此思路實現的工具類代碼以下所示:java

//定義字符串工具類
public class StringUtil {
	
	// 根據過濾器StringFilter從字符串數組挑選符合條件的元素,並重組成新數組返回。
	// 其中StringFilter只對字符串元素自身進行校驗。
	public static String[] select(String[] originArray, StringFilter filter) {
		int count = 0;
		String[] resultArray = new String[0];
		for (String str : originArray) { // 遍歷全部字符串
			if (filter.isMatch(str)) { // 符合過濾條件
				count++;
				// 數組容量增大一個
				resultArray = Arrays.copyOf(resultArray, count);
				// 往數組末尾填入剛纔找到的字符串
				resultArray[count-1] = str;
			}
		}
		return resultArray;
	}
}

 

而後在外部構建原始的字符串數組,並經過StringUtil工具的select方法對其進行數據挑選。爲了能看清過濾器實例的完整面貌,一開始仍是以匿名內部類形式聲明,這樣外部的調用代碼示例以下:正則表達式

	// 在挑選符合條件的數組元素時,可採起方法引用
	private static void testSelect() {
		// 原始的字符串數組
		String[] strArray = { "Hello", "world", "What", "is", "The", "Wether", "today", "" };
		// 篩選後的字符串數組
		String[] resultArray;
		// 採起匿名內部類方式篩選字符串數組
		resultArray = StringUtil.select(strArray, new StringFilter() {
			@Override
			public boolean isMatch(String str) {
				return str.contains("e"); // 是否包含字母e
			}
		});
	}

 

顯然匿名內部類太過囉嗦,僅僅是挑選包含字母「e」的字符串,就得寫上好幾行代碼。俗話說「一回生二回熟」,前面用了許屢次Lambda表達式,如今閉着眼睛就能信手拈來字符串篩選的Lambda代碼,請看如下改寫後的調用代碼:數組

		// 採起Lambda表達式來篩選字符串數組
		resultArray = StringUtil.select(strArray, (str) -> str.contains("e"));
		resultArray = StringUtil.select(strArray, (str) -> str.indexOf("e")>0);
		resultArray = StringUtil.select(strArray, (str) -> str.isEmpty());

 

沒想到俺也把Lambda表達式運用得如此爐火純青了,正所謂「道高一尺魔高一丈」,Lambda表達式當然精煉,可是Java又設計了另外一種更加簡約的寫法,它的大名叫作「方法引用」。以前介紹函數式接口之時,提到Java的輸入參數只能是基本變量類型、某個類、某個接口,總之不能是某個方法,故而必定要經過接口將某個方法包裝起來才行。然而分明僅需某個方法的動做,結果硬要塞給它一個接口對象,實在是強人所難。爲此Java專門提供了「方法引用」,只要符合必定的規則,便可將方法名稱做爲輸入參數傳進去。以上述的字符串篩選爲例,其中的「(str) -> str.isEmpty()」便知足方法引用的規定,則該Lambda表達式可進一步簡化成「String::isEmpty」,就像下面代碼這樣:ide

		// 採起雙冒號的方法引用來篩選字符串數組。只挑選空串
		resultArray = StringUtil.select(strArray, String::isEmpty);

 

可見採起了方法引用的參數格式爲「變量類型::該變量調用的方法名稱」,其中變量類型和方法名稱之間用雙冒號隔開。之因此挑選空串容許寫成方法引用,是由於表達式「(str) -> str.isEmpty()」知足了下列三個條件:
一、裏面的str爲字符串String類型,而且式子右邊調用的isEmpty正好屬於字符串變量的方法;
二、式子左邊有且僅有一個String類型的參數,同時式子右邊有且僅有一行字符串變量的方法調用;
三、isEmpty的返回值爲boolean布爾類型,Lambda表達式對應的匿名方法的返回值也是布爾類型;
既然表達式「(str) -> str.isEmpty()」支持經過方法引用改寫,那麼前兩個式子「(str) -> str.contains("e")」和「(str) -> str.indexOf("e")>0」可否也如法炮製改寫成方法引用呢?惋惜的是,這兩個式子裏的方法有別於isEmpty方法,由於isEmpty方法不帶輸入參數,而無論contains方法仍是indexOf方法都存在輸入參數,要是在select方法中填寫「String::contains」或「String::indexOf」,它倆的輸入參數"e"該往哪裏放?因此必須另外想辦法。就式子「(str) -> str.contains("e")」而言,匿名方法內部的contains僅僅比isEmpty多了個匹配串,能否考慮把這個匹配串單獨拎出來另外定義輸入參數?如此一來,須要修改原先的過濾器接口,給校驗方法isMatch添加一個匹配串參數。因而從新定義的過濾器接口代碼以下所示:函數

//定義字符串的過濾接口2
public interface StringFilter2 {

	// 聲明一個輸入參數包括源字符串和標記串的抽象方法
	public boolean isMatch(String str, String sign);
}

眼瞅着isMatch增長了新參數,工具類StringUtil也得補充對應的挑選方法select2,該方法不但在調用isMatch之時傳入匹配串,並且自身的輸入參數列表也要添加這個匹配串,不然編譯器怎知該匹配串來自何方?下面即是新增的挑選方法代碼例子:工具

	// 根據過濾器StringFilter2從字符串數組挑選符合條件的元素,並重組成新數組返回。
	// 其中StringFilter2根據標記串對字符串元素進行校驗。
	public static String[] select2(String[] originArray, StringFilter2 filter, String sign) {
		int count = 0;
		String[] resultArray = new String[0];
		for (String str : originArray) { // 遍歷全部字符串
			if (filter.isMatch(str, sign)) { // 符合過濾條件
				count++;
				// 數組容量增大一個
				resultArray = Arrays.copyOf(resultArray, count);
				// 往數組末尾填入剛纔找到的字符串
				resultArray[count-1] = str;
			}
		}
		return resultArray;
	}

 

如今回到外部篩選字符串數組的地方,此時外部調用StringUtil工具的select2方法,終於能夠將方法引用「String::contains」冠冕堂皇傳進去了,同時select2方法的第三個參數填寫contains所需的匹配串。推而廣之,不僅僅是contains方法,String類型的startsWith方法和endsWith方法也支持採起方法引用的形式,這三個方法的引用代碼示例以下:測試

		// 被引用的方法存在輸入參數,則將該參數挪到挑選方法select2的後面。只挑選包含字母o的串
		resultArray = StringUtil.select2(strArray, String::contains, "o");
		print(resultArray, "contains方法");
		// 被引用的方法換成了startsWith。只挑選以字母W開頭的串
		resultArray = StringUtil.select2(strArray, String::startsWith, "W");
		print(resultArray, "startsWith方法");
		// 被引用的方法換成了endsWith。只挑選以字母y結尾的串
		resultArray = StringUtil.select2(strArray, String::endsWith, "y");
		print(resultArray, "endsWith方法");

 

運行上述包含方法引用的測試代碼,觀察到如下的日誌信息,可見字符串篩選方法運行正常:設計

contains方法的挑選結果爲:Hello, world, today, 
startsWith方法的挑選結果爲:What, Wether, 
endsWith方法的挑選結果爲:today, 

 

不料indexOf方法並不適用於方法引用,緣於式子「(str) -> str.indexOf("e")>0」多了個「>0」的判斷,要知道方法引用的條件很是嚴格,符合條件的表達式只能有方法自身,不容許出現其它額外的邏輯運算。被引用方法的輸入參數尚能經過給過濾器添加參數來實現,多出來的邏輯運算可就無能爲力了。不過對於字符串的篩選過程來講,更復雜的條件判斷徹底可以交給正則匹配方法matches,只要給定待篩選的字符串格式規則,那麼matches方法就能夠自動校驗某個字符串是否符合正則條件了。假如要挑選首字母爲w或者W的字符串數組,則採起方法引用的matches調用代碼以下所示:日誌

		// 如需對字符串進行更復雜的條件篩選,可利用matches方法經過正則表達式來校驗
		resultArray = StringUtil.select2(strArray, String::matches, "[wW][a-zA-Z]*");
		print(resultArray, "matches方法");

 

再來運行上面的測試代碼,日誌結果顯示字符串篩選的結果符合預期:

matches方法的挑選結果爲:world, What, Wether, 

除了字符串數組的過濾功能,方法引用還能用於字符串數組的排序操做,正如你們熟悉的比較器接口Comparator。Arrays工具的sort方法,在判斷兩個字符串的前後順序之時,默認經過它們的首字母進行比較,也就是調用字符串類型的compareTo方法。使用sort方法給字符串數組排序,用到的比較器既支持以匿名內部類方式書寫,又支持以Lambda表達式書寫,合併了兩種方式的排序代碼見下:

	// 在對字符串數組排序時,也可採起方法引用
	private static void testCompare() {
		String[] strArray = { "Hello", "world", "What", "is", "The", "Wether", "today" };
		// 採起匿名內部類方式對字符串數組進行默認的排序操做
		Arrays.sort(strArray, new Comparator<String>() {
			@Override
			public int compare(String o1, String o2) {
				return o1.compareTo(o2);
			}
		});
		// 採起Lambda表達式對字符串數組進行默認的排序操做
		Arrays.sort(strArray, (o1, o2) -> o1.compareTo(o2));
		print(strArray, "字符串數組按首字母不區分大小寫");
	}

 

從上面排序方法用到的Lambda表達式可知,該式子對應的匿名方法有o1和o2兩個輸入參數,它們的數據類型都是String。相比之下,以前介紹字符串數組的挑選功能時,採用的過濾器內部方法isMatch只有一個字符串參數。過濾器和比較器的共同點在於,不論是隻有一個入參,仍是有兩個入參,它們的處理方法內部都用到了惟一的字符串方法,前者是contains方法,然後者是compareTo方法。所以,比較器的匿名方法也容許改寫成方法引用,反正編譯器曉得該怎麼辦就行,因而修改以後的方法引用代碼以下所示:

		// 由於compareTo先後的兩個變量都是數組的字符串元素,
		// 因此可直接簡寫爲該方法的引用形式,反正編譯器曉得該怎麼調用
		Arrays.sort(strArray, String::compareTo);
		print(strArray, "字符串數組按首字母拼寫順序");

 

運行以上的排序代碼,獲得下面的日誌結果,可見compareTo方法會把首字母大寫的字符串排在前面,把首字母小寫的字符串排在後面:

字符串數組按首字母拼寫順序的挑選結果爲:Hello, The, Wether, What, is, today, world, 

 

與compareTo類似的方法還有compareToIgnoreCase,不過該方法在比較字符串首字母時忽略了大小寫。利用compareToIgnoreCase進行排序的方法引用代碼示例以下:

		//Arrays.sort(strArray, (s1,s2) -> s1.compareToIgnoreCase(s2));
		// 把compareTo方法換成compareToIgnoreCase方法,表示首字母不區分大小寫
		Arrays.sort(strArray, String::compareToIgnoreCase);
		print(strArray, "字符串數組按首字母不區分大小寫");

 

再次運行新寫的排序代碼,從輸入的日誌信息可知,compareToIgnoreCase比較首字母時的確忽略了大小寫的區別:

字符串數組按首字母不區分大小寫的挑選結果爲:Hello, is, The, today, Wether, What, world, 

  

更多Java技術文章參見《Java開發筆記(序)章節目錄

相關文章
相關標籤/搜索