【Java 8】一篇搞定Lambda表达式——Lambda表达式详解与使用

——Lamda表达式,为Java中的行为参数化提供了更简洁方便的实现 。这也是Java 中引入该语法的一个重要原因。

关于行为参数化,可参照:
【Java】函数式编程之通过行为参数化传递代码

1. Lamda表达式是什么?

可理解为:一种可传递的匿名函数;没有名称,但它有参数列表函数主体返回类型,可能还有一个可以抛出的异常列表

  • 匿名:没有名称
  • 函数:Lambda函数不像普通方法那样属于某个特定的类,但和方法一样,Lambda有参数列表、函数主体(普通方法{}中的内容)、返回类型,还可能有可以抛出的异常列表。
  • 传递:Lambda表达式可以作为参数传递给方法存储在变量中
  • 简洁:无需像匿名类那样写很多模板代码。

示例:

利用Lambda 表达式,你可以更为简洁地自定义一个Comparator对象:

先前:
20200404205352662
用了Lambda表达式:
image-20200404205412423

2. Lamda表达式的组成及语法

1)组成

Lambda表达式由参数箭头主体组成:
20200404205235414
如上图:

  1. 参数列表:这里它采用了Comparator中compare方法的参数,两个Apple;
  2. 箭头:箭头->把参数列表与Lambda主体分隔开
  3. 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)
20200406022240644

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();
答案:只有12是有效的。
第一个例子有效,是因为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中的常用函数式接口:

20200405003040738
20200405003055407
测验:函数式接口

对于下列函数描述符(即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表达式的类型检查过程:

20200405150843625

类型检查过程可以分解为如下所示。
 首先,你要找出filter方法的声明。
 第二,要求它是Predicate(目标类型)对象的第二个正式参数。
 第三,Predicate是一个函数式接口,定义了一个叫作test的抽象方法。
 第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。
 最后,filter的任何实际参数都必须匹配这个要求。

ps:如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配

2)类型推断

可以在Lambda语法中省去标注参数类型:
20200405151935005

ps:当Lambda仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略

3)使用局部变量

上面所介绍的所有Lambda表达式都只用到了其主体里面的参数。但Lambda表达式 也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被称作捕获Lambda。例如,下面的Lambda捕获了portNumber变量:

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

Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量静态变量。但局部变量必须显式声明为final, 或事实上是final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获 实例变量可以被看作捕获最终局部变量this。)

例如,下面的代码无法编译,因为portNumber 变量被赋值两次:
20200405152914249
为什么对局部变量的限制?

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

8. 方法引用

方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。

示例:

20200405153328128

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

20200405153451412

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表达式构建方法引用的办法
20200405160331221
测验:

下列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)无参构造方法

20200406010901039

2)有参构造方法

Apple(Integer weight):
20200406010948167

10. 复合Lambda表达式的有用方法

这意味着你可以把多个简单的Lambda复合成复杂的表达式

1)Comparator

1. 逆序 - reversed
20200406020945429
2. 比较器链 - thenComparing

20200406021037856

2)Predicate

1. 非 - negate
20200406021205273
2. 且 - and
在这里插入图片描述
3. 或 - or
20200406021500065

3)Function

1. andThen
在这里插入图片描述
2. compose
在这里插入图片描述
andThen和compose之间的区别
20200406021723559

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表达式的默认方法。
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值