Lambda表达式
可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
- 匿名——我们说匿名,是因为它不像普通的方法那样有一个明确的名称:写得少而想得多!
- 函数——我们说它是函数,是因为Lambda函数不像方法那样属于某个特定的类。但和方法一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
- 传递——Lambda表达式可以作为参数传递给方法或存储在变量中。
- 简洁——无需像匿名类那样写很多模板代码。
Lambda来自于学术界开发出来的一套用来描述计算的λ演算法。它可以让你十分简明地传递代码。理论上来说,你在Java 8之前做不了的事情,Lambda也做不了。但是,现在你用不着再用匿名类写一堆笨重的代码,来体验行为参数化的好处了!Lambda表达式鼓励你采用行为参数化风格。最终结果就是你的代码变得更清晰、更灵活。比如,利用Lambda表达式,你可以更为简洁地自定义一个 Comparator 对象。
先前代码:
Comparator<Apple> byWeight = new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
};
之后(用了Lambda表达式):
Comparator<Apple> byWeight =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Lambda表达式有三个部分:
- 参数列表——这里它采用了 Comparator 中 compare 方法的参数,两个 Apple 。
- 箭头——箭头 -> 把参数列表与Lambda主体分隔开。
- Lambda主体——比较两个 Apple 的重量。表达式就是Lambda的返回值了。
Lambda的基本语法是:
(parameters) -> expression
或(请注意语句的花括号)
(parameters) -> { statements; }
下面的例子:
- () -> {}
- () -> "Raoul"
- () -> {return "Mario";}
- (Integer i) -> return "Alan" + i;
- (String s) -> {"IronMan";}
只有4和5是无效的Lambda。
(1) 这个Lambda没有参数,并返回 void 。它类似于主体为空的方法: public void run() {} 。
(2) 这个Lambda没有参数,并返回 String 作为表达式。
(3) 这个Lambda没有参数,并返回 String (利用显式返回语句)。
(4) return 是一个控制流语句。要使此Lambda有效,需要使花括号,如下所示:(Integer i) -> {return "Alan" + i;} 。
(5)“Iron Man”是一个表达式,不是一个语句。要使此Lambda有效,你可以去除花括号和分号,如下所示: (String s) -> "Iron Man" 。或者如果你喜欢,可以使用显式返回语句,如下所示:
(String s)->{return "IronMan";} 。
函数式接口
定义:函数式接口就是只定义一个抽象方法的接口。
注意:接口现在还可以拥有 默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口
下面哪些接口是函数式接口?
public interface Adder{
int add(int a, int b);
}
public interface SmartAdder extends Adder{
int add(double a, double b);
}
public interface Nothing{
}
只有 Adder 是函数式接口。SmartAdder 不是函数式接口,因为它定义了两个叫作 add 的抽象方法(其中一个是从Adder 那里继承来的)。Nothing 也不是函数式接口,因为它没有声明抽象方法。
Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。下面的代码是有效的,因为 Runnable 是一个只定义了一个抽象方法 run的函数式接口:
@FunctionalInterface
函数式接口带有 @FunctionalInterface 的标注这个标注用于表示该接口会设计成一个函数式接口。如果你用 @FunctionalInterface 定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding abstract methods found in interface Foo”,表明存在多个抽象方法。请注意, @FunctionalInterface 不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是 @Override标注表示方法被重写了。
特殊的void兼容规则
如果一个Lambda的主体是一个语句表达式, 它就和一个返回 void 的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管 List 的 add 方法返回了一个boolean ,而不是 Consumer 上下文( T -> void )所要求的 void :
// Predicate返回了一个boolean
Predicate<String> p = s -> list.add(s);
// Consumer返回了一个void
Consumer<String> b = s -> list.add(s);
使用局部变量
Lambda表达式允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被称作捕获Lambda。例如,下面的Lambda捕获了 portNumber 变量:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为 final ,或事实上是 final 。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量 this 。) 例如,下面的代码无法编译,因为 portNumber变量被赋值两次:
实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。使用final修饰之后,变量值不会被修改,所以这份副本始终有效。
方法引用
方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。
先前:
inventory.sort((Apple a1, Apple a2)
-> a1.getWeight().compareTo(a2.getWeight()));
之后(使用方法引用和 java.util.Comparator.comparing ):
inventory.sort(comparing(Apple::getWeight));
方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。但是,显式地指明方法的名称,你的代码的可读性会更好。它是如何工作的呢?当你需要使用方法引用时,目标引用放在分隔符 :: 前,方法的名称放在后面。例如,Apple::getWeight 就是引用了 Apple 类中定义的方法 getWeight 。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是Lambda表达式 (Apple a) -> a.getWeight() 的快捷写法。表3-4给出了Java 8中方法引用的其他一些例子。
如何构建方法引用
方法引用主要有三类。
- 指向静态方法的方法引用(例如 Integer 的 parseInt 方法,写作 Integer::parseInt )。
- 指向任意类型实例方法的方法引用( 例如String 的 length 方 法 , 写 作String::length )。
- 指向现有对象的实例方法的方法引用(假设你有一个局部变量 expensiveTransaction用于存放 Transaction 类型的对象,它支持实例方法 getValue ,那么你就可以写 expensiveTransaction::getValue )。
第二种和第三种方法引用可能乍看起来有点儿晕。类似于 String::length 的第二种方法引用的思想就是你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。例如,Lambda表达式 (String s) -> s.toUppeCase() 可以写作 String::toUpperCase 。但第三种方法引用指的是,你在Lambda中调用一个已经存在的外部对象中的方法。例如,Lambda表达式()->expensiveTransaction.getValue() 可以写作 expensiveTransaction::getValue 。
类型 | 语法 | 对应的Lambda表达式 |
---|---|---|
静态方法引用 | 类名::staticMethod | (args) -> 类名.staticMethod(args) |
实例方法引用 | inst::instMethod | (args) -> inst.instMethod(args) |
对象方法引用 | 类名::instMethod | (inst,args) -> 类名.instMethod(args) |
构造方法引用 | 类名::new | (args) -> new 类名(args) |
总结:
- Lambda 表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。
- Lambda 表达式让你可以简洁地传递代码。
- 函数式接口就是仅仅声明了一个抽象方法的接口。
- 只有在接受函数式接口的地方才可以使用 Lambda 表达式。
- Lambda 表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
- Java 8 自带一些常用的函数式接口,放在 java.util.function 包里,包括 Predicate<T> 、 Function<T,R> 、 Supplier<T> 、 Consumer<T> 和 BinaryOperator<T> ,如表3-2 所述。
- 为了避免装箱操作,对 Predicate<T> 和 Function<T, R> 等通用函数式接口的原始类型特化: IntPredicate 、 IntToLongFunction 等。
- 环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配和清理)可以配合 Lambda 提高灵活性和可重用性。
- Lambda 表达式所需要代表的类型称为目标类型。
- 方法引用让你重复使用现有的方法实现并直接传递它们。
- Comparator 、 Predicate 和 Function 等函数式接口都有几个可以用来结合 Lambda 表达式的默认方法。
参考:Java8实战