Lambda概述

lambda表达式没有名称,但是它有参数列表,函数体和返回类型,还可能包含一个异常列表。

  • 匿名,lambda表达式不像方法一样有一个显式的名字
  • 函数,lambda表达式不像方法一样关联到一个特定的类
  • 传递,lambda表达式可以像函数参数一样传递,或者保存到变量里面
  • 简洁,不需要像匿名类一样写很多样板代码

lambda基本语法有2种

1
(parameters) -> expression

1
(parameters) -> { statements; }
用例 lambda示例
布尔表达式 (List<String> list) -> list.isEmpty()
创建对象 () -> new Apple(10)
消费对象 (Apple a) -> { System.out.println(a.getWeight()); }
从对象中提取属性 (String s) -> s.length()
合并两个值 (int a, int b) -> a * b
比较两个对象 (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

哪里可以使用lambda

你可以在函数接口上下文中使用lambda表达式

函数接口

简单地说,函数接口就是只有一个抽象方法的接口,比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface Predicate<T> {
    boolean test (T t);
}

public interface Comparator<T> {
    int compare(T o1, T o2);
}

public interface Runnable {
    void run();
}

public interface ActionListener extends EventListener {
    void actionPerformed(ActionEvent e);
}

public interface Callable<V> {
    V call() throws Exception;
}

public interface PrivilegedAction<T> {
    T run();
}

注意,接口现在可以有默认方法。如果一个接口有许多默认方法,只要它只有一个抽象方法,那么它仍然是一个函数接口。

lambda表达式可以为函数接口的抽象方法提供实现,并将整个表达式看做函数接口的一个实例。

函数描述符

函数接口的抽象方法的签名描述了lambda表达式的签名,我们称这个抽象方法为函数描述符。我们使用一个特殊的标记来描述函数描述符,比如() -> void表示参数为空并返回void的函数。

只要lambda表达式具有与函数接口的抽象方法相同的签名,就可以将lambda表达式赋值给变量或传递给参数为函数接口的方法。

下面这个lambda表达式合法:() -> System.out.println(“This is awesome”); System.out.println不是一个表达式,为什么不需要用花括号括起来? 原来,在Java语言规范中定义了一个用于void方法调用的特殊规则,void方法调用不需要使用花括号。

@FunctionalInterface用于指示一个接口是函数接口。如果使用@FunctionalInterface定义一个不是函数接口的接口,编译器会返回一个警告。@FunctionalInterface不是强制性的,但是使用它定义函数接口是个好习惯。

使用函数接口

Predicate<T>

java.util.function.Predicate<T>定义如下:

1
2
3
4
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Consumer<T>

java.util.function.Consumer<T>定义如下:

1
2
3
4
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

Function<T, R>

java.util.function.Function<T, R>定义如下:

1
2
3
4
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

Java 8同样添加了特殊版的函数接口以避免基础类型的autoboxing:

1
2
3
public interface IntPredicate {
    boolean test(int t);
}

常用函数接口表

函数接口 函数描述符 特殊版本函数接口
Predicate<T> T -> boolean IntPredicateLongPredicateDoublePredicate
Consumer<T> T -> void IntConsumerLongConsumerDoubleConsumer
Function<T, R> T -> R IntFunction<R>IntToDoubleFunctionIntToLongFunctionLongFunction<R>LongToDoubleFunctionLongToIntFunctionDoubleFunction<R>DoubleToIntFunctionDoubleToLongFunctionToIntFunction<T>ToDoubleFunction<T>ToLongFunction<T>
Supplier<T> () -> T BooleanSupplierIntSupplierLongSupplierDoubleSupplier
UnaryOperator<T> T -> T IntUnaryOperatorLongUnaryOperatorDoubleUnaryOperator
BinaryOperator<T> (T, T) -> T IntBinaryOperatorLongBinaryOperatorDoubleBinaryOperator
BiPredicate<T, U> (T, U) -> boolean
BiConsumer<T, U> (T, U) -> void ObjIntConsumer<T>ObjLongConsumer<T>ObjDoubleConsumer<T>
BiFunction<T, U, R> (T, U) -> R ToIntBiFunction<T, U>ToLongBiFunction<T, U>ToDoubleBiFunction<T, U>

注意这些函数接口都不允许抛出checked异常。如果你需要lambda表达式抛出异常,你可以定义自己的函数接口声明checked异常,或者在lambda表达式函数体中catch异常。

类型检查,类型推断与限制

类型检查

lambda表达式的类型是从使用lambda表达式的上下文中推导出来的。lambda表达式上下文中期待的类型称为目标类型。

同样的lambda不同的函数接口

由于目标类型的原因,如果不同的函数接口具有兼容的抽象方法签名,相同的lambda表达式可以关联到它们。

1
2
3
Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

特殊的void兼容规则

如果lambda函数体是一个表达式语句,则它和返回void的函数描述符兼容。比如:

// Predicate has a boolean return

Predicate<String> p = (String s) -> list.add(s);

// Consumer has a void return

Consumer<String> b = (String s) -> list.add(s);

类型推断

Java编译器可以推断出lambda表达式关联的函数接口,意味着它也能够推导出相应的抽象函数的签名。好处是Java编译器知道lambda表达式的参数类型,因此lambda表达式可以忽略掉参数类型:

1
2
3
List<Apple> greenApples = filter(inventory, apple -> GREEN.equals(apple.getColor()));
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

使用局部变量

lambda表达式允许使用自由变量(不是参数且定义在外部作用域),就像匿名类一样。

1
2
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);

