Java8之lambda表达式详解

来源:互联网 发布:点餐平台源码 编辑:程序博客网 时间:2024/05/21 09:23

一、什么是lambda表达式

Java SE 8添加了2个对集合数据进行批量操作的包: java.util.function 包以及 java.util.stream 包。 流(stream)就如同迭代器(iterator),但附加了许多额外的功能。 Lambda表达式是Java SE 8中一个重要的新特性。lambda表达式允许你通过表达式来代替功能接口。 lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)。Lambda表达式还增强了集合库。总的来说,lambda表达式和 stream 是自Java语言添加泛型(Generics)和注解(annotation)以来最大的变化。
在使用java8之前,我们在处理一些包含有单个方法的接口时,一般是通过实现具体类或者匿名类的方式来处理的。这种方式能实现所期望的功能,而且也是传统的一切皆对象思想的体现。从实现的细节来看,却显得比较繁琐。
我们先来看一个简单的示例,假定我们首先定义如下的类:

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方法中去。其详细的实现如下:

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表达式可以视为一个匿名方法,但是它可以像普通的对象参数那样被传递。它没有具体定义的名字,但是可以有一组参数,函数体以及返回类型。它甚至可以包含有被抛出的异常列表。我们针对它的每个具体特征来讨论。

1、基本语法

lambda表达式的一般语法:

(Type1 param1, Type2 param2, ..., TypeN paramN) -> {  statment1;  statment2;  //.............  return statmentM;}

从lambda表达式的一般语法可以看出来,还是挺符合非精确版本的定义–“一段带有输入参数的可执行语句块”。
上面的lambda表达式语法可以认为是最全的版本,写起来还是稍稍有些繁琐。下面陆续介绍一下lambda表达式的各种简化版:

1、参数类型省略–绝大多数情况,编译器都可以从上下文环境中推断出lambda表达式的参数类型。这样lambda表达式就变成了:

(param1,param2, ..., paramN) -> {  statment1;  statment2;  //.............  return statmentM;}

2、当lambda表达式的参数个数只有一个,可以省略小括号。lambda表达式简写为:

param1 -> {  statment1;  statment2;  //.............  return statmentM;}

3、当lambda表达式只包含一条语句时,可以省略大括号、return和语句结尾的分号。lambda表达式简化为:

param1 -> statment

2、匿名(Anonymous)

像在前面的代码里,我们传递给inventory.sort方法的是一段代码。这段代码没有方法声明。

(a1, a2) -> a1.getWeight().compareTo(a2.getWeight())  

3、 函数式(Function)

在上述传递的函数里,这个函数的定义和使用和我们通常使用的方法不一样。它并不是专门定义在某个类里面。但是它却有我们在普通类里定义的方法具有的特性。比如函数参数列表,返回值以及抛出的异常列表。

4、可传递性(Passed around)

从往常的理解来看,一个函数的定义是放在某个类里面的。但是这里它却表现的像一个类一样。实际上我们甚至可以将它赋值给一个变量。比如上述示例里的代码可以声明如下:

Comparator<Apple> comparator = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());  inventory.sort(comparator);  

从代码的表面上看起来,我们可以用一个lambda表达式来替换一个对应的接口。

5、lambda表达式语法示例

从前面示例我们可以看到一个lambda表达式的基本原则如下:

  • 一个 Lambda 表达式可以有零个或多个参数;
  • 参数的类型既可以明确声明,也可以根据上下文来推断。例如:(int a)与(a)效果相同;
  • 所有参数需包含在圆括号内,参数之间用逗号相隔。例如:(a, b) 或 (int a, int b) 或 (String a, int b, float c);
  • 空圆括号代表参数集为空。例如:() -> 42;
  • 当只有一个参数,且其类型可推导时,圆括号()可省略。例如:a -> return a*a;
  • Lambda 表达式的主体可包含零条或多条语句;
  • 如果 Lambda 表达式的主体只有一条语句,花括号{}可省略。匿名函数的返回类型与该主体表达式一致;
  • 如果 Lambda 表达式的主体包含一条以上语句,则表达式必须包含在花括号{}中(形成代码块)。匿名函数的返回类型与代码块的返回类型一致,若没有返回则为空。

因为lambda不需要定义名字,所以首先是包含有输入的函数参数列表,一般用一个括号来包含,比如:()然后是一个箭头符号,后面包含具体的函数语句,可以有一句或者多句。比如: a.length - b.length。

(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表达式实现原理

1、函数式接口(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对象并返回booleanConsumer<T>——接收T对象,不返回值Function<T, R>——接收T对象,返回R对象Supplier<T>——提供T对象(例如工厂),不接收值UnaryOperator<T>——接收T对象,返回T对象BinaryOperator<T>——接收两个T对象,返回T对象

我们也可以自定义一个函数式//定义一个函数@FunctionalInterface

 //定义一个函数式接口@FunctionalInterfacepublic interface WorkerInterface {   public void doSomeWork();}public class WorkerInterfaceTest {public static void execute(WorkerInterface worker) {    worker.doSomeWork();}public static void main(String [] args) {    //invoke doSomeWork using Annonymous class    execute(new WorkerInterface() {        @Override        public void doSomeWork() {            System.out.println("Worker invoked using Anonymous class");        }    });    //invoke doSomeWork using Lambda expression     execute( () -> System.out.println("Worker invoked using Lambda expression") );}}

输出:

Worker invoked using Anonymous class Worker invoked using Lambda expression

这上面的例子里,我们创建了自定义的函数式接口并与 Lambda 表达式一起使用。execute() 方法现在可以将 Lambda 表达式作为参数。

2、目标类型(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的类型声明。这样可以使得我们的代码更加简练。

3、方法引用(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。对于具体对象上的实例方法引用,我们则需要在对象名和方法名之间加入分隔符: 。

针对上述的讨论,我们先来看个示例。假设我们需要对一组String进行排序,并忽略大小写。那么我们可以采用lambda表达式写成如下:

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]  

4、Lambda 表达式与匿名类的区别

使用匿名类与 Lambda 表达式的一大区别在于关键词的使用。对于匿名类,关键词 this 解读为匿名类,而对于 Lambda 表达式,关键词 this 解读为写就 Lambda 的外部类。
Lambda 表达式与匿名类的另一不同在于两者的编译方法。Java 编译器编译 Lambda 表达式并将他们转化为类里面的私有函数,它使用 Java 7 中新加的 invokedynamic 指令动态绑定该方法。