java8 lambda表达式学习总结
简介
在使用java8之前,我们在处理一些包含有单个方法的接口时,一般是通过实现具体类或者匿名类的方式来处理的。这种方式能实现所期望的功能,而且也是传统的一切皆对象思想的体现。从实现的细节来看,却显得比较繁琐。在引入了lambda表达式这种新特性之后,我们有了一种更加简练的方式来实现对应的功能特性,当然,也带来了一种函数式编程思想上的转变。
简单示例
我们先来看一个简单的示例,假定我们首先定义如下的类:
public class Apple { private Integer weight = 0; private String color = ""; public Apple(Integer weight, String color) { this.weight = weight; this.color = color; } public Integer getWeight() { return weight; } public void setWeight(Integer weight) { this.weight = weight; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } public String toString() { return "Apple{" + "color='" + color + '\'' + ", weight=" + weight + '}'; } }
这仅仅是一个普通的实体类。然后我们有一组这样的对象,在实现中需要针对这些Apple对象的weight属性进行排序。这是一个非常简单的问题,一种最传统的方式无非就是实现一个Comparator<Apple>的接口,再将该实现的对象作为参数传递到原来的sort方法中去。其详细的实现如下:
AppleComparator:
import java.util.Comparator; public class AppleComparator implements Comparator<Apple> { public int compare(Apple a1, Apple a2) { return a1.getWeight().compareTo(a2.getWeight()); } }
主函数代码如下:
import java.util.*; public class Sorting { public static void main(String[] args) { List<Apple> inventory = new ArrayList<>(); inventory.addAll(Arrays.asList(new Apple(80, "green"), new Apple(155, "green"), new Apple(120, "red"))); inventory.sort(new AppleComparator()); System.out.println(inventory); } }
这样,我们就实现了一个基于自定义对象进行排序的功能。它能够实现排序的要点是inventory.sort方法里需要接收的参数是Comparator类型的对象。而这个类型的对象必须要实现compare方法。从功能实现的角度来说,我们的方法需要传递的参数类型和实际传递的都是对象,也正好符合一切皆对象的这个说法。
当然,这种实现方式显得比较繁琐,因为我们这里仅仅是需要实现一个简单的接口,这里却需要定义一个类,专门实现它。而且真正能够在排序里起作用的就是compare这个方法。只有根据它才能知道怎么排序。可是在这里没办法,必须针对这个需要的方法行为包装成一个对象传递过去。
当然,我们还想到一种稍微简单一点的方法,就是使用匿名类,这种实现的方式如下:
import java.util.*; public class Sorting { public static void main(String[] args) { List<Apple> inventory = new ArrayList<>(); inventory.addAll(Arrays.asList(new Apple(80, "green"), new Apple(155, "green"), new Apple(120, "red"))); inventory.sort(new Comparator<Apple>() { public int compare(Apple a1, Apple a2) { return a1.getWeight().compareTo(a2.getWeight()); } }); System.out.println(inventory); } }
这种方式实现的代码要稍微简洁一点,但还是显得比较冗长。如同前面我们讨论中提到,对这个接口建模最关键的就是它的这个compare方法。至于其传递方式,由于要求面向对象设计的要求,必须将该方法包装在一个对象里。那么,有没有更加简洁的方式来解决这个问题呢?
lambda表达式
在详细讨论lambda表达式之前,我们先看看用这种方式来解决上述问题有多简单。我们实现排序比较的代码如下:
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
我们甚至可以将类型信息给省略掉:
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
甚至更简单的情况下可以用如下代码来描述:
inventory.sort(comparing(Apple::getWeight));
在上述代码里,我们没有新建什么对象,而是采用一种类似于方法传递的方式来实现排序的目的。这里,我们使用的就是lambda表达式。在函数式编程语言的概念里,这里相当于将一个函数作为参数传递到另外一个对象方法里。笼统的来说,在java8里新加入的特性是的我们可以将一个函数作为参数来传递了。
那么,该怎么来理解lambda表达式呢?一个lambda表达式可以视为一个匿名方法,但是它可以像普通的对象参数那样被传递。它没有具体定义的名字,但是可以有一组参数,函数体以及返回类型。它甚至可以包含有被抛出的异常列表。我们针对它的每个具体特征来讨论。
匿名(Anonymous)
像在前面的代码里,我们传递给inventory.sort方法的是一段代码。这段代码没有方法声明。
(a1, a2) -> a1.getWeight().compareTo(a2.getWeight())
函数式(Function)
在上述传递的函数里,这个函数的定义和使用和我们通常使用的方法不一样。它并不是专门定义在某个类里面。但是它却有我们在普通类里定义的方法具有的特性。比如函数参数列表,返回值以及抛出的异常列表。
可传递性(Passed around)
从往常的理解来看,一个函数的定义是放在某个类里面的。但是这里它却表现的像一个类一样。实际上我们甚至可以将它赋值给一个变量。比如上述示例里的代码可以声明如下:
Comparator<Apple> comparator = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); inventory.sort(comparator);
从代码的表面上看起来,我们可以用一个lambda表达式来替换一个对应的接口。关于这方面的详情将在后面讨论。
lambda表达式语法
从前面示例我们可以看到一个lambda表达式的样式基本如下:
它不需要定义名字,所以首先是包含有输入的函数参数列表,一般用一个括号来包含,比如:()
然后是一个箭头符号,后面包含具体的函数语句,可以有一句或者多句。比如: a.length - b.length。
所以上述示例的解析结构如下图:
基于上述的描述,我们可以定义很多类似的lambda表达式,它们对应的函数签名如下:
(String s) -> s.length
输入参数为String类型,返回结果为int类型的函数。它的函数签名样式为: (String) -> int
(Apple a) -> a.getWeight() > 150
输入参数为Apple类型,返回结果为boolean类型的函数,函数签名样式为:(Apple) -> boolean
在需要返回结果的函数签名里,如果函数语句只有一句的话,该语句执行的结果将作为函数结果返回。
(int i, int j) -> { System.out.println(i); System.out.println(j); System.out.println("Result printed"); }
输入参数为int, int,返回结果为void,即没有返回任何结果。函数签名样式为:(int, int) -> void
(String a, String b) -> { System.out.println(a); return a + b; }
该示例代码的输入参数为(String, String),返回结果为String。但是因为函数中有多条语句,所以需要添加一个return语句在最后作为返回的结果。
看了上述简单的介绍后,估计我们心里还是有很多的疑问。比如说,为什么上述的代码可以对等的替换一个接口呢?难道说它和一个接口是等价的?那么它是不是可以替换所有类型的接口呢?另外一个就是,我们前面写的lambda表达式里,对于传入的参数甚至连类型都没有声明,它是怎么知道我们的参数类型的呢?
函数式接口(Functional interfaces)
在前面的示例代码里,我们看到,可以将一个lambda表达式用在一个接口所使用的地方。在java8里,lambda表达式可以传递和识别的类型是函数式接口。那么函数式接口是什么呢?
在java里,我们经常可以看到不少只包含有一个方法定义的接口,比如Runnable, Callable, Comparator等。而这种仅仅包含有一个接口方法的接口就可以称其为函数式接口。需要特别注意的一点就是,这里指的方法是接口里定义的抽象方法。由于java8里引入了默认方法(default method),在接口里也可以定义默认方法的实现。但是这些方法并不算抽象方法。关于默认方法我们会在后续的文章里讨论。
另外,如果某个接口定义了一个抽象方法的同时继承了一个包含其他抽象方法的接口,那么该接口就不是函数式接口。实际上,如果我们去查看目前那些常见的java类库里的函数式接口,它们都有一个如下的声明修饰: @FunctionalInterface。比如Comparator接口和Runnable接口:
@FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); // details ignored. }
@FunctionalInterface public interface Runnable { /** * When an object implementing interface <code>Runnable</code> is used * to create a thread, starting the thread causes the object's * <code>run</code> method to be called in that separately executing * thread. * <p> * The general contract of the method <code>run</code> is that it may * take any action whatsoever. * * @see java.lang.Thread#run() */ public abstract void run(); }在这里@FunctionalInterface相当于函数式接口的声明,类似于我们继承类里实现某个方法使用的@Override声明。它表示该接口是函数式接口,方便在编译的时候进行检查。
这样,我们可以发现,每个函数对象对应一个函数式接口的实例。所有传递单个方法接口的地方就可以用lambda表达式来替换了。
除此之外,Java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:
- Predicate<T>——接收T对象并返回boolean
- Consumer<T>——接收T对象,不返回值
- Function<T, R>——接收T对象,返回R对象
- Supplier<T>——提供T对象(例如工厂),不接收值
- UnaryOperator<T>——接收T对象,返回T对象
- BinaryOperator<T>——接收两个T对象,返回T对象
目标类型(Target typing)
在前面的讨论中我们发现,其实一个lambda表达式就是一个对应的函数式接口对象。但是,一个lambda表达式它本身并没有包含它到底实现哪个函数式接口的信息。我们怎么知道我们定义的某个lambda表达式可以用到某个函数式接口呢?实际上,对于lambda表达式的类型是通过它的应用上下文来推导出来的。这个过程我们称之为类型推导(type inference)。那么,在上下文中我们期望获得到的类型则称之为目标类型。该怎么来理解上述的内容呢?
例如,下面代码中的lambda表达式类型是ActionListener:
ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());
但是同样的lambda表达式在不同的上下文中可以有不同的类型:
Callable<String> c = () -> "done"; PrivilegedAction<String> a = () -> "done";
第一个lambda表达式() -> "done"是Callable的实例,而第二个lambda表达式则是PrivilegedAction的实例。
下面,我们来结合前面的示例代码做一个详细的类型检查分析:
首先,我们这部分应用lambda表达式的代码如下:
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
1. 我们首先检查inventory.sort方法的签名,它的详细签名如下:void sort(Comparator<? super E> c)。
2. 那么它期待的参数类型是Comparator<Apple>.
3. 我们来看Comparator接口,它是一个函数式接口,并有定义的抽象方法compare。
4. 这个compare方法的详细签名如下:int compare(Apple o1, Apple o2),这表示这个方法期待两个类型为Apple的输入参数,并返回一个整型的结果。
5. 比对lambda表达式的函数签名类型,它也是两个输入类型为Apple,并且输出为int类型。
这样,lambda表达式的目标类型和我们的类型匹配了。
总结起来,当且仅当下面所有条件均满足时,lambda表达式才可以被赋给目标类型T:
- T是一个函数式接口
- lambda表达式的参数和T的方法参数在数量和类型上一一对应
- lambda表达式的返回值和T的方法返回值相兼容(Compatible)
- lambda表达式内所抛出的异常和T的方法throws类型相兼容
由于目标类型(函数式接口)已经“知道”lambda表达式的形式参数(Formal parameter)类型,所以我们没有必要把已知类型再重复一遍。也就是说,lambda表达式的参数类型可以从目标类型中得出:
Comparator<Apple> comp = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
在上面的例子里,编译器可以推导出a1和a2的类型是Apple。所以它就在lambda表达式里省略了a1, a2的类型声明。这样可以使得我们的代码更加简练。
方法引用(Method references)
我们定义一些lambda表达式并传递给一些函数,这种方式可以使得我们实现的代码很简练。但是在有的情况下,我们已经有一些方法实现同样的功能了,那么我们能不能想办法重用这些原有的功能而不至于自己去重复实现呢?
像我们前面代码示例里使用的如下代码:
inventory.sort(comparing(Apple::getWeight));这里就是引用了一个方法。将它作为一个参数传递给comparing方法。这里的Apple::getWeight可以看做lambda表达式p -> p.getWeight()的一个简写形式。其中Apple::getWeight就是一个对Apple类中实现方法getWeight的引用。所以,我们可以将方法引用当做lambda表达式的语法糖。
方法引用有很多种,它们的语法如下:
- 静态方法引用:ClassName::methodName
- 实例上的实例方法引用:instanceReference::methodName
- 超类上的实例方法引用:super::methodName
- 类型上的实例方法引用:ClassName::methodName
- 构造方法引用:Class::new
- 数组构造方法引用:TypeName[]::new
对于静态方法引用,我们需要在类名和方法名之间加入::分隔符,例如Integer::sum。
对于具体对象上的实例方法引用,我们则需要在对象名和方法名之间加入分隔符:
Set<String> knownNames = ... Predicate<String> isKnown = knownNames::contains;
List<String> str = Arrays.asList("a","b","A","B"); str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));按照我们这里方法引用的定义,我们可以进一步将代码简化成如下:
List<String> str = Arrays.asList("a","b","A","B"); str.sort(String::compareToIgnoreCase);实际上对于方法引用的类型检查和lambda表达式的类型检查过程基本上一致,我们也可以用前面类型检查的步骤来验证方法引用。
和静态方法引用类似,构造方法也可以通过new关键字被直接引用:
SocketImplFactory factory = MySocketImpl::new;而对于包含有参数的函数,比如我们有一个构造函数Apple(Integer weight),我们可以采用这种方式来构造:
Function<Integer, Apple> c2 = Apple::new; Apple a2 = c2.apply(100);
数组的构造方法引用的语法则比较特殊,为了便于理解,我们可以假想存在一个接收int参数的数组构造方法。参考下面的代码:
IntFunction<int[]> arrayMaker = int[]::new; int[] array = arrayMaker.apply(10) // 创建数组 int[10]
背后的故事
如果我们感到好奇的话,会想更深入一步了解一下。为什么在java8里可以将方法或者说函数当一个参数传递给一个别的方法。这种行为使得方法的传递就像我们将普通对象当参数传递一样。
如果我们将前面用到的示例代码给反编译之后进行比较,将会发现如下一些有趣的地方:
面向对象式实现:
74: aload_1 75: new #11 // class AppleComparator 78: dup 79: invokespecial #12 // Method AppleComparator."<init>":()V 82: invokeinterface #13, 2 // InterfaceMethod java/util/List.sort:(Ljava/util/Comparator;)V 87: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream; 90: aload_1 91: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 94: return
lambda表达式实现:
73: pop 74: aload_1 75: invokedynamic #11, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function; 80: invokestatic #12 // InterfaceMethod java/util/Comparator.comparing:(Ljava/util/function/Function;)Ljava/util/Comparator; 83: invokeinterface #13, 2 // InterfaceMethod java/util/List.sort:(Ljava/util/Comparator;)V 88: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream; 91: aload_1 92: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 95: return
这里省略了大部分的反编译后的代码。我们会发现它们大部分都非常相似,除了在基于lambda表达式实现里有一个invokedynamic的指令。而java8这里之所以能够实现将方法当做参数传递的特性就是基于这个invokedynamic指令。由于篇幅的限制,关于invokedynamic指令的详细讨论会放在后面的文章里。
总结
Java8里引入了函数式编程的特性,这里每个定义的lambda表达式通过和传递的目标对象进行类型检查比较来寻找到一个匹配的结果。这样一个表达式就和一个函数式接口实现了对应的代换关系。通常我们也可以通过方法引用来重用一些已有的方法,这种方法引用的语法相当于是对lambda表达式的进一步增强,它使得我们的实现更加简练。同样,它的类型检查和lambda表达式是一样的。
参考材料
http://www.drdobbs.com/jvm/lambda-expressions-in-java-8/240166764
http://www.drdobbs.com/jvm/lambdas-and-streams-in-java-8-libraries/240166818
http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html
http://cr.openjdk.java.net/~briangoetz/lambda/lambda-libraries-final.html
http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html
http://blog.headius.com/2008/09/first-taste-of-invokedynamic.html
http://www.infoq.com/cn/articles/jdk-dynamically-typed-language
http://www.javaworld.com/article/2860079/scripting-jvm-languages/invokedynamic-101.html
下一篇: 读书和书籍选择问题的讨论:观点和思考篇