Lambda可以不受限制地捕获实例变量和静态变量。但是当捕获局部变量时,必须显式地声明它们为final或有效的final。Lambda表达式可以捕获只分赋值一次的局部变量。下面的代码将编译不了:

1
2
3
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;

局部变量的限制

实例变量和局部变量在实现方式上有一个关键区别。实例变量存储在堆上,而局部变量存储在堆栈上。如果lambda可以直接访问局部变量,并且lambda在线程中使用,那么使用lambda的线程可以在分配变量的线程释放变量之后尝试访问该变量。因此Java将对自由变量的访问实现为对它的副本的访问,而不是对原始变量的访问。

方法引用

方法引用可以看作是只调用特定方法的lambda表达式的简写,可以将方法引用看作lambda的语法糖。

Lambda 等价方法引用
(Apple apple) -> apple.getWeight() Apple::getWeight
() -> Thread.currentThread().dumpStack() Thread.currentThread()::dumpStack
(str, i) -> str.substring(i) String::substring
(String s) -> System.out.println(s) System.out::println
(String s) -> this.isValidName(s) this::isValidName

构造方法引用

如下图:

method-reference.png

构造函数引用

使用类名和关键字new创建构造函数引用:ClassName::new,类似静态方法引用。比如,如果有不带参数的构造函数

1
2
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();

等价于

1
2
Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();

如果有带一个参数的构造函数,则满足Function函数接口:

1
2
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);

如果有带亮个参数的构造函数,则满足BiFunction函数接口:

1
2
BiFunction<Color, Integer, Apple> c3 = Apple::new;
Apple a3 = c3.apply(GREEN, 110);

组合lambda表达式的有用方法

组合Comparators

静态方法Comparator.comparing返回一个基于函数的Comparator,该函数提取一个关键字进行比较,如下所示:

1
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

反向比较

reversed方法反转指定Comparator的顺序:

1
inventory.sort(comparing(Apple::getWeight).reversed());

链接Comparators

1
inventory.sort(comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry));

组合Predicates

谓词接口包含三个方法,可以重用现有的谓词来创建更复杂的谓词:negateandor

1
2
3
Predicate<Apple> notRedApple = redApple.negate();
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150).or(apple -> GREEN.equals(a.getColor()));

注意方法的优先级是从左到右。

组合Functions

Function接口提供2个方法用于组合lambda表达式:andThencompose

1
2
3
4
5
6
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g); // In mathematics you’d write g(f(x))
Function<Integer, Integer> j = f.compose(g); // In mathematics you’d write f(g(x))
int result1 = h.apply(1);
int result2 = j.apply(1);