前面介绍了如何自己定义函数式接口,本文接续函数式接口的实现原理,阐述它在数组处理中的实际应用。数组工具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开发笔记(序)章节目录

Java开发笔记(六十三)双冒号标记的方法引用的更多相关文章

  1. Java开发笔记(十三)利用关系运算符比较大小

    前面在<Java开发笔记(九)赋值运算符及其演化>中提到,Java编程中的等号“=”表示赋值操作,并非数学上的等式涵义.Java通过等式符号“==”表示左右两边相等,对应数学的等号“=”: ...

  2. Java开发笔记(序)章节目录

    现将本博客的Java学习文章整理成以下笔记目录,方便查阅. 第一章 初识JavaJava开发笔记(一)第一个Java程序Java开发笔记(二)Java工程的帝国区划Java开发笔记(三)Java帝国的 ...

  3. Java开发笔记(八十六)通过缓冲区读写文件

    前面介绍了利用文件写入器和文件读取器来读写文件,因为FileWriter与FileReader读写的数据以字符为单位,所以这种读写文件的方式被称作“字符流I/O”,其中字母I代表输入Input,字母O ...

  4. Java开发笔记(二十三)数组工具Arrays

    数组作为一种组合形式的数据类型,必然要求提供一些处理数组的简便办法,包括数组比较.数组复制.数组排序等等.为此Java专门设计了Arrays工具,该工具包含了几个常用方法,方便程序员对数组进行加工操作 ...

  5. Java开发笔记(六十四)静态方法引用和实例方法引用

    前面介绍了方法引用的概念及其业务场景,虽然在所列举的案例之中方法引用确实好用,但是显而易见这些案例的适用场合非常狭窄,因为被引用的方法必须属于外层匿名方法(即Lambda表达式)的数据类型,像isEm ...

  6. Java开发笔记(六十一)Lambda表达式

    前面介绍了匿名内部类的简单用法,通过在sort方法中运用匿名内部类,不但能够简化代码数量,还能保持业务代码的连续性.只是匿名内部类的结构仍显啰嗦,虽然它省去了内部类的名称,但是花括号里面的方法定义代码 ...

  7. Java开发笔记(六十七)清单:ArrayList和LinkedList

    前面介绍了集合与映射两类容器,它们的共同特点是每个元素都是唯一的,并且采用二叉树方式的类型还自带有序性.然而这两个特点也存在弊端:其一,为啥内部元素必须是唯一的呢?像手机店卖出了两部Mate20,虽然 ...

  8. Java开发笔记(七十三)常见的程序异常

    一个程序开发出来之后,无论是用户还是程序员,都希望它稳定地运行,然而程序毕竟是人写的,人无完人哪能不犯点错误呢?就算事先考虑得天衣无缝,揣着一笔巨款跑去岛国买了栋抗震性能良好的海边别墅,谁料人算不如天 ...

  9. Java开发笔记(九十六)线程的基本用法

    每启动一个程序,操作系统的内存中通常会驻留该程序的一个进程,进程包含了程序的完整代码逻辑.一旦程序退出,进程也就随之结束:反之,一旦强行结束进程,程序也会跟着退出.普通的程序代码是从上往下执行的,遇到 ...

随机推荐

  1. let和const

    ES6新增了let取代var,let主要有以下特点. 1 只在代码块内有效,代码块外不能使用let声明的变量.let很适合声明循环体的变量. 它可以解决一些闭包的问题存在的问题比如: var a = ...

  2. 把一下程序中的print()函数改写成

    源代码: #include <iostream> using namespace std; void print( int w ) { ; i <= w ; i++ ) { ; j ...

  3. Vue 单文件原件 — vCheckBox

    简书原文 做东西一向奉行的是致简原则,一定要让使用者简单 这是我在使用 Vue 一段时间后尝试制作的一个小玩意 我希望可以做一堆这样的小玩意,随意组合使用,感觉挺好的 源码在最后 演示DEMO 示例: ...

  4. Android 音视频开发(三):使用 AudioTrack 播放PCM音频

    一.AudioTrack 基本使用 AudioTrack 类可以完成Android平台上音频数据的输出任务.AudioTrack有两种数据加载模式(MODE_STREAM和MODE_STATIC),对 ...

  5. 再谈反向传播(Back Propagation)

    此前写过一篇<BP算法基本原理推导----<机器学习>笔记>,但是感觉满纸公式,而且没有讲到BP算法的精妙之处,所以找了一些资料,加上自己的理解,再来谈一下BP.如有什么疏漏或 ...

  6. OpenOCD的概念,安装和使用

    概念: OpenOCD是一个运行于PC上的开源调试软件,它可以控制包括Wiggler之内的很多JTAG硬件:我们可以将它理解为一种GDB服务程序.OpenOCD的源码只能通过SVN下载,地址是:svn ...

  7. 当使用vue的按键修饰符不起效果的时候怎么办?如@keyup.enter = '' ;

    这个问题困扰了我一个多小时,各种测bug !始终测不出来! 直接上代码(错误示范) <el-form-item prop="password"> <el-inpu ...

  8. 剖析项目多个logback配置(下)

    来源:http://www.cnblogs.com/guozp/p/5973038.html 上篇大概描述了logback的加载顺序以及加载的源码,本篇将分析如果在你的Maven或者其他多模块的项目中 ...

  9. SpringCloud(6)---熔断降级理解、Hystrix实战

    SpringCloud(6)---熔断降级理解.Hystrix实战 一.概念 1.为什么需要熔断降级 (1)需求背景 它是系统负载过高,突发流量或者网络等各种异常情况介绍,常用的解决方案. 在一个分布 ...

  10. Javascript的原型继承,说清楚

    一直以来对Javascript的原型.原型链.继承等东西都只是会用和了解,但没有深入去理解这门语言关于继承这方面的本质和特点.闲暇之余做的理解和总结,欢迎各位朋友一起讨论. 本文本主要从两段代码的区别 ...