lambda 就是希腊字母 λ

代码块

lambda 表达式是一个可传递的代码块,可以在以后执行一次或多次。
先来看一下我们前面用到的代码块。
例如,向 Arrays.sort() 传入一个 Comparator 对象:

  1. public LenghtComparator implements Comparator<String> {
  2. public int compare(String first, String second) {
  3. return first.length() - second.length();
  4. }
  5. }
  6. ...
  7. Arrays.sort(Strings, new LenghtComparator());

compare 方法不是立即调用。实际上,在数组完成排序之前,sort 方法会一直调用 compare 方法,只要元素的顺序不正确就会调用 compare 方法重新排列元素。
总结一下,上述过程就是将一个代码块传递到某个对象。这个代码块将会在将来某个时间调用。

lambda 表达式的语法

lambda 表达式非常简单,就拿上面的例子转换成 lambda 表达式为:

  1. (String first, String second) -> first.length() - second.lenght()
  2. // like
  3. (String first, String second) -> { return first.length() - second.lenght() }

因为 Java 是强类型语言,所以这里要指定他们的类型(String)。
如果可以推导出一个 lambda 表达式的参数类型,可以忽略其类型:

  1. Comparator<String> comp = (first, second) // Same as (String first, String second)
  2. -> first.length() - second.length();

这里编译器会自动推断出 first 和 second 是字符串,因为这个 lambda 表达式将赋值给一个字符串比较器
如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:

  1. ActionListener listener = event ->
  2. System.out.println("The time is " + new Date());

无需指定 lambda 表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出。

函数式接口

对于只有一个抽象方法的接口,需要这种接口的对象(实例)时,就可以提供一个 lambda 表达式。这种接口称为函数式接口(functional interface)。

讲解一下抽象方法的理解。一种方式:接口可能会重新声明 Object 类的方法,如 toString 或 clone,这些声明有可能会让方法不再是抽象的。另一种是,在 Java SE 8 中,接口本身就可以声明非抽象方法:默认方法。

比如 Arrays.sort() 第二个参数需要一个 Comparator 实例, Comparator 就是只有一个方法的接口,所以可以提供一个 lambda 表达式:

  1. Arrays.sort(words, (first, second) -> first.length() - second.lenght())

在底层,Arrays.sort() 会接收实现了 Comparator 的某个类的对象。在这个对象上调用 compare 方法会执行这个 lambda 表达式的体。这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能要高效得多。最好把 lambda 表达式看作是一个函数,而不是一个对象,另外要接受 lambda 表达式可以传递到函数式接口。
lambda 表达式可以转换为接口。具体的语法很简短。下面再来看一个例子:

  1. Timer t = new Timer(1000, event -> {
  2. System.out.println("At the tone, the tiem is " + new Date());
  3. Toolkit.getDefaultToolkit().beep();
  4. });

与使用实现了 ActionListener 接口的类相比,这个代码可读性要好得多。

不能把lambda表达式赋给类型为Object的变量,Object不是一个函数式接口。

Java API 在 java.util.function 包中定义了很多非常通用的函数式接口。其中一个接口 BiFunction 描述了参数类型为 T 和 U 而且返回类型为 R 的函数。可以把我们的字符串比较 lambda 表达式保存在这个类型的变量中:

  1. BiFunction<String, String, Integer> biFunction =
  2. (first, second) -> first.length() - second.length();

不过,这对于排序并没有帮助。没有哪个 Arrays.sort 方法想要接收一个 BiFunction。

方法引用

有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如,假设你希望只要出现一个定时器事件就打印这个事件对象。当然,为此也可以调用:

  1. Timer timer = new Timer(1000, event -> System.out.println(event));

但是,如果直接把 println 方法传递到 Timer 构造器就更好了。具体做法如下:

  1. Timer timer = new Timer(1000, System.out::println);

表达式 System.out::println 是一个方法引用(method reference),它等价于 lambda 表达式 x->System.out.println(x)

这里的 x 是表示任意值,可以是 event -> System.out.println(event)),也可以是其他值

假设你想对字符串排序,而不考虑字母的大小写。可以传递以下方法表达式:

  1. Arrays.sort(strings, String::compareToIgnoreCase);

从这些例子可以看出,要用 :: 操作符分隔方法名与对象或类名。主要有3种情况:

  • Object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod

