Lambda 表达式是 Java 8 的新特性,本场 Chat 讲解了 Lambda 表达式的所有知识。内容涉及到 Lambda 表达式是什么,Lambda 表达式用在何处,在 GUI 应用程序中使用 Lambda 表达式,Lambda 表达式语法,变量访问权限,目标类型,序列化,方法引用等。
本场 Chat 主要内容:
- Lambda 表达式是什么?
- Lambda 表达式用在何处?2.1. 方法一:创建方法,寻找符合条件的会员,但方法所指定的条件是硬编码;2.2. 方法二:创建一个适应性更好的方法,去寻找符合条件的会员;2.3. 方法三:在独立类中定义筛选会员的条件;2.4. 方法四:在匿名类中定义筛选会员的条件;2.5. 方法五:使用 Lambda 表达式规定筛选会员的条件;2.6. 方法六:在标准函数式接口环境中,使用 Lambda 表达式;2.7. 方法七:让程序的所有功能都使用 Lambda 表达式;2.8. 方法八:使用泛型,进一步提高方法的适应性;2.9. 方法九:使用聚合操作,并用 Lambda 表达式作为聚合操作的参数。
- 在 GUI 应用程序中使用 Lambda 表达式;
- Lambda 表达式语法;
- 变量访问权限;
- 目标类型;6.1. 目标类型和方法参数。
- 序列化;
- 方法引用。8.1. 访问静态方法;8.2. 访问特定对象的实例方法;8.3. 访问特定类型的随机对象的实例方法;8.4. 访问构造函数。
Lambda 表达式是 Java 8 的新特性,是 Oracle 公司为了增强 Java 基础功能而引入的一种编程语法。请注意, Lambda 表达式是一种新的 Java 编程语法,你将会看到你以前所没有看到的 Java 编程语法,相信能够让你耳目一新。
首先,需要指出的是,对于第一次接触 Lamdba 表达式的程序员来说,尽管 Lambda 表达式看起来很新鲜,但需要注意的是, Lamdba 表达式本质是一个函数式接口( functional interface )的实现类的实例。
函数式接口:一个加上注解 @FunctionalInterface 的接口,例如接口 Comparator 。
这样的接口只有一个抽象方法( 方法被 public abstract 修饰,或是默认没有任何修饰)。注解 @FunctionalInterface 是一个信息型( informative annotation )的注解,标示接口是一个函数式的接口,区别于普通的接口。
需要注意的是,函数式接口中的默认方法,因为他们已经有了默认实现,所以他们并不计入抽象方法。关于默认方法,以后会讲,此处暂时不展开。
另外,函数式接口所定义的抽象方法若是和顶级类 Object 定义的抽象方法一样,该方法不计入函数式接口的抽象方法。比如,函数式接口定义了这样一个方法,“ int hashCode(); ”,因为 Object 顶级类也定义这样一个抽象方法,“ public native int hashCode(); ”,所以抽象方法 hashCode 不计入函数式接口的抽象方法。
这时你在思考,为何这样的抽象方法不计入呢?原因很简单。
Java 语法规定,任何类都有一个默认的上级类,那就是顶级类 Object,并对顶级 Object 的抽象方法做了默认实现。在本例中,函数式接口的实现类也是一样,它也默认继承了顶级类 Object ,并顶级类 Object 的抽象方法 hashCode 做了默认实现,这相当于,对函数式接口中定义的 hashCode 也做了默认实现,函数式接口的实现类若是不想覆盖抽象方法 hashCode ,保持默认实现,在 Java 语法上讲,完全没有问题。
正是因为这些抽象方法(函数式接口和顶级类 Object 都定义的抽象方法),在函数式接口的实现类中可以不覆盖,所有它们不计入函数式接口的抽象方法。只有全新定义的抽象方法能被计入函数式接口的抽象方法,而且只有唯一的一个。
需要指出的是,若是一个接口的定义符合函数式接口的定义,即使没有加上注解 @FunctionalInterface ,编译器同样会认为它是一个函数式接口。@FunctionalInterface 是一个信息型的注解,起到标示的作用,让人一目了然。同时,若是一个接口加上此注解,但是,定义却是不符合函数式接口的定义,编译器便会报出错误。所以,我们建议,若是你想定义一个函数式接口,最好还是写上注解 @FunctionalInterface ,虽然这不是必须的。
我们说过, Lambda 表达式是函数式接口实现类的实例,那么,编写Lambda 表达式,实际上,就是在编写函数式接口唯一的抽象方法的实现。
Lambda 表达式有如下特点:
调用方法时,它可以作为方法的参数。调用方法时,需要给方法参数传值,这个值可以是基础类型的值,也可以是一个类的实例。在 Java 8 中,你可以把一个 Lambda 表达式传递给方法,作为方法的参数。 Lambda 表达式是函数式接口实现类的实例,所以, lambda 表达式作为方法的参数,实际就是把类实例作为方法的参数。编写 Lambda 表达式,实际是编写函数式接口唯一的抽象方法的实现。因此,它是具备某种行为,或者说是具备某种功能的代码单元,这样的功能代码,可以传递给方法的参数。
方法引用( Method References )更加简洁和可读性更好,它由lambda 表达式演变而来。关于方法引用,我们接下来会详细的讲解,你会见识它这一振奋人心的特性。
默认方法的功能允许你把新的默认方法添加到老旧的接口中,但依旧能够保持兼容性。一般来说,我们定义了接口,接着就会给这个接口添加一个或一个以上的实现类。在后期的程序版本升级中,我们需要修改早期定义的接口,为之添加新的方法。若是这些方法是默认的,也即是 public abstract,那么,该接口的所有实现类都必须做相应的修改,为这些新添加的抽象方法添加实现。若是不去修改这些实现类,那么,编译报错,出现了代码兼容性的问题。于是,我们就开始思考,能否做到,为早期的接口添加方法的同时,不用去修改它的实现类,代码依旧可以不报错,保持兼容性呢?若是把这些在早期定义的接口中新添加的方法定义为默认方法,即有关键字 default,这样可以保证兼容性。默认方法,即是在接口定义它时,已经为之做了默认实现的方法。既然如此,接口的实现类可以重写它,也可以不重写它,保持它默认的实现。再次提醒一下,默认方法的定义,需要加上关键字 default。关于默认方法,以后会详细讲解,此处暂时不展开。
静态方法的功能允许你把新的静态方法添加到老旧的接口中,但依旧能够保持兼容性。静态方法即是静态的默认方法。在接口定义方法中,加上关键字 static。既然静态方法也是默认方法,为早期的接口添加静态方法的同时,不用去修改它的实现类,代码依旧可以不报错,保持兼容性。关于静态方法,以后会详细讲解,此处暂时不展开。
Java 8 新添加了一些类和增强了一些类(修改原有的类,使之功能更加强大),很好的利用了 Lambda 表达式和 Stream 。关于 Stream,接下类我们会详细的讲解。
许多的方法,它的参数是接口类型的,当我们的程序调用这个方法时,需要为之传递一个实现了这个接口的实现类的实例。此处,我们假设有一个方法 F,它的参数是接口类型 I,为了调用这个方法 F,一种比较笨拙的方式是,定义一个类,假设为 B,类 B 实现接口 I,创建类 B 的实例,拿着类 B 的实例作为方法 F 的参数。显然,人们意识到了这种调用方法的笨拙,于是,就出现了匿名的实现类。我们知道,匿名的实现类,不需要独立创建接口的实现类,在给方法传递参数时即可直接实例化一个匿名类的实例,同时,不需要指定接口实现类的类名(即称之为匿名)。这样的方式,显得更加的简洁和方便。
但是,有了匿名实现类,我们依旧面临一个问题。若是接口只有一个抽象方法,为了实现这个抽象方法,我们还要为之创建匿名实现类,这样还是显得很笨拙和不清晰。在这样的情景中,使用 Lambda 表达式,你将会看到更加简洁和可读性更好的代码。如同前面所讲,调用方法时, Lambda 可以作为方法的参数。Lambda 表达式是函数式接口实现类的实例,所以,Lambda 表达式作为方法的参数,实际就是把一个类实例作为方法的参数。Lambda 表达式表达或是设计了一组功能,把它传递给方法作为参数,实际上,可以理解为把一组功能传递给了方法。因此,为了让代码更加简洁,编程更加高效,调用方法(该方法的参数类型是接口类型)时,若接口有多个抽象方法,我们可以创建这个接口的匿名实现类的实例,作为方法的参数。若是接口只有唯一一个抽象方法,比如函数式接口,我们可以创建这个接口的 Lambda 表达式,作为调用方法的参数,把一组功能传递给方法。比如说,在一个 GUI ( Graphical User Interface )程序中,我们点击按钮,就会调用响应函数,我们可以把 Lambda 表达式作为响应函数的参数,传递给响应函数。这个 Lambda 表达式规定了响应逻辑,比如弹出一个提示窗口给 GUI 使用用户。
2. Lambda 表达式用在何处我们假设有这样一种情景,我们要创建一个社交网络应用( social networking application ),管理员应该能够管理所有的应用会员( members of the social networking application ),当会员符合一定的条件,管理员就会对他们执行一些操作,比如给他们发送消息。下面,我们详细地描述这个场景。
- 业务:对符合条件,被选中的会员执行一些操作;
- 执行者:管理员;
- 前置条件:管理员已经登录社交网络应用;
- 后置条件:只对符合条件,被选中的会员执行一些操作,而不是针对所有会员。
- 详细业务:
- 管理员指定条件;
- 管理员指定操作;
- 管理员点击提交按钮;
- 系统后台找出符合条件的会员;
- 系统后台对符合条件的会员执行操作。
- 扩展:管理员在点击提交按钮之前,或者是在管理员指定的操作发生之前,管理员能够预览符合指定条件的会员。
- 业务发生的频率:一天能发生多次。
假设社交网络应用的会员实体类如下所示:
package cn.lambda.test;import java.time.LocalDate;public class Person { // 性别枚举 public enum Sex { MALE, FEMALE } private String name; // 姓名 private LocalDate birthday; // 生日 private Sex gender; // 性别 private String emailAddress; // 邮件地址 private int age; // 年龄 public void printPerson() { // 打印会员的个人信息 } // getter setter methods}
假设所有应用会员存储在集合 List roster 对象中。
下面我们使用 9 种方法实现上述的场景,设计的难度由浅到深,适应性由窄到广。一开始,我们使用单纯( naive )的方法,接着呢,我们使用独立类和匿名类改善这个方法,最终,我们使用 Lambda 表达式,让方法变得高效并且简洁。
2.1. 方法一:创建方法,寻找符合条件的会员,但方法所指定的条件是硬编码因为这个方法只能匹配一种条件,即年龄大于指定的数字,若是需要匹配其他的条件,比如性别是男的,一种最简单的方式就是,再次创建一个方法,让他匹配另一种条件,即性别。
public static void printPersonsOlderThan(List roster, int age) { for (Person p : roster) { if (p.getAge() >= age) { p.printPerson(); } }}
这个方法的适应性很窄,若是你的程序进行升级,这个方法很有可能就不能用了。假设你修改了会员实体类的数据结构,把年龄 age 的数据类型修改为字符 String 类型;假设你修改了计算年龄的算法,年龄小于某个指定的数字。这样的一些修改,这个方法不但不能实现业务,而且有可能编译错误。另外,就算后期不去升级程序,为了适应匹配其他的条件,比如指定性别,指定邮件地址等,我们需要创建许多类似的方法去满足业务的需要。
2.2. 方法二:创建一个适应性更好的方法,去寻找符合条件的会员如下这个方法比起上一个例子的方法 printPersonsOlderThan ,它的适应性更好,目标会员的条件是他们的年龄范围。
public static void printPersonsWithinAgeRange(List roster, int low, int high) { for (Person p : roster) { if (low = 18 && p.getAge() = 18 && p.getAge() p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() p.printPerson());
关注 processPersons 的第三个参数,它代表对符合条件的应用会员执行操作的 Lambda 表达式。该 Lambda 表达式只有一个参数,即 p ,它对标准函数式接口 Consumer 的抽象方法 accept 的实现逻辑是“ p -> p.printPerson()); ”,即打印出会员的信息,当然,它也可以是其他的实现逻辑,其他的操作。也许,现在你对 processPersons 方法的第三个参数的内涵依旧不是很能理解。下面,我们尝试使用匿名实现类的方式来调用 processPerson 方法,相信你会有更深的认识。
List roster = ...processPersons(roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() p.getEmailAddress(), email -> System.out.println(email));
传递给 processPersonsWithFunction 的第三个参数的是 Lambda 表达式,该 Lambda表达式代表提取符合条件的应用会员的邮件地址(把符合条件的应用会员转化为会员的邮件地址)。第四个参数仍然是一个 Lambda 表达式,该表达式代表对邮件地址执行一个操作,即打印邮件地址。作为初学者,也许现在你对processPersonsWithFunction 方法的第三、第四个参数的内涵依旧不是很能理解。下面,我们尝试使用匿名实现类的方式来调用processPersonsWithFunction 方法,相信你会有更深的认识。
List roster = ...processPersonsWithFunction(roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() p.getEmailAddress(), email -> System.out.println(email) );
对方法 processElements 的调用和对方法 processPersonsWithFunction 的调用没有不同。只是,方法 processElements 的适应性得到更进一步的提升。
值得关注的是, processElements 这个方法的执行流程,通过分析执行流程,我们可以为此进入另一个话题。
- 从集合数据源中获取一个资源对象( source object )。在本例中,遍历集合 List roster,获取一个一个的应用会员 Person 。需要注意的是,roster 的类型是 List ,同样也是类型 Iterable 。
- 使用断言,筛选资源对象。经筛选后,资源对象成为了已筛选对象( filtered object )。在本例中,筛选符合美国义务兵役制度的应用会员,制度的具体条件是男性且年龄在 18 至 25 岁之间。断言参数是标准函数式接口类型, Predicate tester ,我们使用 Lambda 表达式为该参数传值。
- 对已筛选对象执行转化操作。经转化后,已筛选对象成为了已映射对象( mapped object )。在本例中,把筛选出来的应用会员转化为会员邮件地址(从筛选出来的应用会员中提取他们的邮件地址)。转化操作参数是标准函数式接口类型, Function mapper ,我们使用 Lambda 表达式为该参数传值。
- 对已映射对象执行操作。在本例中,该操作就是打印会员的邮件地址。操作参数是标准函数式接口类型, Consumer block ,我们使用 Lambda 表达式为该参数传值。
我们可以清楚的看到,以上执行流程是一环接一环。集合数据源->资源对象->已筛选对象->已映射对象->操作。为了执行这个流程,我们的方法 processElements 一共声明了 4 个参数,其中一个是集合类型,三个是标准函数式接口类型。
- 针对第一个集合类型的参数 Iterable source ,我们对它进行遍历,获取资源对象。于是我们思考,能否让 JDK 自动遍历
- 针对第二个标准函数式接口类型的参数 Predicate tester ,我们调用它的 test 方法对资源对象进行断言,获取已筛选对象。于是我们思考,能否让 JDK自动进行断言?
- 针对第三个标准函数式接口类型的参数 Function mapper ,我们调用它的 apply 方法对已筛选对象进行转化,获得已映射对象。于是我们思考,能否让 JDK 自动进行转化?
- 针对第四个标准函数式接口类型的参数 Consumer block ,我们调用它的 accept 方法对已映射对象执行一个操作,于是我们思考,能否让 JDK自动执行一个操作?
答案是肯定的,这需要用到聚合操作( aggregate operation )。关于聚合操作,以后会详细的讲解。下面的例子,我们先给出一个聚合操作完成我们的场景,并对聚合操作作简单解释,目的是让大家感受一个聚合操作的简洁、高效。
2.9. 方法九:使用聚合操作,并用 Lambda 表达式作为聚合操作的参数下面我将用一句话来表达我们的业务场景,因为聚合操作从头到尾就是一条语句,这是聚合操作的典型特性,就像流水线一般,从头流到尾,不需要中断。这句话是,在应用会员列表中筛选出符合美国义务兵役制度的会员并打印出会员的邮件地址。
public static void processWithAggregate( Collection source, Predicate tester, Function mapper, Consumer block) { source.stream().filter(tester).map(mapper).forEach(block);}
下面简单介绍上述聚合操作中涉及到的四个 API。
default Stream stream() : 使用方法 processWithAggregate的第一个参数 Collection source 作为数据源,返回一个有序流( sequential Stream ),这个有序流的类型是 Stream ,核心作用是能够自动遍历 Collection source 中元素,并对元素(资源对象)执行各种操作。既然如此,拥有了有序流,我们不再需要手工遍历 Collection source了。需要注意的是,该方法 stream 的返回值是一个泛型接口类型 Stream ,尖括号“ ”的类型参数 X 和 Collection 中的 X 保持一致。也就是说,集合中的元素类型和有序流中的元素类型是一致的。这样描述之后,你可能会误会元素存放在 Stream 类型的有序流中,请你务必注意, Stream 类型的有序流不会存放任何元素,它的功能是自动遍历和操作元素,元素是存放在集合中的。
Stream filter(Predicate