1.0 前言

lambda 表达式是 java 8 中最引人注目的新特性,但是它自身的概念并不好理解,为了更好的掌握它,我们必须先了解一下函数式编程。网上关于这方面的介绍有很多,但是大多说的含糊不清。这里提供一篇我认为说的最明白的文章,是一篇译文,如果有疑问还可以直接对比原文。 傻瓜函数式编程

1.1 lambda 表达式是什么?

一个lambda 是一段带有参数的代码块,它可以被传递,因此,它可以执行一次或多次。先从一个简单、经典的比较器例子入手,来感受一下 lambda 表达式

List<String> list = Arrays.asList("aaa", "bbb", "ccc");
list.sort(new Comparator<String>() {
    @Override public int compare(String o1, String o2) {
        return Integer.compare(o1.length(), o2.length());
    }
});

它改写成 lambda 表达式的样子是:

List<String> list = Arrays.asList("aaa", "bbb", "ccc");
list.sort((o1, o2) -> Integer.compare(o1.length(), o2.length()));

1.2 为什么要使用 lambda 表达式?

仔细对比一下上面的例子的两段代码,我们思考几个问题:

  1. 我们为什么要 new 一个比较器? 因为我们需要 compara方法的实现,即 Integer.compare(o1.length(), o2.length())。

  2. 为什么不直接把 compara 的实现代码直接给 sort 方法用? 因为 Java 在之前没有这种传递代码的机制。我们只能把复用的代码封装到一个类里。在面向对象的 Java 里,你不可能将一段代码传来传去,想要向一段代码中传递另一段代码你不得不构造一个属于某个类的对象,由它的某个方法包含所需要的代码。我们一直以这种方式达到我们传代码的目的,但是这样做并不好。

1.3 lambda 表达式语法

简单抽象一下,基本语法的格式是这个样子:

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

但是上面的例子 (o1, o2) -> Integer.compare(o1.length(), o2.length())) 显然和语法格式不一致,我们继续以这段代码为例,详细说一下 lambda 表达式的语法。 o1, o2 是什么呢?它们都是字符串,Java 是强类型语言,我们必须为每个变量或参数指明类型,一个正经 lambda 的写法如下: 如果一个表达式的参数类型是可以被推导的,那么可以忽略它们的类型

(o1, o2) -> 
    return Integer.compare(o1.length(), o2.length());

如果一个表达式的返回类型是可以被推导的,那么它将会从上下文自动推导

(o1, o2) -> Integer.compare(o1.length(), o2.length());

也就变成了例子中的样子。 其它简化写法:如果某个表达式不含有参数,你可以使用一对空的小括号,跟无参方法一样;如果只含有一个参数,且可以被推导,可以不写小括号。

() -> System.out.println("无参的 lambda 表达式");
event -> System.out.println("按钮点击");

1.4 函数式接口

这部分是理解 Java lambda 的重点

对于只包含一个抽象 方法的接口,你可以通过 lambda 表达式创建该接口的对象。这种接口被称为函数式接口

上面已经提到,我们有传递一段代码的需求。同样,Java 中很多已有的接口也需要封装代码块,比如 Runnable、Comparator。使用 lambda 表达式实现本应由这些接口实例实现的功能的行为,即lambda 表达式转换成一个接口的实例叫函数式接口转换(ps:个人理解,非标准定义)。 实际上完成函数式接口的转换是 lambda 表达式在Java 中唯一能做的事。我们以API 中的几段代码来说明什么是函数式接口转换:

public void sort(Comparator<? super E> c) {
    final int expectedModCount = modCount;
    Arrays.sort((E[]) elementData, 0, size, c);
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    modCount++;
}

上面一段代码是 Arraylist 类中的 sort 方法,从方法签名中的参数列表看,这个方法需要一个参数:比较器实现。回顾一下我们最开始的例子,参数传的是什么? lambda 表达式对不对?

List<String> list = Arrays.asList("aaa", "bbb", "ccc");
list.sort((o1, o2) -> Integer.compare(o1.length(), o2.length()));

