Java开发笔记(六十三)双冒号标记的方法引用

前面介绍了如何自己定义函数式接口,本文接续函数式接口的实现原理,阐述它在数组处理中的实际应用。数组工具Arrays提供了sort方法用于数组元素排序,可是并未提供更丰富的数组加工操作,比如从某个字符串数组中挑选符合条件的字符串并形成新的数组。现在就让我们从零开始,利用函数式接口实现数组元素筛选的功能。
首先要定义一个字符串的过滤器接口,该接口内部声明了一个用于字符串匹配的抽象方法,由此构成了如下所示的函数式接口代码:

//定义字符串的过滤接口
public interface StringFilter {

	// 声明一个输入参数只有源字符串的抽象方法
	public boolean isMatch(String str);
}

接着编写一个字符串处理工具类,在工具类里面定义一个字符串数组的筛选方法select,该方法的输入参数包括原始数组和过滤器实例,方法内部根据过滤器的isMatch函数判断每个字符串是否符合筛选条件,并把所有符合条件的字符串重新生成新数组。按此思路实现的工具类代码如下所示:

//定义字符串工具类
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”,就像下面代码这样:

		// 采取双冒号的方法引用来筛选字符串数组。只挑选空串
		resultArray = StringUtil.select(strArray, String::isEmpty);

 

可见采取了方法引用的参数格式为“变量类型::该变量调用的方法名称”,其中变量类型和方法名称之间用双冒号隔开。之所以挑选空串允许写成方法引用,是因为表达式“(str) -> str.isEmpty()”满足了下列三个条件:
1、里面的str为字符串String类型,并且式子右边调用的isEmpty正好属于字符串变量的方法;
2、式子左边有且仅有一个String类型的参数,同时式子右边有且仅有一行字符串变量的方法调用;
3、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开发笔记(序)章节目录

posted @ 2019-02-23 13:13  pinlantu  阅读(599)  评论(0编辑  收藏  举报