在前 2 种情况中,方法引用等价于提供方法参数的 lambda 表达式。前面已经提到,System.out::println (第一种情况)等价于 x->System.out.println(x)。类似地,Math::pow (第二种情况)等价于 (x,y)->Math.pow(x,y)
但是对于第 3 钟情况,第一个参数会成为目标的方法。例如,String::compareToIgnoreCase 等同于 (x, y) -> x.compareToIgnoreCase(y)
可以在方法引用中使用 this 参数。例如,this::equals 等同于 x->this.equals(x)。使用 super 也是合法的。例如:

  1. class Greeter {
  2. public void greet() {
  3. System.out.println("Hello world!");
  4. }
  5. }
  6. class TimedGreeter extends Greeter {
  7. public void greet() {
  8. Timer t = new Timer(1000, super::greet);
  9. t.start();
  10. }
  11. }

构造器引用

构造器引用与方法引用很类似,只不过方法名为 new。例如,Person::new 是 Person 构造器的一个引用。哪一个构造器呢?这取决于上下文。
假设你有一个字符串列表。可以把它转换为一个 Person 对象数组,为此要在各个字符串上调用构造器,调用如下:

  1. ArrayList<String> names = new ArrayList<String>(Arrays.asList(new String[]{"yikang", "kangkang"}));
  2. Stream<Person> stream = names.stream().map(Person::new);
  3. List<Person> people = stream.collect(Collectors.toList());

map 方法会为各个列表元素调用 Person(String) 构造器。如果有多个 Person 构造器,编译器会选择有一个 String 参数的构造器,因为它从上下文推导出这是在对一个字符串调用构造器。
可以用数组类型建立构造器引用。例如,int[]::new 是一个构造器引用,它有一个参数:即数组的长度。这等价于 lambda 表达式 x->new int[x]

变量作用域

lambda 表达式有三部分:

  • 一个代码块
  • 参数
  • 只有变量的值,指非参数而且不再代码中定义的变量。

例如:

  1. public static void repeatMessage(String text, int dalay) {
  2. ActionListener listener = event -> {
  3. System.out.println(text);
  4. Toolkit.getDefaultToolkit().beep();
  5. };
  6. new Timer(dalay, listener).start();
  7. }

这个 lambda 表达式有一个自由变量 text。表示lambda表达式的数据结构必须存储自由变量的值。我们说 text 被 lambda 表达式捕获(captured)。
可以看到,lambda 表达式可以捕获外围作用域中变量的值。在 Java 中,要确保所捕获的值是明确定义的,这里有一个重要的限制。在 lambda 表达式中,只能引用值不会改变的变量。例如,下面的做法是不合法的:

  1. public static void coutDown(int start, int dalay) {
  2. ActionListener listener = event -> {
  3. start--; // Error: Can't mutate cappture varibale
  4. System.out.println(start);
  5. };
  6. new Timer(dalay, listener).start();
  7. }

之所以有这个限制是有原因的。如果在 lambda 表达式中改变变量,并发执行多个动作时就会不安全
另外如果在 lambda 表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。例如,下面就是不合法的:

  1. public static void repeat(String text, int count) {
  2. for (int i = 1; i <= count; i++) {
  3. ActionListener listener = event -> {
  4. System.out.println(i + ": " + text);
  5. };
  6. new Timer(1000, listener).start();
  7. }
  8. }

这里有一条规则:lambda 表达式中捕获的变量必须实际上是最终变量(effectively final)。实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。在这里,text 总是指示同一个 String 对象,所以捕获这个变量是合法的。不过,i 的值会改变,因此不能捕获 i。
lambda 表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。在 lambda 表达式中声明与一个局部变量同名的参数或局部变量是不合法的。

  1. Paht first = Paths.get("/usr/bin");
  2. Comparator<String> comp = (first, second) -> first.length() - second.length();

在方法中,不能有两个同名的局部变量,因此,lambda 表达式中同样也不能有同名的局部变量。
在一个 lambda 表达式中使用 this 关键字时,是指创建这个 lambda 表达式的方法的 this 参数。例如,考虑下面的代码:

  1. public class Application() {
  2. public void init() {
  3. ActionListener listener = event -> {
  4. System.out.println(this.toString();
  5. ...
  6. }
  7. ...
  8. }
  9. }

表达式this.toString()会调用Application对象的toString方法,而不是ActionListener实例的方法。

处理 lambda 表达式

使用 lambda 表达式的重点是延迟执行(deferred execution)。之所以希望以后再执行代码,这有很多原因,如:

  • 在一个单独的线程中运行代码;
  • 多次运行代码;
  • 在算法的适当位置运行代码(例如,排序中的比较操作);
  • 发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等);
  • 只在必要时才运行代码。

比如,你现在想要重复一个动作 n 次,将这个动作和重复次数传递到一个 repeat 方法:

  1. repeat(10, () -> System.out.println("Hello World!"));

要接受这个 lambda 表达式,需要选择(偶尔可能需要提供)一个函数式接口。在这里,我们可以使用 Runnable 接口:

  1. public static void repeat(int n, Runnable action) {
  2. for (int i = 0; i < n; i++) action.run();
  3. }