即我们以 lambda 表达式实现了接口实例的功能。是怎么实现的呢?list.sort 方法会接收一个实现了 Comparator 接口的类的实例,调用该对象的 compare 方法会执行 lambda 表达式中的代码。

Java API 在 java.util.function 包中定义了一些通用的函数式接口,比如 BiFunction<T, U, R>意思是T,U类型的参数以及R类型的返回值。

BiFunction<String, String, Integer> c = (o1, o2) -> Integer.compare(o1.length(), o2.length());

虽然你可以使用 BiFunction 声明、引用一个表达式,但是并没有什么卵用,因为没有任何一个方法可以接收函数式接口作为参数。

1.5 方法引用

把一个已有的方法传递给其他代码。 比如说上面一直用的排序例子,我们不按长度排序了,现在按字符串大小排序,并且忽略大小写,代码应该是这样子的:

List<String> list = new ArrayList<>();
list.sort((String x, String y) -> x.compareToIgnoreCase(y));

其实我们传递给sort 方法的代码片段就是 String 类的 compareToIgnoreCase 方法。为了更优雅的写代码,API 简化这种写法,即

List<String> list = new ArrayList<>();
list.sort(String::compareToIgnoreCase);

::操作符分隔对象/类名 和 方法名,主要有三种情况

  • 对象::实例方法
  • 类::静态方法
  • 类::实例方法

对于前两种情况,方法引用就是对参数执行该方法。比如打印 list 元素,

list.forEach(System.out::print);  // 相当于 list.forEach(x -> System.out.println(x));

对于第三种情况,第一个参数会成为执行方法的对象,第2个是方法的参数。比如上面的字符串排序 demo。

1.6 构造器引用

构造器引用与方法引用类似,不同的是构造器引用引用的方法名是 new。貌似用处不大,了解即可。

1.7 变量作用域

简单地说,lambda 表达式的变量作用域和内部类的变量作用域类似。

static int b = 10;
public static void main(String[] args) {
    int a = 100;
    Runnable r = () -> {
        a++;       // 错误,不能更改局部变量的值
        b++;         // 正确,但有线程安全问题
        int a = 1;  // 错误,不能与局部变量重名
        int b = 1;  // 正确
    };
    new Thread(r).run();
}

1.8 默认方法

接口可以有实现方法,而且不需要实现类去实现其方法。只需在方法名前面加个default关键字即可。

为什么要定义默认方法? Java 的接口是不可扩展的,一旦你为某个父接口增加了一个方法,你就必须为所有的实现类增加这个方法的实现。 因为新增了对 lambda 表达式的支持,现在 Java 有了传递代码块的能力,所以对集合的处理有了更简洁、高效的方式,比如,输出集合元素:

// old
for(String s : list) {
   System.out.println(s);
}
// now
list.forEach(System.out::println);

但是,如果给Collection 或 Iterator 增加forEach 接口,会要求所有的实现类增加 forEach 的实现,显然这是无法接受的。所以,为了解决接口的修改与现有的实现不兼容的问题,引入了默认方法。 在JVM中,默认方法的实现是非常高效的,并且通过字节码指令为方法调用提供了支持。 使用规则 如果接口接口定义了一个默认方法,而另一个父类/接口有定义了一个同名方法,该选择哪个? 1.选择父类中的方法。如果父类中提供了实现,忽略接口中的方法。 2.接口冲突。如果多个父接口中都提供了对同一方法的实现,接口中的默认方法都忽略,实现类必须自己定义实现。

1.9 接口中的静态方法

Java 8 中,接口中允许添加静态方法。比如我们在 章节“5.3 比较器” 中使用到的 Comparator.comparing 方法。使用静态方法,代码会更简洁。我们再以开头的那段代码作为例子:

List<String> list = Arrays.asList("aaa", "bbb", "ccc");
list.sort(new Comparator<String>() {
    @Override public int compare(String o1, String o2) {
        return Integer.compare(o1.length(), o2.length());
    }
});

可以写成

List<String> list = Arrays.asList("aaa", "bbb", "ccc");
list.sort(Comparator.comparing(String::length));