本文是对Java SE 8: Lambda Quick Start 的翻译,供个人学习Lambda表达式之用。成文日期:2018-11-01,所有以引文出现的文字为笔者阅读过程中添加,非原文内容。 强烈建议CET4以上的读者阅读原文。
概述 目标 本文旨在介绍在 Java SE 8 中引进的 Lambda 表达式。
阅读时间 大约 1 小时
简介 Lambda 表达式是 Java SE 8 新引入的重要特性。它提供了通过一个表达式表示指代接口的简洁方法。Lambda 表达式也在遍历、过滤、以及读取数据方面优化了Collection
类的使用。此外,新的并发特性提升了多核环境下的程序性能。
总结一下就是三个优点:1.更简洁的匿名类写法,2.更便利的集合类处理,3.多核环境性能优化。
本文首先提供了对 Lambda 表达式的介绍说明,然后介绍匿名内部函数,接着对函数借口和新的 Lambda 语法进行讨论。然后列举出一些常用使用方法模版。
下节通过对一个搜索场景的讨论,引申出 Lambda 表达式如何优化这种功能实现。然后介绍了一些java.util.function
包里面常用的函数接口,如Predicate
和Function
,并举了一些栗子。
文末介绍如何通过 Lambda 优化集合类使用。
提供所有样例源码。
硬件和软件需求 以下是硬件和软件需求列表
可以用IDEA代替NetBeans
(运行样例的)先决条件 要运行样例,你的电脑上必须安装有JDK 8和NetBeans 7.4或更高版本。你可以在the main Lambda site 找到下载链接。或者可以直接使用下面的链接。
注意: 安装包适用于所有主流操作系统。本文基于 Linux Mint 13(Ubuntu/Debian)完成。
安装完成JDK8和NetBeans后。将它们的bin
文件夹添加到你的环境变量PATH
中。
注意: 本文最后更新于 2013 年十二月。
竟然是5年以前的文章了…
背景知识 匿名内部类(Anonymous Inner Class) 在 Java 中,匿名内部类提供一种实现仅使用一次的类的途径。例如,在标准的 Swing 或者 JavaFX 应用中,需要用一系列的 handler 来处理键盘和鼠标事件。除了为每一个事件单独写一个事件处理类(该类需要实现 ActionListener 接口)这种方式,你可以用以下代码来实现类似需求
1 2 3 4 5 6 JButton testButton = new JButton("Test Button" ); testButton.addActionListener(new ActionListener(){ @Override public void actionPerformed (ActionEvent ae) { System.out.println("Click Detected by Anon Class" ); } });
除此之外,你需要为每一个事件都写下类似的代码,它们都需要实现ActionListener
接口。通过上述匿名内部类的写法,代码似乎变得容易阅读一些。然而这种写法很不优雅,因为有太多无用的模版代码,我们需要的其实只是“在函数里我要做什么事”这个信息。
函数接口(Functional Interfaces) 定义ActionListener
的类代码如下
1 2 3 4 5 6 7 8 package java.awt.event;import java.util.EventListener;public interface ActionListener extends EventListener { public void actionPerformed (ActionEvent e) ;}
ActionListener
的梨子是一个只含有一个函数声明的接口。在 Java SE 8 中,这种模式的接口被称为“函数接口”。
注意: 这种接口之前被称为“Single Abstrace Method type”(单一抽象方法类型)
在 Java 中使用匿名类实现函数接口是一种常见写法。除了EventListener
类以外,像Runnable
和Comparator
这样的接口也有一样的模式。因此,函数式接口是我们使用 Lambda 表达式的一个重要原因。
Lambda 表达式语法 Lambda 可以把匿名内部类的代码从 5 行精简到 1 行,极大的缩减了代码的冗余。如下这种“水平的”解决方案解决了匿名内部类带来的“垂直的”问题。
这里“水平”和“垂直”是指代码块在页面中呈现的形式
一个 Lambda 表达式由三部分构成
Argument List
Arrow Token
Body
(int x, int y)
->
x + y
这三个部分可以翻译为“参数列表”、“箭头标志”、“函数体”,不过下文仍然以英文称呼,为了加深记忆。
Body 部分可以是一个简单的表达式(expression)或者代码块(statement block)。在上面的模版中,body 中进行了简单的计算和返回。在代码块格式中,body 呈现形式与普通函数写法一样,同样需要一个 return 语句来返回结果给匿名函数的调用者。break
和continue
关键字在最外层是禁止使用的,不过如果代码里存在循环(loops),则可以使用。如果 body 需要返回一个结果值,每一条控制路径都必需有值返回,或者抛出异常。
看一下这些栗子:
1 2 3 4 5 (int x, int y) -> x + y () -> 42 (String s) -> { System.out.println(s); }
第一个表达式读取两个整型参数,x
和y
,然后返回x+y
的结果。第二个表达式不需要参数,使用“表达式”格式的返回结果,值为42。第三个表达式读取字符串参数,然后将它打印在控制台,不返回任何结果。
有了上面的基础知识,我们再来看一些样例。
Lambda 样例(Lambda Examples) 这里是一些使用到上面提到的栗子的常见用法。
Runnable Lambda 你可以用 Lambda 来写一个实现了 Runnable 接口的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class RunnableTest { public static void main (String[] args) { System.out.println("=== RunnableTest ===" ); Runnable r1 = new Runnable(){ @Override public void run () { System.out.println("Hello world one!" ); } }; Runnable r2 = () -> System.out.println("Hello world two!" ); r1.run(); r2.run(); } }
在两种场景里,请注意到接口不需要读取参数,也没有返回值。Runnable
的 Lambda 表达式使用了代码块模式,将 5 行代码浓缩为 1 行。
“代码块”应为原作者笔误,实际上是“表达式”格式。
Comparator Lambda Comparator
类在 Java 中用于给集合排序。在以下的例子中,一个由Person
对象构成的ArrayList
被按照surName
进行排序。以下是Person
类的成员变量
1 2 3 4 5 6 7 8 public class Person { private String givenName; private String surName; private int age; private Gender gender; private String eMail; private String phone; private String address;
以下代码分别使用了匿名内部类和 Lambda 表达式两种方式生成Comparator
进行排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class ComparatorTest { public static void main (String[] args) { List<Person> personList = Person.createShortList(); Collections.sort(personList, new Comparator<Person>(){ public int compare (Person p1, Person p2) { return p1.getSurName().compareTo(p2.getSurName()); } }); System.out.println("=== Sorted Asc SurName ===" ); for (Person p:personList){ p.printName(); } System.out.println("=== Sorted Asc SurName ===" ); Collections.sort(personList, (Person p1, Person p2) -> p1.getSurName().compareTo(p2.getSurName())); for (Person p:personList){ p.printName(); } System.out.println("=== Sorted Desc SurName ===" ); Collections.sort(personList, (p1, p2) -> p2.getSurName().compareTo(p1.getSurName())); for (Person p:personList){ p.printName(); } } }
由于 Markdown 不支持代码行数表示,这里的行数请自行体会。
17 - 21 行的排序语句可以被 32 行的 Lambda 表达式精简。注意到第一个 Lambda 表达式声明了传进来的参数类型。第二个 Lambda 表达式则省略了类型声明。Lambda 支持“target typing”(类型自动匹配?),意味着它可以自动从上下文中获取到对象类型信息。因为我们将Comparator
应用于泛型集合类(personList),编译器能够自动判断参数类型为Person
。
Listener Lambda 最后我们来看一下ActionListener
的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class ListenerTest { public static void main (String[] args) { JButton testButton = new JButton("Test Button" ); testButton.addActionListener(new ActionListener(){ @Override public void actionPerformed (ActionEvent ae) { System.out.println("Click Detected by Anon Class" ); } }); testButton.addActionListener(e -> System.out.println("Click Detected by Lambda Listner" )); JFrame frame = new JFrame("Listener Test" ); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.add(testButton, BorderLayout.CENTER); frame.pack(); frame.setVisible(true ); } }
注意 Lambda 表达式作为参数被传入。类型自动匹配可以应用在很多场景中,以下举例说明:
Variable declarations(变量声明)
Assignments(赋值)
Return statements(返回语句)
Array initializers(数组初始化)
Method or constructor arguments(方法或构造器参数)
Lambda expression bodies(Lambda 表达式体)
Conditional expressions?:(三元运算符?:)
Cast expressions(类型转换语句)
简单说就是所有为expression
的地方,都可以使用 Lambda 表达式,它本身就是一个 expression/statement。
资源下载 本小节中的 NetBeans 工程样例代码可以在下面下载到
LambdaExamples01.zip
通过 Lambda 表达式优化代码 在你掌握了前面展示的例子后,本节将介绍如何借助 Lambda 改进你的代码。Lambda 表达式将帮助你轻松实现“Don’t Repeat Yourself(DRY)”原则,并且使你的代码更加简洁易于阅读。
一个查询的 Case 一个常见的应用程序场景是,从一个集合中遍历,找出那些符合某种过滤条件的成员。在 2012 年的 JavaOne 大会上,Stuart Marks 和 Mike Duigou 通过格外出色的Jump-Starting Lambda 展示了这种应用场景。给你一个名单列表以及一系列筛选条件,需要找出那些符合条件的人并且自动呼叫他们。这个指南在有一些轻微变更的情况下实现了上述需求。
在这个例子中,我们需要从以下三个美国群体中提取出消息:
司机: 年龄大于 16 岁
入伍人员: 男性,18 ~ 25 岁
飞行员(特指商业飞行员): 23 ~ 65 岁
我们将要实现上述需求,不过不是通过打电话或者发邮件,而是在控制台输出信息。信息内容包括姓名、年龄和联系方式(例如邮件地址或者电话号码)
Person 类 属性定义如下:
1 2 3 4 5 6 7 8 9 10 public class Person { private String givenName; private String surName; private int age; private Gender gender; private String eMail; private String phone; private String address; }
Person
类通过Builder
来创建新对象,使用createShortList
来创建人员列表。如下方代码片段所示。注意: 以下所有样例代码都可以在本节末尾给出的 NetBeans 工程里找到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public static List<Person> createShortList () { List<Person> people = new ArrayList<>(); people.add( new Person.Builder() .givenName("Bob" ) .surName("Baker" ) .age(21 ) .gender(Gender.MALE) .email("bob.baker@example.com" ) .phoneNumber("201-121-4678" ) .address("44 4th St, Smallville, KS 12333" ) .build() ); people.add( new Person.Builder() .givenName("Jane" ) .surName("Doe" ) .age(25 ) .gender(Gender.FEMALE) .email("jane.doe@example.com" ) .phoneNumber("202-123-4678" ) .address("33 3rd St, Smallville, KS 12333" ) .build() ); people.add( new Person.Builder() .givenName("John" ) .surName("Doe" ) .age(25 ) .gender(Gender.MALE) .email("john.doe@example.com" ) .phoneNumber("202-123-4678" ) .address("33 3rd St, Smallville, KS 12333" ) .build() );
第一次尝试 有了上面的Person
类,加上定义好的检索条件,你可以写一个RoboContact
类。一个可以参考的写法中定义了每种使用场景里的方法:
RoboContactMethods.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.example.lambda;import java.util.List;public class RoboContactMethods { public void callDrivers (List<Person> pl) { for (Person p:pl){ if (p.getAge() >= 16 ){ roboCall(p); } } } public void emailDraftees (List<Person> pl) { for (Person p:pl){ if (p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE){ roboEmail(p); } } } public void mailPilots (List<Person> pl) { for (Person p:pl){ if (p.getAge() >= 23 && p.getAge() <= 65 ){ roboMail(p); } } } public void roboCall (Person p) { System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone()); } public void roboEmail (Person p) { System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail()); } public void roboMail (Person p) { System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress()); } }
如上所示,上面列出的方法说明了对不同对象采取的行为(打电话、发电子邮件、发实体邮件)。可以清楚地看出过滤条件和采取的行为。然而,这种设计存在一些缺陷:
没有践行 DRY 原则
每一个方法都是重复的模版
必须为每个方法重写过滤条件
每个用户场景都需要实现大量的方法(比如今天是打电话给入伍人员,发邮件给飞行员;明天可能就是打电话给飞行员,发邮件给入伍人员)
代码呆板。如果过滤条件发生改变,不得不改动大量代码。因此,这段代码是难以维护的。
重构这些方法 如何修复这个类?可以从过滤条件入手。如果可以把过滤调价提取出一个独立的方法,将会有一些提升。
RoboContactMethods2.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package com.example.lambda;import java.util.List;public class RoboContactMethods2 { public void callDrivers (List<Person> pl) { for (Person p:pl){ if (isDriver(p)){ roboCall(p); } } } public void emailDraftees (List<Person> pl) { for (Person p:pl){ if (isDraftee(p)){ roboEmail(p); } } } public void mailPilots (List<Person> pl) { for (Person p:pl){ if (isPilot(p)){ roboMail(p); } } } public boolean isDriver (Person p) { return p.getAge() >= 16 ; } public boolean isDraftee (Person p) { return p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE; } public boolean isPilot (Person p) { return p.getAge() >= 23 && p.getAge() <= 65 ; } public void roboCall (Person p) { System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone()); } public void roboEmail (Person p) { System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail()); } public void roboMail (Person p) { System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress()); } }
比起上一个例子,这次的过滤条件被封装在单独的方法里。这些判断用的代码可以复用,需求变更时也避免了在类里面进行大范围的修改。然而,仍然存在着大量重复代码,以及必需为每种联系方法(打电话、发电子邮件、发邮件)单独写一个方法。是否有更好的将过滤条件传递给这些联系方法的方式?
匿名内部类 在 Lambda 表达式问世之前,可采用匿名内部类的方案。例如,一个解决方法是声明一个具有返回布尔值的test
方法的MyTest.java
类。把过滤条件在这个方法里,如下:
1 2 3 public interface MyTest <T > { public boolean test (T t) ; }
更新后的模拟联系方式类如下:
RobocontactAnon.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class RoboContactAnon { public void phoneContacts (List<Person> pl, MyTest<Person> aTest) { for (Person p:pl){ if (aTest.test(p)){ roboCall(p); } } } public void emailContacts (List<Person> pl, MyTest<Person> aTest) { for (Person p:pl){ if (aTest.test(p)){ roboEmail(p); } } } public void mailContacts (List<Person> pl, MyTest<Person> aTest) { for (Person p:pl){ if (aTest.test(p)){ roboMail(p); } } } public void roboCall (Person p) { System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone()); } public void roboEmail (Person p) { System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail()); } public void roboMail (Person p) { System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress()); } }
这里无疑是有了一些优化,因为只需要 3 个用于进行联系的方法。然而,在实际进行调用时,代码会有一些丑陋。看下面的测试代码:
RoboCallTest03.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package com.example.lambda;import java.util.List;public class RoboCallTest03 { public static void main (String[] args) { List<Person> pl = Person.createShortList(); RoboContactAnon robo = new RoboContactAnon(); System.out.println("\n==== Test 03 ====" ); System.out.println("\n=== Calling all Drivers ===" ); robo.phoneContacts(pl, new MyTest<Person>(){ @Override public boolean test (Person p) { return p.getAge() >=16 ; } } ); System.out.println("\n=== Emailing all Draftees ===" ); robo.emailContacts(pl, new MyTest<Person>(){ @Override public boolean test (Person p) { return p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE; } } ); System.out.println("\n=== Mail all Pilots ===" ); robo.mailContacts(pl, new MyTest<Person>(){ @Override public boolean test (Person p) { return p.getAge() >= 23 && p.getAge() <= 65 ; } } ); } }
这是“垂直”代码的一个典型案例。这样的代码难以阅读。此外,我们必须为每一种使用场景单独写自定义的过滤条件。
Lambda 表达式解千愁 Lambda 表达式可以解决目前为止我们遇到的一切问题。不过,让我们先了解一点别的。
java.uitl.function
在上一个例子中,MyTest
函数接口将匿名内部类传递给方法调用。然而,写一个接口并不是必需的。Java SE 8 提供了java.util.function
包,其中有一系列标准的函数借口。在这种场景下,Predicate
接口符合我们的需求。
Predict
意为“谓语,断言”,即判断条件的意思
1 2 3 public interface Predicate <T > { public boolean test (T t) ; }
test
方法接收一个泛型类,返回一个布尔值。这正是我们所需要的过滤场景。下面是模拟联系方式类的最终版本。
RobocontactsLambda.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package com.example.lambda;import java.util.List;import java.util.function.Predicate;public class RoboContactLambda { public void phoneContacts (List<Person> pl, Predicate<Person> pred) { for (Person p:pl){ if (pred.test(p)){ roboCall(p); } } } public void emailContacts (List<Person> pl, Predicate<Person> pred) { for (Person p:pl){ if (pred.test(p)){ roboEmail(p); } } } public void mailContacts (List<Person> pl, Predicate<Person> pred) { for (Person p:pl){ if (pred.test(p)){ roboMail(p); } } } public void roboCall (Person p) { System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone()); } public void roboEmail (Person p) { System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail()); } public void roboMail (Person p) { System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress()); } }
在这种写法里,只需要三个方法用来模拟联系候选人。Lambda 表达式作为参数被传递给方法,用来过滤需要联系的人员。
垂直问题迎刃而解 Lambda 表达式解决了垂直代码的问题,并且更容易重用。来看看新的使用了 Lambda 表达式的测试类。
RoboCallTest04.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.example.lambda;import java.util.List;import java.util.function.Predicate;public class RoboCallTest04 { public static void main (String[] args) { List<Person> pl = Person.createShortList(); RoboContactLambda robo = new RoboContactLambda(); Predicate<Person> allDrivers = p -> p.getAge() >= 16 ; Predicate<Person> allDraftees = p -> p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE; Predicate<Person> allPilots = p -> p.getAge() >= 23 && p.getAge() <= 65 ; System.out.println("\n==== Test 04 ====" ); System.out.println("\n=== Calling all Drivers ===" ); robo.phoneContacts(pl, allDrivers); System.out.println("\n=== Emailing all Draftees ===" ); robo.emailContacts(pl, allDraftees); System.out.println("\n=== Mail all Pilots ===" ); robo.mailContacts(pl, allPilots); System.out.println("\n=== Mail all Draftees ===" ); robo.mailContacts(pl, allDraftees); System.out.println("\n=== Call all Pilots ===" ); robo.phoneContacts(pl, allPilots); } }
注意到,我们为每组联系人(allDrivers, allDraftees, allPilots)声明了一个断言(Predicate
)。你可以将任何一个断言传给模拟联系的方法。代码简洁易于阅读,并且减少了重复代码。
资源下载 本小节中的 NetBeans 工程样例代码可以在下面下载到
RoboCallExample.zip
java.util.function
包上节介绍的Predicate
并不是 Java SE 8 所提供的唯一函数接口。开发者可以使用一系列的标准接口帮助开发。
像我这么吊的
Predicate
:参数对象的某个属性
Consumer
:对参数对象执行的某种操作
Function
:把 T 转换为 U
Supplieer
:提供一个 T 的实例(例如工厂)
UnaryOperator
:一个单元运算符,T -> T
BinaryOperator
:一个双元运算符,(T, T) -> T
此外,很多接口都有原始形式(类似上文中的Predicate
和人工过滤器这种比较),可以帮助你更好地理解 Lambda 表达式。
东方取名法以及方法调用 在写上面一个例子时,我认为Person
类应当具备一个灵活的输出系统。一个需求是以西方和东方两种命名方式打印出名字。在西方,名在前,姓在后。在东方则恰好相反。
老式写法的例子 这是在未使用 Lambda 表达式时的写法
Person.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void printWesternName () { System.out.println("\nName: " + this .getGivenName() + " " + this .getSurName() + "\n" + "Age: " + this .getAge() + " " + "Gender: " + this .getGender() + "\n" + "EMail: " + this .getEmail() + "\n" + "Phone: " + this .getPhone() + "\n" + "Address: " + this .getAddress()); } public void printEasternName () { System.out.println("\nName: " + this .getSurName() + " " + this .getGivenName() + "\n" + "Age: " + this .getAge() + " " + "Gender: " + this .getGender() + "\n" + "EMail: " + this .getEmail() + "\n" + "Phone: " + this .getPhone() + "\n" + "Address: " + this .getAddress()); }
你需要两个方法,分别打印西方命名、东方命名
Function
接口Function
接口适用于这个问题。它只有一个名为apply
的方法,方法签名如下:
它接收一个泛型 T 的参数,然后返回一个 R 的对象。对于这个例子,传入Person
类型,返回String
类型。一个更加灵活的打印方法如下所示:
Person.java
1 2 3 public String printCuston (Function <Person, String> f) { return f.apply(this ); }
这可真简单。方法接收一个Function
参数,返回一个字符串。apply
方法通过一个 Lambda 表达式返回Person
对象的信息。
Function
是如何定义的?如下是测试的样例代码。
NameTestNew.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public class NameTestNew { public static void main (String[] args) { System.out.println("\n==== NameTestNew02 ===" ); List<Person> list1 = Person.createShortList(); System.out.println("===Custom List===" ); for (Person person:list1){ System.out.println( person.printCustom(p -> "Name: " + p.getGivenName() + " EMail: " + p.getEmail()) ); } Function<Person, String> westernStyle = p -> { return "\nName: " + p.getGivenName() + " " + p.getSurName() + "\n" + "Age: " + p.getAge() + " " + "Gender: " + p.getGender() + "\n" + "EMail: " + p.getEmail() + "\n" + "Phone: " + p.getPhone() + "\n" + "Address: " + p.getAddress(); }; Function<Person, String> easternStyle = p -> "\nName: " + p.getSurName() + " " + p.getGivenName() + "\n" + "Age: " + p.getAge() + " " + "Gender: " + p.getGender() + "\n" + "EMail: " + p.getEmail() + "\n" + "Phone: " + p.getPhone() + "\n" + "Address: " + p.getAddress(); System.out.println("\n===Western List===" ); for (Person person:list1){ System.out.println( person.printCustom(westernStyle) ); } System.out.println("\n===Eastern List===" ); for (Person person:list1){ System.out.println( person.printCustom(easternStyle) ); } } }
第一个循环只是打印出名字和电子邮件地址。不过可以把任何的表达式传给printCuston
方法。东方命名法和西方命名法使用 Lambda 表达式定义,并且保存在变量中。这些变量接下来被传入最后的两个循环。可以很容易地将 Lambda 表达式合并成为Map
,用起来更加方便。Lambda 表达式提供了显著的灵活性。
样例输出 下面是程序的样例输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 ==== NameTestNew02 === ===Custom List=== Name: Bob EMail: bob.baker@example.com Name: Jane EMail: jane.doe@example.com Name: John EMail: john.doe@example.com Name: James EMail: james.johnson@example.com Name: Joe EMail: joebob.bailey@example.com Name: Phil EMail: phil.smith@examp;e.com Name: Betty EMail: betty.jones@example.com ===Western List=== Name: Bob Baker Age: 21 Gender: MALE EMail: bob.baker@example.com Phone: 201-121-4678 Address: 44 4th St, Smallville, KS 12333 Name: Jane Doe Age: 25 Gender: FEMALE EMail: jane.doe@example.com Phone: 202-123-4678 Address: 33 3rd St, Smallville, KS 12333 Name: John Doe Age: 25 Gender: MALE EMail: john.doe@example.com Phone: 202-123-4678 Address: 33 3rd St, Smallville, KS 12333 Name: James Johnson Age: 45 Gender: MALE EMail: james.johnson@example.com Phone: 333-456-1233 Address: 201 2nd St, New York, NY 12111 Name: Joe Bailey Age: 67 Gender: MALE EMail: joebob.bailey@example.com Phone: 112-111-1111 Address: 111 1st St, Town, CA 11111 Name: Phil Smith Age: 55 Gender: MALE EMail: phil.smith@examp;e.com Phone: 222-33-1234 Address: 22 2nd St, New Park, CO 222333 Name: Betty Jones Age: 85 Gender: FEMALE EMail: betty.jones@example.com Phone: 211-33-1234 Address: 22 4th St, New Park, CO 222333 ===Eastern List=== Name: Baker Bob Age: 21 Gender: MALE EMail: bob.baker@example.com Phone: 201-121-4678 Address: 44 4th St, Smallville, KS 12333 Name: Doe Jane Age: 25 Gender: FEMALE EMail: jane.doe@example.com Phone: 202-123-4678 Address: 33 3rd St, Smallville, KS 12333 Name: Doe John Age: 25 Gender: MALE EMail: john.doe@example.com Phone: 202-123-4678 Address: 33 3rd St, Smallville, KS 12333 Name: Johnson James Age: 45 Gender: MALE EMail: james.johnson@example.com Phone: 333-456-1233 Address: 201 2nd St, New York, NY 12111 Name: Bailey Joe Age: 67 Gender: MALE EMail: joebob.bailey@example.com Phone: 112-111-1111 Address: 111 1st St, Town, CA 11111 Name: Smith Phil Age: 55 Gender: MALE EMail: phil.smith@examp;e.com Phone: 222-33-1234 Address: 22 2nd St, New Park, CO 222333 Name: Jones Betty Age: 85 Gender: FEMALE EMail: betty.jones@example.com Phone: 211-33-1234 Address: 22 4th St, New Park, CO 222333
资源下载 本小节中的 NetBeans 工程样例代码可以在下面下载到
LambdaFunctionExamples.zip
Lambda 表达式和集合 上节介绍了Function
接口和示例语法。本节我们将看到 Lambda 表达式如何提升Collections
类。
Lambda 表达式和集合 到目前为止的例子中,集合类出现的很少。然而,相当一部分 Lambda 表达式的特征改变了我们使用集合类的方式。本节介绍其中的一部分。
类增强 司机、飞行员、入伍人员的过滤条件被包装进了SearchCriteria
类。
SearchCriteria.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package com.example.lambda;import java.util.HashMap;import java.util.Map;import java.util.function.Predicate;public class SearchCriteria { private final Map<String, Predicate<Person>> searchMap = new HashMap<>(); private SearchCriteria () { super (); initSearchMap(); } private void initSearchMap () { Predicate<Person> allDrivers = p -> p.getAge() >= 16 ; Predicate<Person> allDraftees = p -> p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE; Predicate<Person> allPilots = p -> p.getAge() >= 23 && p.getAge() <= 65 ; searchMap.put("allDrivers" , allDrivers); searchMap.put("allDraftees" , allDraftees); searchMap.put("allPilots" , allPilots); } public Predicate<Person> getCriteria (String PredicateName) { Predicate<Person> target; target = searchMap.get(PredicateName); if (target == null ) { System.out.println("Search Criteria not found... " ); System.exit(1 ); } return target; } public static SearchCriteria getInstance () { return new SearchCriteria(); } }
本例的编码不是很规范,作为参数的PredicateName
应该写为predicateName
更严谨。
这个类保存了基于Predicate
的搜索过滤条件,你可以在测试用例中使用它们。
循环 首先登场的是任何集合类都可以使用的forEach
方法。这里是一些打印出Person
列表的例子。
Test01ForEach.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Test01ForEach { public static void main (String[] args) { List<Person> pl = Person.createShortList(); System.out.println("\n=== Western Phone List ===" ); pl.forEach( p -> p.printWesternName() ); System.out.println("\n=== Eastern Phone List ===" ); pl.forEach(Person::printEasternName); System.out.println("\n=== Custom Phone List ===" ); pl.forEach(p -> { System.out.println(p.printCustom(r -> "Name: " + r.getGivenName() + " EMail: " + r.getEmail())); }); } }
例 1 展示了一个 Lambda 表达式,它调用printWesternName
方法打印出列表里每个人的名字。例 2 展示了一个 方法调用 。当已经存在对类对象操作的方法时,这种写法可以代替通用 Lambda 表达式的写法。最后,例 3 展示了printCustom
方法也可以在这种场景下使用。注意 Lambda 表达式嵌套时,变量名的细小差别。
你可以用这种方式遍历任何集合。基础结构与增强型for
训话类似。然而,一些增强型机制能带来很多好处。
链式调用与过滤器 除了遍历集合内元素,你还可以将方法组装成链式。第一个要讲解的方法是filter
,它接收一个Predicate
类型的参数。
下面例子在过滤元素后,遍历列表。
Test02Filter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Test02Filter { public static void main (String[] args) { List<Person> pl = Person.createShortList(); SearchCriteria search = SearchCriteria.getInstance(); System.out.println("\n=== Western Pilot Phone List ===" ); pl.stream().filter(search.getCriteria("allPilots" )) .forEach(Person::printWesternName); System.out.println("\n=== Eastern Draftee Phone List ===" ); pl.stream().filter(search.getCriteria("allDraftees" )) .forEach(Person::printEasternName); } }
第一个和最后一个循环展示了List
如何基于过滤条件被过滤。控制台输出如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 === Eastern Draftee Phone List === Name: Baker Bob Age: 21 Gender: MALE EMail: bob.baker@example.com Phone: 201-121-4678 Address: 44 4th St, Smallville, KS 12333 Name: Doe John Age: 25 Gender: MALE EMail: john.doe@example.com Phone: 202-123-4678 Address: 33 3rd St, Smallville, KS 12333
偷点儿懒 这些特征虽然有价值,但是在已经有了一个完美的for
循环语法的情况下,把它们引入集合类的意义何在?通过将集合操作归纳成库,Java 开发者可以在代码上进行更多的优化。首先看一对名词解释。
懒惰 :在编程领域,懒惰意味着只处理你需要处理的对象。在先前的例子中,最后一个循环是“懒惰”的,因为它只遍历了List
被过滤出来的两个Person
对象。这样的代码效率更高,因为最后只处理了两个对象,(而非整个列表)。
主动性 :处理列表中每个元素的代码被称为“主动性强”的。例如,一个增强型for
循环遍历整个列表,只为处理 2 个元素,这种方式被认为“主动性”很强。
stream
方法在先前的代码样例中,你会发现在对列表进行过滤和遍历之前,我们调用了stream
方法。这个方法接收Collection
类型的参数,返回java.util.stream.Stream
类型的结果。一个Stream
对象意味着一个元素序列,你可以在上面进行链式操作。默认地,一旦元素被消费了,他们就不再处于 Stream 中。此外,随着调用方法不同,Stream
可以用在串行(默认)或者并行的场景中。本节末尾将介绍一个并行的场景。
变更与运算结果 如刚才所提到的,Stream
在使用完成后就会被丢弃。因此,我们无法通过Stream
来对列表中的元素进行修改。然而,如果你想要保存链式处理后的元素呢?你可以将它们存入一个新的集合。如下代码所示:
Test03toList.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Test03toList { public static void main (String[] args) { List<Person> pl = Person.createShortList(); SearchCriteria search = SearchCriteria.getInstance(); List<Person> pilotList = pl .stream() .filter(search.getCriteria("allPilots" )) .collect(Collectors.toList()); System.out.println("\n=== Western Pilot Phone List ===" ); pilotList.forEach(Person::printWesternName); } }
collect
方法只有一个参数,是Collector
类。Collector
类用于从处理流的结果中生成一个List
或者Set
。上例介绍了如何将列表过滤后的结果存成List
。
使用map
进行计算 map
方法通常和filter
搭配使用。这个方法去除类里面的某个属性,然后对它进行操作。如下代码基于年龄字段进行了一系列计算。
Test04Map.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class Test04Map { public static void main (String[] args) { List<Person> pl = Person.createShortList(); SearchCriteria search = SearchCriteria.getInstance(); System.out.println("== Calc Old Style ==" ); int sum = 0 ; int count = 0 ; for (Person p:pl){ if (p.getAge() >= 23 && p.getAge() <= 65 ){ sum = sum + p.getAge(); count++; } } long average = sum / count; System.out.println("Total Ages: " + sum); System.out.println("Average Age: " + average); System.out.println("\n== Calc New Style ==" ); long totalAge = pl .stream() .filter(search.getCriteria("allPilots" )) .mapToInt(p -> p.getAge()) .sum(); OptionalDouble averageAge = pl .parallelStream() .filter(search.getCriteria("allPilots" )) .mapToDouble(p -> p.getAge()) .average(); System.out.println("Total Ages: " + totalAge); System.out.println("Average Age: " + averageAge.getAsDouble()); } }
输出如下:
1 2 3 4 5 6 7 == Calc Old Style == Total Ages: 150 Average Age: 37 == Calc New Style == Total Ages: 150 Average Age: 37.5
这段代码计算列表中所有飞行员的平均年龄。第一个循环展示了老式用for
循环的写法。第二个循环使用了map
方法获取串流中的每个人的年龄。注意totalAge
是一个long
类型对象。map
方法返回一个IntStream
对象,可以对其调用sum
方法,这会返回一个long
值。
注意: 第二次计算平均年龄时,再对所有值求和是多余的。然而,上例中这样写是为了展示sum
函数的调用方法。
最后一个循环基于串流计算平均年龄。注意parallelStream
方法,它是 Java 8 中流式计算的并行版本,返回结果也不是简单的double
,而是OptionalDouble
。
parallelStream
提供了一种并发处理集合元素的方法,以上文的列表求平均值为例,如果一共有 100 个元素,parallelStream
会将任务分为 n 份(n 是线程池的大小,默认为 CPU 核数),每份任务交由单独一个线程处理,最终再汇总这些任务的结果,得出平均值
资源下载 本小节中的NetBeans工程样例代码可以在下面下载到
LambdaCollectionExamples.zip
总结 在本篇指南中,你学会了使用以下技能:
Java 匿名内部类
使用 Java SE 8 的 Lambda 表达式代替匿名内部类
Lambda 表达式的语法
通过Predicate
接口在列表中查找
通过Function
接口处理对象,并得到一个新类型的对象
Java SE 8 中新增的支持 Lambda 表达式的集合特性
更多资源 如果你想要了解更多关于 Lambda 表达式和 Java SE 8 的信息,参考以下链接:
著者
课程组长:Michael Williams
QA:Juan Quesada Nunez
翻译:李磊