调用 action.run() 会执行上述的 lambda 表达式:() -> System.out.println("Hello World!")
这里列出常用的函数式接口:

| 函数式接口 | 参数类型 | 返回类型 | 抽象方法名 | 描述 | 其他方法 | | —- | —- | —- | —- | —- | —- |

| Runnable | 无 | void | run | 作为无参数或返回值的动作进行 | |

| Supplier | 无 | T | get | 提供一个 T 类型的值 | |

| Consumer | T | void | accept | 处理一个 T 类型的值 | andThen |

| BiConsumer | T, U | void | accept | 处理 T 和 U 类型的值 | andThen |

| Function | T | R | apply | 有一个 T 类型参数的函数 | compose, andThen, identity |

| BiFunction | T, U | R | apply | 有 T 和 U 类型参数的函数 | andThen |

| UnaryOperator | T | T | apply | 类型 T 上的一元操作符 | compose, andThen, identity |

| BinaryOperator | T, U | T | apply | 类型 T 上的二元操作符 | andThen, maxBy, minBy |

| Predicate | T | boolean | test | 布尔值函数 | and, or, negate, isEqual |

| BiPredicate | T, U | boolean | test | 有两个参数的布尔值函数 | and, or, negate |

现在来改进一下上述例子,每次打印的时候,打印出是那一次迭代。这样的函数式接口需要包含一个方法,这个方法你有一个 int 参数而且返回类型为 void。处理 int 值得标准接口如下:

  1. public interface IntComsumer {
  2. void accept(int value);
  3. }

这样就可以写出 repeact 得改进版本:

  1. public static void repeat(int n, IntConsumer action) {
  2. for (int i = 0; i < n; i++) action.accept(i);
  3. }
  4. ...
  5. repeact(10, i -> System.out.println("Countdown: " + (9 - i)));

下面列出基本类型的函数式接口:

| 函数式接口 | 参数类型 | 返回类型 | 抽象方法名 | | —- | —- | —- | —- |

| BooleanSupplier | none | boolean | getAsBoolean |

| P_Supplier | none | _p | getAsP |

| P_Consumer | _p | void | accept |

| ObjP_Consumer | T, _p | void | accept |

| P_Functino | _p | T | apply |

| P_To_Q_Function | _p | q | applyAsQ |

| ToP_Function | T | _p | applyAsP |

| ToP_BiFunction | T, U | _p | applyAsP |

| P_UnaryOperator | _p | p | applyAsP |

| P_BinaryOperator | _p, p | p | applyAsP |

| P_Predicate | _p | boolean | test |

p, q 为 int, long, double; P, Q 为 Int, Long, Double

如果设计你自己的接口,其中只有一个抽象方法,可以用 @FunctionalInterface 注解来标记这个接口。这样做有两个优点。如果你无意中增加了另一个非抽象方法,编译器会产生一个错误消息。另外 javadoc 页里会指出你的接口是一个函数式接口。当然不是必须使用注解,根据定义,任何有一个抽象方法的接口都是函数式接口。不过使用 @FunctionalInterface 注解确实是一个很好的做法。

再谈 Comparator

Comparator 接口包含很多方便的静态方法来创建比较器。这些方法可以用于 lambda 表达式或方法引用
静态 comparing 方法是一个「键提取器」函数,它将类型 T 映射为一个可比较的类型。对要比较的对象应用这个函数,然后对返回的键完成比较
例如,假设有一个 Person 对象数组,可以如下按名字对这些对象排序:

  1. Arrays.sort(people, Comparator.comparing(Person::getName));

上述方法比手动实现一个 Comparator 相比,要容易得多。另外,代码也更为清晰,因为显然我们都希望按人名来进行比较。
可以把比较器与 thenComparing 方法串起来。例如:

  1. Arrays.sort(people, Comparator.comparing(Person::getLastName)
  2. .thenComparing(Person:getFirstName));

如果两个人的姓相同,就会使用第二个比较器。
comparing 有很多变体形式,可以该方法提取的键指定一个比较器,例如,可以根据人名长度完成排序:

  1. Arrays.sort(people, Comparator.comparing(Person::getLastName),
  2. (s, t) -> Integer.compare(s.length(), t.length()));

comparing 还有变体形式,可以避免 int, long, double 值的装箱。比如,这样可以完成前一个操作:

  1. Arrays.sort(people, Comparator.comparingInt(p -> p.getName().lenght()));

如果键函数会返回 null 值,就要用到 nullsFirst 和 nullLast 适配器。这些静态方法会修改现有的比较器,从而在遇到null值时不会抛出异常,而是将这个值标记为小于或大于正常值。