——Lamda表达式,为Java中的行为参数化提供了更简洁方便的实现 。这也是Java 中引入该语法的一个重要原因。
关于行为参数化,可参照:
【Java】函数式编程之通过行为参数化传递代码
文章目录
1. Lamda表达式是什么?
可理解为:一种可传递的匿名函数;没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
- 匿名:没有名称
- 函数:Lambda函数不像普通方法那样属于某个特定的类,但和方法一样,Lambda有参数列表、函数主体(普通方法{}中的内容)、返回类型,还可能有可以抛出的异常列表。
- 传递:Lambda表达式可以作为参数传递给方法或存储在变量中。
- 简洁:无需像匿名类那样写很多模板代码。
示例:
利用Lambda 表达式,你可以更为简洁地自定义一个Comparator对象:
先前:

用了Lambda表达式:

2. Lamda表达式的组成及语法
1)组成
Lambda表达式由参数、箭头和主体组成:

如上图:
- 参数列表:这里它采用了Comparator中compare方法的参数,两个Apple;
- 箭头:箭头->把参数列表与Lambda主体分隔开
- Lambda主体:比较两个Apple的重量。表达式就是Lambda的返回值
2)基本语法
(parameters) -> expression 即 (参数列表) -> 表达式
或(请注意语句的花括号)
(parameters) -> { statements; } 即 (参数列表) -> { 语句;}
3)练习
根据上述语法规则,以下哪个不是有效的Lambda表达式?
(1) () -> {}
(2) () -> "Raoul"
(3) () -> {return "Mario";}
(4) (Integer i) -> return "Alan" + i;
(5) (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";}。
3. Lamda表达式示例
1)

2)

4. 在哪里以及如何使用Lambda
Lambda可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法
函数式接口:
函数式接口就是只定义了一个抽象方法的接口。eg:Comparator和Runnable

测验:
下面哪些接口是函数式接口?
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可以做函数式接口一个具体实现的实例:Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。(用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化)
如下图示例:

函数描述符:
函数式接口的抽象方法的签名称为函数描述符;函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作 函数描述符。
即说明一个函数式接口的参数列表和返回值;eg:() -> void代表 了参数列表为空,且返回void的函数;
测验:
以下哪些是使用Lambda表达式的有效方式?
(1) execute(() -> {});
public void execute(Runnable r){
r.run();
}
(2) public Callable<String> fetch() {
return () -> "Tricky example ;-)";
}
(3) Predicate<Apple> p = (Apple a) -> a.getWeight();
答案:只有1和2是有效的。
第一个例子有效,是因为Lambda() -> {}具有签名() -> void,这和Runnable中的
抽象方法run的签名相匹配。请注意,此代码运行后什么都不会做,因为Lambda是空的!
第二个例子也是有效的。事实上,fetch方法的返回类型是Callable<String>。 Callable<String>基本上就定义了一个方法,签名是() -> String,其中T被String代替 了。因为Lambda() -> "Trickyexample;-)"的签名是() -> String,所以在这个上下文中可以使用Lambda。
第三个例子无效,因为Lambda表达式(Apple a) -> a.getWeight()的签名是(Apple) ->
Integer,这和Predicate<Apple>:(Apple) -> boolean中定义的test方法的签名不同。
5. 使用函数式接口
为了应用不同的Lambda表达式,你需要一套能够描述常见函数描述符的函数式接口。
Java API中已经有了几个函数式接口,Java 8的库设计师也帮你在java.util.function包中引入了几个新的函数式接口,方便我们可以直接使用。
Java 8中的常用函数式接口:


测验:函数式接口
对于下列函数描述符(即Lambda表达式的签名),你会使用哪些函数式接口?在表3-2中
可以找到大部分答案。作为进一步练习,请构造一个可以利用这些函数式接口的有效Lambda 表达式:
(1) T->R
(2) (int, int)->int
(3) T->void
(4) ()->T
(5) (T, U)->R
答案如下。
(1) Function<T,R>不错。它一般用于将类型T的对象转换为类型R的对象(比如
Function<Apple, Integer>用来提取苹果的重量)。
(2) IntBinaryOperator具有唯一一个抽象方法,叫作applyAsInt,它代表的函数描述
符是(int, int) -> int。
(3) Consumer<T>具有唯一一个抽象方法叫作accept,代表的函数描述符是T -> void。 (4) Supplier<T>具有唯一一个抽象方法叫作get,代表的函数描述符是()-> T。或者,
Callable<T>具有唯一一个抽象方法叫作call,代表的函数描述符是() -> T。
(5) BiFunction<T, U, R>具有唯一一个抽象方法叫作apply,代表的函数描述符是(T,
U) -> R。
Lambdas及函数式接口的例子:

6. 函数式接口的异常处理
任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda 包在一个try/catch块中。
比如,
// 方法一:
下面我们创建了一个新的函数式接口BufferedReaderProcessor,它显式声明了一个IOException:
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();
// 方法二:
你可能是在使用一个接受函数式接口的API,比如Function<T, R>,没有办法自己创建一个。这种情况下,你可以显式捕捉受检异常:
Function<BufferedReader, String> f = (BufferedReader b) -> {
try {
return b.readLine();
}
catch(IOException e) {
throw new RuntimeException(e);
}
7. Lambda 类型检查、类型推断以及限制
我们第一次提到Lambda表达式时,说它可以为函数式接口生成一个实例。然而,Lambda 表达式本身并不包含它在实现哪个函数式接口的信息。为了全面了解Lambda表达式,你应该知 道Lambda的实际类型是什么。
1)类型检查
Lambda的类型是从使用Lambda的上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)推断出来的。
Lambda表达式的类型检查过程:

类型检查过程可以分解为如下所示。
首先,你要找出filter方法的声明。
第二,要求它是Predicate(目标类型)对象的第二个正式参数。
第三,Predicate是一个函数式接口,定义了一个叫作test的抽象方法。
第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。
最后,filter的任何实际参数都必须匹配这个要求。
ps:如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配。
2)类型推断
可以在Lambda语法中省去标注参数类型:

ps:当Lambda仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略
3)使用局部变量
上面所介绍的所有Lambda表达式都只用到了其主体里面的参数。但Lambda表达式 也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被称作捕获Lambda。例如,下面的Lambda捕获了portNumber变量:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final, 或事实上是final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获 实例变量可以被看作捕获最终局部变量this。)
例如,下面的代码无法编译,因为portNumber 变量被赋值两次:

为什么对局部变量的限制?
第一,实例变量和局部变量背后的实现有一 个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局 部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线 程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它 的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了 这个限制。
第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(我们会在以后的各章中 解释,这种模式会阻碍很容易做到的并行处理)。
8. 方法引用
方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。
示例:

Lambda及其等效方法引用的例子:

1)指向静态方法的方法引用
如Integer的parseInt方法,写作Integer::parseInt
2) 指向任意类型实例方法的方法引用
你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。例如,Lambda 表达式(String s) -> s.toUppeCase()可以写作String::toUpperCase。
如String的length方法,写作 String::length;
3) 指向现有对象的实例方法的方法引用
你在Lambda中调用一个已经存在的外部对象中的方法:
假设你有一个局部变量expensiveTransaction 用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)。
如下图:三种不同类型的Lambda表达式构建方法引用的办法

测验:
下列Lambda表达式的等效方法引用是什么?
(1) Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);
(2) BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element);
答案如下。
(1) 这个Lambda表达式将其参数传给了Integer的静态方法parseInt。这种方法接受一 个需要解析的String,并返回一个Integer。因此,可以使用上图中的办法➊(Lambda表达 式调用静态方法)来重写Lambda表达式,如下所示:
Function<String, Integer> stringToInteger = Integer::parseInt;
(2) 这个Lambda使用其第一个参数,调用其contains方法。由于第一个参数是List类型
的,你可以使用上图的办法➋,如下所示: BiPredicate<List<String>, String> contains = List::contains;
这是因为,目标类型描述的函数描述符是 (List<String>,String) -> boolean,而 List::contains可以被解包成这个函数描述符。
9. 构造方法引用
ClassName::new
示例:
1)无参构造方法

2)有参构造方法
Apple(Integer weight):

10. 复合Lambda表达式的有用方法
这意味着你可以把多个简单的Lambda复合成复杂的表达式
如:
1)Comparator
1. 逆序 - reversed

2. 比较器链 - thenComparing

2)Predicate
1. 非 - negate

2. 且 - and

3. 或 - or

3)Function
1. andThen

2. compose

andThen和compose之间的区别:

11. 小结
- Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。
- Lambda表达式让你可以简洁地传递代码。
- 函数式接口就是仅仅声明了一个抽象方法的接口。
- 只有在接受函数式接口的地方才可以使用Lambda表达式。
- Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
- Java 8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate、Function<T,R>、Supplier、Consumer和BinaryOperator等
- 为了避免装箱操作,对Predicate和Function<T, R>等通用函数式接口的原始类型特化:IntPredicate、IntToLongFunction等。
- 环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配 和清理)可以配合Lambda提高灵活性和可重用性。
- Lambda表达式所需要代表的类型称为目标类型。方法引用让你重复使用现有的方法实现并直接传递它们。
- Comparator、Predicate和Function等函数式接口都有几个可以用来结合Lambda表达式的默认方法。
2271





