Java8做出了比较大的更新,支持了很多新特性,Java8引入的Lambda表达式这一特性,使Java8支持函数式编程,目前网上有较多的教程,本文整理了来自Oralce官方提供的教程。
Lambda表达式
匿名类的一个问题是,如果你的匿名类的实现很简单,比如仅包含一个方法的接口,然后匿名类的语法看似笨拙,目前还不清楚。在这种情况下,你通常试图通过功能作为参数传递给其他方法,比如什么,当有人点击一个按钮,应采取的行动。Lambda表达式使您能够做到这一点,治疗功能的方法参数,或代码的数据。
上一节, 匿名类,展示了如何实现一个基类没有给它一个名称。虽然这往往比命名的类更加简洁,为教学班,只有一个方法,即使是匿名类似乎有点过度和繁琐。Lambda表达式让你表达单一方法类的实例更紧凑。
本节包括以下主题:
-
理想的使用案例Lambda表达式
- 方法1:创建的方法搜索会员那场比赛的一个特点
- 方法2:创建更广义的搜索方法
- 方法3:在局部类指定搜索条件码
- 方法4:在匿名类指定搜索条件码
- 方法5:指定一个Lambda表达式搜索条件码
- 方法6:Lambda表达式使用标准的功能接口
- 方法7:使用Lambda表达式在整个应用程序
- 方法8:使用泛型更广泛
- 方法9:使用聚合操作接受Lambda表达式作为参数
- Lambda表达式中的GUI应用程序
- Lambda表达式语法
- 访问封闭范围内的局部变量
-
目标打字
- 目标类型和方法的参数
- 序列化
理想的使用案例Lambda表达式
假设你正在创建一个社交网络应用。你想创建一个功能,使管理员能够执行任何的动作,如发送消息,在满足一定条件的社交网络应用程序的成员。下表详细描述了这个用例:
领域 |
描述 |
名称 |
对选定的成员执行操作 |
主要演员 |
管理员 |
前提条件 |
管理员登录到系统中。 |
后置条件 |
行动仅在适合指定标准执行委员。 |
主要成功案例 |
|
扩展 |
1A。管理员有一个选项来预览那些谁符合指定条件的成员,他或她指定要执行的操作或选择前前提交按钮。 |
出现频率 |
白天多次。 |
假设这家社交网络应用程序的成员由以下代表 Person
类:
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
假设你的社交网络应用程序的成员都存储在一个 List<Person>实例。
本节一开始就天真的做法来这个用例。它改进了这一方法与本地和匿名类,然后用lambda表达式的高效和简洁的方式完成。查找的例子本节所述的代码摘录RosterTest
。
方法1:创建的方法搜索会员那场比赛的一个特点
一个简单的方法是创建几种方法; 每种方法搜索匹配一个特性,即成员,如性别或年龄。下面的方法打印成员是超过指定年龄大:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
注:一个 列表
是一个有序 集合
。一个集合是一个对象,该组中的多个元素成一个单元。集合用于存储,检索,操作和交流汇总数据。有关集合的详细信息,请参阅 Collections踪迹。
这种方法有可能使应用程序脆,这是一个应用程序不因为引入更新(如新的数据类型)的工作的可能性。假设你升级你的应用程序,并改变结构的人
类,使得它包含不同的成员变量; 也许是记录类和年龄的措施,不同的数据类型或算法。你将不得不重新编写了很多你的API,以适应这种变化。此外,这种方法是不必要的限制; 如果你想打印的成员超过一定年龄年轻,例如?
方法2:创建更广义的搜索方法
以下方法比更通用printPersonsOlderThan
; 它打印年龄的一个指定范围内的成员:
public static void printPersonsWithinAgeRange(
List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}
如果你想打印指定的性别,或指定性别和年龄段的组合的成员呢?如果你决定要改变什么的人
类,并添加其他属性,如关系状态或地理位置?虽然这种方法比更通用printPersonsOlderThan
,试图为每个可能搜索查询的独立的方法仍然可以导致脆代码。您可以取代单独指定您要在不同的类搜索标准的代码。
方法3:在局部类指定搜索条件码
符合搜索条件在指定的以下方法打印成员:
public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
此方法检查每个人
包含在实例列表
参数名册
它是否满足在指定的搜索条件CheckPerson
参数测试仪
通过调用方法tester.test
。如果方法tester.test
返回真
值,则该方法printPersons
被调用的人员
实例。
要指定搜索条件,你实现了 CheckPerson
接口:
interface CheckPerson {
boolean test(Person p);
}
以下类实现CheckPerson
通过指定的方法的实现的接口测试
。这种方法筛选成员有资格对选择性服务在美国:它返回一个真正的
,如果它的价值的人
的参数是男性,18至25岁之间:
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
要使用这个类,创建它的一个新的实例并调用printPersons
方法:
printPersons(
roster, new CheckPersonEligibleForSelectiveService());
虽然这种方法不太脆,你不必重写方法,如果你改变了结构的人
-你还有额外的代码:一个新的接口,并为每个局部类搜索您计划在应用程序执行。由于CheckPersonEligibleForSelectiveService
实现了一个接口,可以使用匿名类,而不是本地类和绕过需要声明一个新的类为每个搜索。
方法4:在匿名类指定搜索条件码
其中一个方法的以下调用的参数printPersons
是匿名类过滤成员有资格对选择性服务在美国:那些谁是男性,18至25岁之间:
printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);
这种方法减少的代码,因为你没有创建一个新的类要执行每个搜索所需的量。然而,匿名类的语法是笨重考虑到CheckPerson
接口只包含一个方法。在这种情况下,你可以使用lambda表达式,而不是一个匿名类,如下一节所述。
方法5:指定一个Lambda表达式搜索条件码
该CheckPerson
接口是一个功能接口。功能接口是只包含一个任何接口 抽象方法。(功能接口可以包含一个或多个 默认的方法或 静态方法。)因为功能接口只包含一个抽象方法,当你实现它,你可以省略方法的名称。要做到这一点,而不是使用匿名类表达式,您可以使用lambda表达式,这在下面的方法调用强调:
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
参见Lambda表达式的语法有关如何定义lambda表达式的信息。
可以代替接口的使用标准功能接口CheckPerson
,甚至进一步降低了的代码所需的时间。
方法6:Lambda表达式使用标准的功能接口
重新考虑CheckPerson
接口:
interface CheckPerson {
boolean test(Person p);
}
这是一个非常简单的接口。这是一个功能性接口,因为它仅包含一个抽象方法。这种方法需要一个参数,并返回一个 布尔
值。该方法非常简单,它可能不值得来定义一个在你的应用程序。因此,JDK定义了几个标准的功能接口,您可以在软件包中找到java.util.function
。
例如,您可以使用 Predicate<T> 接口代替CheckPerson
。该接口包含方法boolean test(T t) :
interface Predicate<T> {
boolean test(T t);
}
接口谓词
<T>
是一个通用的接口的一个例子。(有关泛型的更多信息,请参见 泛型(更新)的教训。)泛型类型(如通用接口)指定尖括号中的一个或多个类型参数(<>
)。这个接口只包含一个类型参数,牛逼
。当你声明或者实例化一个泛型类型与实际类型参数,你有一个参数化类型。例如,所述参数化的类型 Predicate<Person> 如 下:
interface Predicate<Person> {
boolean test(Person t);
}
此参数化类型包含具有相同的返回类型和参数的方法CheckPerson.boolean test(Person p)。因此,您可以使用Predicate<T>代替CheckPerson
如下面的方法演示:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
这样一来,下面的方法调用是一样的,当你调用 printPersons
在方法3:在局部类指定搜索条件的代码,以获得成员谁都有资格选择服务:
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
这不是在该方法中使用lambda表达式的唯一可能的地方。下面的方法建议其他方式使用lambda表达式。
方法7:使用Lambda表达式在整个应用程序
重新考虑方法printPersonsWithPredicate
看看还有什么地方可以使用lambda表达式:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
此方法检查每个人
包含在实例列表
参数名册
是否满足指定条件谓词
参数测试仪
。如果人
的实例确实满足规定的标准测试
,该方法printPersron
被调用的人员
实例。
而不是调用方法printPerson
,您可以指定不同的动作对那些执行的人
满足由指定的标准实例测试仪
。您可以指定一个lambda表达式这个动作。假设你想类似于lambda表达式printPerson
,即需要一个参数(类型的对象的人
),并返回void。请记住,使用lambda表达式,你需要实现的功能接口。在这种情况下,你需要包含可以利用类型的一个参数抽象方法的功能接口人士
并返回void。该 消费者
<T>
接口包含的方法 无效接收(
T T
)
,它具有这些特点。下面的方法替换调用 p.printPerson
()
与实例消费者
<
人
>
调用该方法接受
:
public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Consumer<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}
这样一来,下面的方法调用是一样的,当你调用printPersons
在方法3:在局部类指定搜索条件的代码,以获得成员谁都有资格选择服务。用于打印成员lambda表达式高亮显示:
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);
如果你想要做更多的与您的会员的个人资料比打印出来。假设你要验证的会员资料或检索自己的联系方式?在这种情况下,你需要包含返回值的抽象方法的功能接口。该功能
<T
,
R>
接口包含的方法r
使用(
T T
)
。下面的方法获取由参数指定的数据映射
,然后执行由参数指定它的动作块
:
public static void processPersonsWithFunction(
List<Person> roster,
Predicate<Person> tester,
Function<Person, String> mapper,
Consumer<String> block) {
for (Person p : roster) {
if (tester.test(p)) {
String data = mapper.apply(p);
block.accept(data);
}
}
}
下面的方法检索包含在每个成员的电子邮件地址名册
谁可享有选择服务,然后打印它:
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
方法8:使用泛型更广泛
重新考虑方法processPersonsWithFunction
。下面是它的一个通用版本接受,作为一个参数,它包含任何数据类型的元素的集合:
public static <X, Y> void processElements(
Iterable<X> source,
Predicate<X> tester,
Function <X, Y> mapper,
Consumer<Y> block) {
for (X p : source) {
if (tester.test(p)) {
Y data = mapper.apply(p);
block.accept(data);
}
}
}
要打印成员谁都有资格选择服务的电子邮件地址,调用processElements
方法如下:
processElements(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
此方法调用执行以下操作:
1. 获取从集合对象的来源源
。在这个例子中,它获得的来源的人
的对象从集合名册
。注意收集名单
,这是类型的集合名单
,也是类型的对象可迭代
。
2. 匹配的滤镜对象谓词
对象测试仪
。在这个例子中,谓词
对象是指定哪些成员将有资格获得选择性服务lambda表达式。
3. 由指定映射每个过滤对象的值函数
对象映射器
。在这个例子中,函数
对象是一个lambda表达式返回成员的电子邮件地址。
4. 由指定执行每个映射对象的动作,消费
对象块
。在这个例子中,消费
对象是一个lambda表达式,打印字符串,这是由返回的电子邮件地址功能
对象。
你可以用一个聚合操作替换这些行动。
方法9:使用聚合操作接受Lambda表达式作为参数
下面的示例使用聚合操作来打印包含在集合成员的电子邮件地址名单
谁都有资格选择服务:
roster
.stream()
.filter(
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25)
.map(p -> p.getEmailAddress())
.forEach(email -> System.out.println(email));
下表映射每一个方法的操作的processElements
与相应的骨料操作执行:
|
聚合操作 |
获得的对象的源 |
Stream<E> stream() |
匹配的筛选对象 |
Stream<T> filter(Predicate<? super T> predicate) |
由指定的对象映射到另一个值 |
<R> Stream<R> map(Function<? super T,? extends R> mapper) |
由指定的执行动作 |
void forEach(Consumer<? super T> action) |
操作过滤器
,地图
,和的
forEach
是聚合操作。聚合操作从流处理元件,而不是直接从集合(这就是为什么在本实施例调用的第一个方法是流
)。一个流是元素的序列。不像的集合,它不是一个数据结构,用于存储元件。相反,流承载从源值,例如收集,通过管道。一个管道是流的操作的序列,其在本实施例是滤波器
- 地图
- 的
forEach
。此外,聚合操作通常接受lambda表达式作为参数,使您可以自定义他们的言行举止。
对于聚合操作更透彻的讨论,请参阅 聚合操作课。
Lambda表达式中的GUI应用程序
要处理的图形用户界面(GUI)应用程序事件,如键盘操作,鼠标操作和滚动操作,则通常会创建事件处理程序,它通常包括实现一个特定的接口。通常情况下,事件处理程序接口的功能接口; 他们往往只有一个方法。
在JavaFX的例子HelloWorld.java
(前一节中讨论的 匿名类),你可以在此声明lambda表达式替换突出匿名类:
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
该方法调用btn.setOnAction
指定当您选择了代表的按钮时会发生什么BTN
对象。这种方法需要类型的对象事件处理
<
的
ActionEvent>
。该事件处理程序
<ActionEvent
的
>
接口只包含一个方法,无效手柄(
T
事件)
。这个接口是一个功能接口,所以你可以使用下面的高亮lambda表达式替换它:
btn.setOnAction(
event -> System.out.println("Hello World!")
);
Lambda表达式语法
lambda表达式包括以下内容:
· 以逗号分隔的括号括起来的形式参数列表。该CheckPerson.test
方法包含一个参数, p
,其表示的一个实例 的人
类。
注意:您可以省略参数的数据类型的lambda表达式。此外,你可以省略括号如果只有一个参数。例如,下面的λ表达式也是有效的:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
· 箭头标记,-
>
· 的主体,它由一个表达式或语句块。本例使用下面的表达式:
· p.getGender() == Person.Sex.MALE
· && p.getAge() >= 18
· && p.getAge() <= 25
如果指定一个表达式,那么Java运行时计算表达式的值,然后返回它的值。另外,也可以使用return语句:
p -> {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
return语句不是表达式; 在lambda表达式,则必须用大括号中的语句({}
)。但是,您不必括在大括号的空白方法调用。例如,下面是一个有效的lambda表达式:
email -> System.out.println(email)
需要注意的是lambda表达式看起来很像一个方法声明; 你可以考虑lambda表达式是匿名方法的方法没有名字。
下面的例子中, 计算器
,是采取多个形式参数lambda表达式的例子:
public class Calculator {
interface IntegerMath {
int operation(int a, int b);
}
public int operateBinary(int a, int b, IntegerMath op) {
return op.operation(a, b);
}
public static void main(String... args) {
Calculator myApp = new Calculator();
IntegerMath addition = (a, b) -> a + b;
IntegerMath subtraction = (a, b) -> a - b;
System.out.println("40 + 2 = " +
myApp.operateBinary(40, 2, addition));
System.out.println("20 - 10 = " +
myApp.operateBinary(20, 10, subtraction));
}
}
该方法operateBinary
对两个整数操作数的数学运算。该操作本身是由实例指定IntegerMath
。这个例子定义了lambda表达式,两个操作加法
和减法
。这个例子打印出以下几点:
40 + 2 = 42
20 - 10 = 10
访问封闭范围内的局部变量
像本地和匿名类,lambda表达式可以 捕捉变量 ; 他们必须封闭范围的局部变量相同的访问。然而,与本地和匿名类,Lambda表达式没有任何遮蔽的问题(见 遮蔽了解更多信息)。Lambda表达式的词法范围。这意味着,他们不从父继承任何名义或引进的范围界定一个新的水平。因为它们是在封闭环境中的lambda表达式声明只是解释。下面的例子, LambdaScopeTest
,说明了这一点:
import java.util.function.Consumer;
public class LambdaScopeTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
// The following statement causes the compiler to generate
// the error "local variables referenced from a lambda expression
// must be final or effectively final" in statement A:
//
// x = 99;
Consumer<Integer> myConsumer = (y) ->
{
System.out.println("x = " + x); // Statement A
System.out.println("y = " + y);
System.out.println("this.x = " + this.x);
System.out.println("LambdaScopeTest.this.x = " +
LambdaScopeTest.this.x);
};
myConsumer.accept(x);
}
}
public static void main(String... args) {
LambdaScopeTest st = new LambdaScopeTest();
LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
这个例子生成以下的输出:
x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0
如果替换为参数点
¯x
代替Ÿ
在lambda表达式的声明myConsumer
,那么编译器会生成一个错误:
Consumer<Integer> myConsumer = (x) -> {
// ...
}
编译器生成“变量x的方法methodInFirstLevel(INT)已经定义”,因为lambda表达式不引入作用域的一个新的水平误差。因此,您可以直接访问字段,方法,和封闭范围的局部变量。例如,lambda表达式直接访问参数点
¯x
的方法的methodInFirstLevel
。要访问封闭类变量,使用关键字此
。在这个例子中,this.x
指成员变量FirstLevel.x
。
然而,像本地和匿名类,Lambda表达式只能访问局部变量和封闭块是最终或有效决赛参数。例如,假设您在后立即添加下面的赋值语句methodInFirstLevel
定义语句:
void methodInFirstLevel(int x) {
x = 99;
// ...
}
因为这个赋值语句,变量FirstLevel.x
得不到有效决赛了。其结果是,Java编译器产生类似的错误消息“从lambda表达式中引用的局部变量必须是最终或有效决赛”里的lambda表达式myConsumer
试图访问FirstLevel.x
变量:
System.out.println("x = " + x);
目标打字
你如何确定一个lambda表达式的类型?回想一下,选定的成员谁是男性,年龄18至25岁之间的lambda表达式:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
在以下两种方法使用该lambda表达式:
· public static void printPersons(List<Person> roster, CheckPerson tester)
· 在方法3:指定搜索条件的代码在本地类
· public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
· 在方法6:使用标准功能与Lambda表达式接口
当Java运行时调用方法printPersons
,它的预期的数据类型CheckPerson
,所以lambda表达式就是这种类型的。然而,当Java运行时调用方法printPersonsWithPredicate
,它的预期的数据类型谓词
<
人
>
,所以lambda表达式就是这种类型的。的数据类型,这些方法需要被称为目标类型。要确定一个lambda表达式的类型,Java编译器使用了其中的lambda表达式被发现的情况下或情况的目标类型。由此可见,你可以仅在Java编译器可以判断目标类型的情况下使用lambda表达式:
· 变量声明
· 分配
· return语句
· 初始化数组
· 方法或构造函数的参数
· Lambda表达式机构
· 条件表达式,?
· 铸造表达式
目标类型和方法的参数
对于方法参数,Java编译器决定了与其他两种语言特性的目标类型:重载解析和类型参数推断。
考虑以下两个功能界面( 了
java.lang.Runnable
和 java.util.concurrent.Callable <V>
):
public interface Runnable {
void run();
}
public interface Callable<V> {
V call();
}
该方法Runnable.run
不返回值,而可赎回
<V> .CALL
一样。
假设你重载方法调用
如下(见 定义方法有关重载方法的详细信息):
void invoke(Runnable r) {
r.run();
}
<T> T invoke(Callable<T> c) {
return c.call();
}
哪种方法将在下面的语句来调用?
String s = invoke(() -> "done");
该方法调用
(Callable<T>)将被调用因为该方法返回一个值; 该方法 调用(
Runnable
接口)
没有。在这种情况下,lambda表达式类型()
- >“
完成
”
是 Callable<T>. 。
序列化
您可 序列化的lambda表达式,如果它的目标类型和拍摄的参数是序列化的。然而,像 内部类,lambda表达式的序列化的强烈反对。