写在前面:
本篇只对Java8实际使用做一个总结,至于概念性的问题请自行阅读“Java8实战”书籍。
1.通过行为参数化传递代码
1.1 应对不断变化的需求
1.1.1 初试牛刀:筛选绿苹果1.1.2 再展身手:把颜色作为参数1.1.3 第三次尝试:对你能想到的每个属性做筛选 1.2 行为参数化
1.2.1 第四次尝试:根据抽象条件筛选 1.3 对付啰嗦
1.3.1 匿名类1.3.2 第五次尝试:使用匿名类1.3.3 第六次尝试:使用 Lambda 表达式1.3.4 第七次尝试:将 List 类型抽象化 1.4 真实的例子
1.4.1 用 Comparator 来排序1.4.2 用 Runnable 执行代码块 2.Lambda表达式
2.1 Lambda 管中窥豹2.2 在哪里以及如何使用 Lambda
2.2.1 函数式接口2.2.2 函数描述符 2.3 使用函数式接口
2.3.1 Predicate2.3.2 Consumer2.3.3 Function
1.通过行为参数化传递代码在软件工程中,一个众所周知的问题就是,不管你做什么,用户的需求肯定会变。比方说,
有个应用程序是帮助农民了解自己的库存的。这位农民可能想有一个查找库存中所有绿色苹果的
功能。但到了第二天,他可能会告诉你:“其实我还想找出所有重量超过150克的苹果。”又过了
两天,农民又跑回来补充道:“要是我可以找出所有既是绿色,重量也超过150克的苹果,那就太
棒了。”你要如何应对这样不断变化的需求?理想的状态下,应该把你的工作量降到最少。
1.1 应对不断变化的需求行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味
着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,
这意味着你可以推迟这块代码的执行。例如,你可以将代码块作为参数传递给另一个方法,稍后
再去执行它。这样,这个方法的行为就基于那块代码被参数化了。
1.1.1 初试牛刀:筛选绿苹果编写能够应对变化的需求的代码并不容易。让我们来看一个例子,我们会逐步改进这个例子,
以展示一些让代码更灵活的最佳做法。就农场库存程序而言,你必须实现一个从列表中筛选绿苹
果的功能。听起来很简单吧?
第一个解决方案可能是下面这样的:
public static ListfilterGreenApples(List inventory) { List result = new ArrayList ();//累积苹果的列表 for(Apple apple: inventory){ if( "green".equals(apple.getColor() ) {//仅仅选出绿苹果 result.add(apple); } } return result; }
筛选绿苹果的代码写好了。但是现在农民改主意了,他还想要筛选红苹果。你该怎么做呢?简单的解决办法就是复制这个方法,把名字改成filterRedApples,然后更改if条件来匹配红苹果。然而,要是农民想要筛选多种颜色:浅绿色、暗红色、黄色等,这种方法就应付不了了。一个良好的原则是在编写类似的代码之后,尝试将其抽象化。
1.1.2 再展身手:把颜色作为参数public static ListfilterApplesByColor(List inventory,String color) { List result = new ArrayList (); for (Apple apple: inventory){ if ( apple.getColor().equals(color) ) { result.add(apple); } } return result; }
现在,只要像下面这样调用方法,农民朋友就会满意了:
ListgreenApples = filterApplesByColor(inventory, "green"); List redApples = filterApplesByColor(inventory, "red");
太简单了对吧?让我们把例子再弄得复杂一点儿。这位农民又跑回来和你说:“要是能区分
轻的苹果和重的苹果就太好了。重的苹果一般是重量大于150克。”
作为软件工程师,你早就想到农民可能会要改变重量,于是你写了下面的方法,用另一个参
数来应对不同的重量:
public static ListfilterApplesByWeight(List inventory, int weight) { List result = new ArrayList (); For (Apple apple: inventory){ if ( apple.getWeight() > weight ){ result.add(apple); } } return result; }
解决方案不错,但是请注意,你复制了大部分的代码来实现遍历库存,并对每个苹果应用筛
选条件。这有点儿令人失望,因为它打破了DRY(Don’t Repeat Yourself,不要重复自己)的软件
工程原则。如果你想要改变筛选遍历方式来提升性能呢?那就得修改所有方法的实现,而不是只
改一个。从工程工作量的角度来看,这代价太大了。
你可以将颜色和重量结合为一个方法,称为filter。不过就算这样,你还是需要一种方式
来区分想要筛选哪个属性。你可以加上一个标志来区分对颜色和重量的查询(但绝不要这样做!
我们很快会解释为什么)。
一种把所有属性结合起来的笨拙尝试如下所示:
public static ListfilterApples(List inventory, String color, int weight, boolean flag) { List result = new ArrayList (); for (Apple apple: inventory){ if ( (flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight) ){ result.add(apple); } } return result; }
你可以这么用(但真的很笨拙):
ListgreenApples = filterApples(inventory, "green", 0, true); List heavyApples = filterApples(inventory, "", 150, false);
这个解决方案再差不过了。首先,客户端代码看上去糟透了。true和false是什么意思?此
外,这个解决方案还是不能很好地应对变化的需求。如果这位农民要求你对苹果的不同属性做筛
选,比如大小、形状、产地等,又怎么办?而且,如果农民要求你组合属性,做更复杂的查询,
比如绿色的重苹果,又该怎么办?你会有好多个重复的filter方法,或一个巨大的非常复杂的
方法。到目前为止,你已经给filterApples方法加上了值(比如String、Integer或boolean)
的参数。这对于某些确定性问题可能还不错。但如今这种情况下,你需要一种更好的方式,来把
苹果的选择标准告诉你的filterApples方法。在下一节中,我们会介绍了如何利用行为参数化
实现这种灵活性。
你在上一节中已经看到了,你需要一种比添加很多参数更好的方法来应对变化的需求。让
我们后退一步来看看更高层次的抽象。一种可能的解决方案是对你的选择标准建模:你考虑的
是苹果,需要根据Apple的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个
boolean值。我们把它称为谓词(即一个返回boolean值的函数)。让我们定义一个接口来对选
择标准建模:
public interface ApplePredicate{
boolean test (Apple apple);
}
现在你就可以用ApplePredicate的多个实现代表不同的选择标准了。
//仅仅选出重的苹果
public class AppleHeavyWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWeight() > 150;
}
}
//仅仅选出绿苹果
public class AppleGreenColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "green".equals(apple.getColor());
}
}
你可以把这些标准看作filter方法的不同行为。你刚做的这些和“策略设计模式”相关,
它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,
算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。
但是,该怎么利用ApplePredicate的不同实现呢?你需要filterApples方法接受
ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(或战
略)作为参数,并在内部使用,来完成不同的行为。
要在我们的例子中实现这一点,你要给filterApples方法添加一个参数,让它接受
ApplePredicate对象。这在软件工程上有很大好处:现在你把filterApples方法迭代集合的
逻辑与你要应用到集合中每个元素的行为(这里是一个谓词)区分开了。
利用ApplePredicate改过之后,filter方法看起来是这样的:
public static ListfilterApples(List inventory, ApplePredicate p){ List result = new ArrayList<>(); for(Apple apple: inventory){ if(p.test(apple)){ result.add(apple); } } return result; }
1. 传递代码/行为
这里值得停下来小小地庆祝一下。这段代码比我们第一次尝试的时候灵活多了,读起来、用
起来也更容易!现在你可以创建不同的ApplePredicate对象,并将它们传递给filterApples
方法。免费的灵活性!比如,如果农民让你找出所有重量超过150克的红苹果,你只需要创建一
个类来实现ApplePredicate就行了。你的代码现在足够灵活,可以应对任何涉及苹果属性的需
求变更了:
public class AppleRedAndHeavyPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "red".equals(apple.getColor()) && apple.getWeight() > 150;
}
}
List redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());
你已经做成了一件很酷的事:filterApples方法的行为取决于你通过ApplePredicate对象传递的代码。换句话说,你把filterApples方法的行为参数化了!
请注意,在上一个例子中,唯一重要的代码是test方法的实现;正是它定义
了filterApples方法的新行为。但令人遗憾的是,由于该filterApples方法只能接受对象,
所以你必须把代码包裹在ApplePredicate对象里。你的做法就类似于在内联“传递代码”,因
为你是通过一个实现了test方法的对象来传递布尔表达式的。而通过使用Lambda,你可以直接把表达式"red".equals(apple.getColor()) &&apple.getWeight() > 150传递给filterApples方法,而无需定义多个ApplePredicate类,从而去掉不必要的代码。
2. 多种行为,一个参数
正如我们先前解释的那样,行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集
合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不
同的目的。
我们都知道,人们都不愿意用那些很麻烦的功能或概念。目前,当要把新的行为传递给
filterApples方法的时候,你不得不声明好几个实现ApplePredicate接口的类,然后实例化
好几个只会提到一次的ApplePredicate对象。下面的程序总结了你目前看到的一切。这真是很
啰嗦,很费时间!
行为参数化:用谓词筛选苹果
//选择较重苹果的谓词
public class AppleHeavyWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWeight() > 150;
}
}
//选择绿苹果的谓词
public class AppleGreenColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "green".equals(apple.getColor());
}
}
public class FilteringApples{
public static void main(String...args){
List inventory = Arrays.asList(new Apple(80,"green"),
new Apple(155, "green"),
new Apple(120, "red"));
List heavyApples =
filterApples(inventory, new AppleHeavyWeightPredicate());
List greenApples =
filterApples(inventory, new AppleGreenColorPredicate());
}
public static List filterApples(List inventory,
ApplePredicate p) {
List result = new ArrayList<>();
for (Apple apple : inventory){
if (p.test(apple)){
result.add(apple);
}
}
return result;
}
}
费这么大劲儿真没必要,能不能做得更好呢?Java有一个机制称为匿名类,它可以让你同时
声明和实例化一个类。它可以帮助你进一步改善代码,让它变得更简洁。但这也不完全令人满意。
匿名类和你熟悉的Java局部类(块中定义的类)差不多,但匿名类没有名字。它允许你同时
声明并实例化一个类。换句话说,它允许你随用随建。
下面的代码展示了如何通过创建一个用匿名类实现ApplePredicate的对象,重写筛选的例子:
//直接内联参数化filterapples方法的行为 ListredApples = filterApples(inventory, new ApplePredicate() { public boolean test(Apple apple){ return "red".equals(apple.getColor()); } });
但匿名类还是不够好。第一,它往往很笨重,因为它占用了很多空间。还拿前面的例子来看,
如下面高亮的代码所示:
整体来说,啰嗦就不好;它让人不愿意使用语言的某种功能,因为编写和维护啰嗦的代码需
要很长时间,而且代码也不易读。好的代码应该是一目了然的。即使匿名类处理在某种程度上改
善了为一个接口声明好几个实体类的啰嗦问题,但它仍不能令人满意。在只需要传递一段简单的
代码时(例如表示选择标准的boolean表达式),你还是要创建一个对象,明确地实现一个方法
来定义一个新的行为(例如Predicate中的test方法或是EventHandler中的handler方法)。
在理想的情况下,我们想鼓励程序员使用行为参数化模式,因为正如你在前面看到的,它让
代码更能适应需求的变化。在第3章中,你会看到Java 8的语言设计者通过引入Lambda表达式——
一种更简洁的传递代码的方式——解决了这个问题。好了,悬念够多了,下面简单介绍一下
Lambda表达式是怎么让代码更干净的。
上面的代码在Java 8里可以用Lambda表达式重写为下面的样子:
Listresult = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
不得不承认这代码看上去比先前干净很多。这很好,因为它看起来更像问题陈述本身了。我
们现在已经解决了啰嗦的问题。下图对我们到目前为止的工作做了一个小结。
在通往抽象的路上,我们还可以更进一步。目前,filterApples方法还只适用于Apple。
你还可以将List类型抽象化,从而超越你眼前要处理的问题:
public interface Predicate{ boolean test(T t); } public static List filter(List list, Predicate p){ List result = new ArrayList<>(); for(T e: list){ if(p.test(e)){ result.add(e); } } return result; }
现在你可以把filter方法用在香蕉、桔子、Integer或是String的列表上了。这里有一个使用Lambda表达式的例子:
ListredApples = filter(inventory, (Apple apple) -> "red".equals(apple.getColor())); List evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);
酷不酷?你现在在灵活性和简洁性之间找到了最佳平衡点,这在Java 8之前是不可能做到的!
1.4 真实的例子你现在已经看到,行为参数化是一个很有用的模式,它能够轻松地适应不断变化的需求。这
种模式可以把一个行为(一段代码)封装起来,并通过传递和使用创建的行为(例如对Apple的
不同谓词)将方法的行为参数化。前面提到过,这种做法类似于策略设计模式。你可能已经在实
践中用过这个模式了。Java API中的很多方法都可以用不同的行为来参数化。这些方法往往与匿
名类一起使用。我们会展示三个例子,这应该能帮助你巩固传递代码的思想了:用一个
Comparator排序,用Runnable执行一个代码块。
对集合进行排序是一个常见的编程任务。比如,你的那位农民朋友想要根据苹果的重量对库
存进行排序,或者他可能改了主意,希望你根据颜色对苹果进行排序。听起来有点儿耳熟?是的,
你需要一种方法来表示和使用不同的排序行为,来轻松地适应变化的需求。
在Java 8中,List自带了一个sort方法(你也可以使用Collections.sort)。sort的行为
可以用java.util.Comparator对象来参数化,它的接口如下:
// java.util.Comparator public interface Comparator{ public int compare(T o1, T o2); }
因此,你可以随时创建Comparator的实现,用sort方法表现出不同的行为。比如,你可以
使用匿名类,按照重量升序对库存排序:
inventory.sort(new Comparator() { public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight()); } });
如果农民改了主意,你可以随时创建一个Comparator来满足他的新要求,并把它传递给
sort方法。而如何进行排序这一内部细节都被抽象掉了。用Lambda表达式的话,看起来就是
这样:
inventory.sort( (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
现在暂时不用担心这个新语法,下一章我们会详细讲解如何编写和使用Lambda表达式。
1.4.2 用 Runnable 执行代码块线程就像是轻量级的进程:它们自己执行一个代码块。但是,怎么才能告诉线程要执行哪块
代码呢?多个线程可能会运行不同的代码。我们需要一种方式来代表稍候执行的一段代码。在
Java里,你可以使用Runnable接口表示一个要执行的代码块。请注意,代码不会返回任何结果
(即void):
// java.lang.Runnable
public interface Runnable{
public void run();
}
你可以像下面这样,使用这个接口创建执行不同行为的线程:
Thread t = new Thread(new Runnable() {
public void run(){
System.out.println("Hello world");
}
});
用Lambda表达式的话,看起来是这样:
Thread t = new Thread(() -> System.out.println("Hello world"));
2.Lambda表达式
在上一章中,你了解了利用行为参数化来传递代码有助于应对不断变化的需求。它允许你定
义一个代码块来表示一个行为,然后传递它。你可以决定在某一事件发生时(例如单击一个按钮)
或在算法中的某个特定时刻(例如筛选算法中类似于“重量超过150克的苹果”的谓词,或排序
中的自定义比较操作)运行该代码块。一般来说,利用这个概念,你就可以编写更为灵活且可重
复使用的代码了。
但你也看到,使用匿名类来表示不同的行为并不令人满意:代码十分啰嗦,这会影响程序
员在实践中使用行为参数化的积极性。在本章中,我们会教给你Java 8中解决这个问题的新工
具——Lambda表达式。它可以让你很简洁地表示一个行为或传递代码。现在你可以把Lambda
表达式看作匿名功能,它基本上就是没有声明名称的方法,但和匿名类一样,它也可以作为参
数传递给一个方法。
我们会展示如何构建Lambda,它的使用场合,以及如何利用它使代码更简洁。我们还会介
绍一些新的东西,如类型推断和Java 8 API中重要的新接口。最后,我们将介绍方法引用(method
reference),这是一个常常和Lambda表达式联用的有用的新功能。
2.1 Lambda 管中窥豹本章的行文思想就是教你如何一步一步地写出更简洁、更灵活的代码。在本章结束时,我们
会把所有教过的概念融合在一个具体的例子里:我们会用Lambda表达式和方法引用逐步改进第2
章中的排序例子,使之更加简明易读。这一章很重要,而且你将在本书中大量使用Lambda。
可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它
有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。这个定义够大的,让我
们慢慢道来。
匿名——我们说匿名,是因为它不像普通的方法那样有一个明确的名称:写得少而想得多!函数——我们说它是函数,是因为Lambda函数不像方法那样属于某个特定的类。但和方法一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。传递——Lambda表达式可以作为参数传递给方法或存储在变量中。简洁——无需像匿名类那样写很多模板代码。
你为什么应该关心Lambda表达式呢?你在上一章中看到了,在Java中传递代码十分繁琐和冗长。那么,现在有了好消息!Lambda解决了这个问题:它可以让你十分简明地传
递代码。理论上来说,你在Java 8之前做不了的事情,Lambda也做不了。但是,现在你用不着再
用匿名类写一堆笨重的代码,来体验行为参数化的好处了!Lambda表达式鼓励你采用我们上一章中提到的行为参数化风格。最终结果就是你的代码变得更清晰、更灵活。比如,利用Lambda表达式,你可以更为简洁地自定义一个Comparator对象。
先前:
ComparatorbyWeight = new Comparator () { public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight()); } };
之后(用了Lambda表达式):
ComparatorbyWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
不得不承认,代码看起来更清晰了!要是现在你觉得Lambda表达式看起来一头雾水的话也没关系,我们很快会一点点解释清楚的。现在,请注意你基本上只传递了比较两个苹果重量所真正需要的代码。看起来就像是只传递了compare方法的主体。你很快就会学到,你甚至还可以进一步简化代码。我们将在下一节解释在哪里以及如何使用Lambda表达式。
我们刚刚展示给你的Lambda表达式有三个部分,如图3-1所示。
参数列表——这里它采用了Comparator中compare方法的参数,两个Apple。箭头——箭头->把参数列表与Lambda主体分隔开。Lambda主体——比较两个Apple的重量。表达式就是Lambda的返回值了。
为了进一步说明,下面给出了Java 8中五个有效的Lambda表达式的例子。
代码清单3-1 Java 8中有效的Lambda表达式
Java语言设计者选择这样的语法,是因为C#和Scala等语言中的类似功能广受欢迎。Lambda的基本语法是
(parameters) -> expression
或(请注意语句的花括号)
(parameters) -> { statements; }
2.2 在哪里以及如何使用 Lambda
现在你可能在想,在哪里可以使用Lambda表达式。在上一个例子中,你把Lambda赋给了一个Comparator类型的变量。你也可以在上一章中实现的filter方法中使用Lambda:
ListgreenApples = filter(inventory, (Apple a) -> "green".equals(a.getColor()));
那到底在哪里可以使用Lambda呢?你可以在函数式接口上使用Lambda表达式。在上面的代码中,你可以把 Lambda 表达式作为第二个参数传给 filter 方法,因为它这里需要Predicate,而这是一个函数式接口。如果这听起来太抽象,不要担心,现在我们就来详细解释这是什么意思,以及函数式接口是什么。
2.2.1 函数式接口还记得你在第2章里,为了参数化filter方法的行为而创建的Predicate接口吗?它就是一个函数式接口!为什么呢?因为Predicate仅仅定义了一个抽象方法:
public interface Predicate{ boolean test (T t); }
一言以蔽之,函数式接口就是只定义一个抽象方法的接口。你已经知道了Java API中的一些
其他函数式接口。
public interface Comparator{ int compare(T o1, T o2); } public interface Runnable{ void run(); } public interface ActionListener extends EventListener{ void actionPerformed(ActionEvent e); } public interface Callable { V call(); } public interface PrivilegedAction { V run(); }
用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。下面的代码是有效的,因为Runnable是一个只定义了一个抽象方法run的函数式接口:
//使用Lambda
Runnable r1 = () -> System.out.println("Hello World 1");
//使用匿名类
Runnable r2 = new Runnable(){
public void run(){
System.out.println("Hello World 2");
}
};
public static void process(Runnable r){
r.run();
}
//打印“HelloWorld 1”
process(r1);
//打印“HelloWorld 2”
process(r2);
//利用直接传递的Lambda打印“Hello World 3”
process(() -> System.out.println("Hello World 3"));
2.2.2 函数描述符
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作
函数描述符。例如,Runnable接口可以看作一个什么也不接受什么也不返回(void)的函数的
签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。
我们在本章中使用了一个特殊表示法来描述Lambda和函数式接口的签名。() -> void代表
了参数列表为空,且返回void的函数。这正是Runnable接口所代表的。举另一个例子,(Apple,
Apple) -> int代表接受两个Apple作为参数且返回int的函数。我们会在3.4节和本章后面的
表3-2中提供关于函数描述符的更多信息。
你可能已经在想,Lambda表达式是怎么做类型检查的。我们会在3.5节中详细介绍,编译器
是如何检查Lambda在给定上下文中是否有效的。现在,只要知道Lambda表达式可以被赋给一个
变量,或传递给一个接受函数式接口作为参数的方法就好了,当然这个Lambda表达式的签名要
和函数式接口的抽象方法一样。比如,在我们之前的例子里,你可以像下面这样直接把一个
Lambda传给process方法:
public void process(Runnable r){
r.run();
}
process(() -> System.out.println("This is awesome!!"));
此代码执行时将打印“This is awesome!!”。Lambda表达式()-> System.out.println("This is awesome!!")不接受参数且返回void。 这恰恰是Runnable接口中run方法的签名。
2.3 使用函数式接口@FunctionalInterface又是怎么回事?
如果你去看看新的Java API,会发现函数式接口带有@FunctionalInterface的标注(3.4
节中会深入研究函数式接口,并会给出一个长长的列表)。这个标注用于表示该接口会设计成
一个函数式接口。如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接
口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding
abstract methods found in interface Foo”,表明存在多个抽象方法。请注意,@FunctionalInterface不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是@Override
标注表示方法被重写了。
函数式接口定义且只定义了一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述Lambda表达式的签名。函数式接口的抽象方法的签名称为函数描述符。所以为了应用不同的Lambda表达式,你需要一套能够描述常见函数描述符的函数式接口。Java API中已经有了几个函数式接口,比如你在之前见到的Comparable、Runnable和Callable。
Java 8的库设计师帮你在java.util.function包中引入了几个新的函数式接口。我们接下
来会介绍Predicate、Consumer和Function。
java.util.function.Predicate接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。这恰恰和你先前创建的一样,现在就可以直接使用了。在你需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。比如,你可以定义一个接受String对象的Lambda表达式,如下所示。
使用Predicate
@FunctionalInterface public interface Predicate{ boolean test(T t); } public static List filter(List list, Predicate p) { List results = new ArrayList<>(); for(T s: list){ if(p.test(s)){ results.add(s); } } return results; } Predicate nonEmptyStringPredicate = (String s) -> !s.isEmpty(); List nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
如果你去查Predicate接口的Javadoc说明,可能会注意到诸如and和or等其他方法。
2.3.2 Consumerjava.util.function.Consumer定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。比如,你可以用它来创建一个forEach方法,接受一个Integers的列表,并对其中每个元素执行操作。在下面的代码中,你就可以使用这个forEach方法,并配合Lambda来打印列表中的所有元素。
使用Consumer
@FunctionalInterface public interface Consumer2.3.3 Function{ void accept(T t); } public static void forEach(List list, Consumer c){ for(T i: list){ c.accept(i); } } forEach(Arrays.asList(1,2,3,4,5),(Integer i) -> System.out.println(i));
java.util.function.Function
使用Function
@FunctionalInterface public interface Function{ R apply(T t); } public static List map(List list,Function f) { List result = new ArrayList<>(); for(T s: list){ result.add(f.apply(s)); } return result; } // [7, 2, 6] List l = map(Arrays.asList("lambdas","in","action"),(String s) -> s.length());
原始类型特化
我们介绍了三个泛型函数式接口:Predicate、Consumer和Function
回顾一下:Java类型要么是引用类型(比如Byte、Integer、Object、List),要么是原始类型(比如int、double、byte、char)。但是泛型(比如Consumer中的T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。①因此,在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱(unboxing)。Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。比如,这就是为什么下面的代码是有效的(一个int被装箱成为Integer):
Listlist = new ArrayList<>(); for (int i = 300; i < 400; i++){ list.add(i); }
但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。比如,在下面的代码中,使用IntPredicate就避免了对值1000进行装箱操作,但要是用Predicate就会把参数1000装箱到一个Integer对象中:
public interface IntPredicate{
boolean test(int t);
}
//true(无装箱)
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000);
//false(装箱)
Predicate oddNumbers = (Integer i) -> i % 2 == 1;
oddNumbers.test(1000);
一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function接口还有针对输出参数类型的变种:ToIntFunction
表3-2总结了Java API中提供的最常用的函数式接口及其函数描述符。请记得这只是一个起点。如果有需要,你可以自己设计一个。请记住,(T,U) -> R的表达方式展示了应当如何思考一个函数描述符。表的左侧代表了参数类型。这里它代表一个函数,具有两个参数,分别为泛型T和U,返回类型为R。
表3-2 Java 8中的常用函数式接口
| 函数式接口 | 函数描述符 | 原始类型特化 |
|---|---|---|
| Predicate | T->boolean | IntPredicate,LongPredicate, DoublePredicate |
| Consumer | T->void | IntConsumer,LongConsumer, DoubleConsumer |
| Function | T->R | IntFunction |
| Supplier | ()->T | BooleanSupplier,IntSupplier, LongSupplier, DoubleSupplier |
| UnaryOperator | T->T | IntUnaryOperator, LongUnaryOperator,DoubleUnaryOperator |
| BinaryOperator | (T,T)->T | IntBinaryOperator,LongBinaryOperator,DoubleBinaryOperator |
| BiPredicate | (L,R)->boolean | |
| BiConsumer | (T,U)->void | ObjIntConsumer |
| BiFunction | (T,U)->R | ToIntBiFunction |
为了总结关于函数式接口和Lambda的讨论,表3-3总结了一些使用案例、Lambda的例子,以
及可以使用的函数式接口。



