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开发笔记(序)章节目录》