Lambda 表达式
想要更好的了解Lambda,请先了解匿名类, 匿名类通常比命名类更简洁,但对于只有一个方法的类,即使是匿名类也似乎有点繁琐,Lambda表达式允许更紧凑地表达单方法类的实例。
我们接下来一步步的来了解Lambda。
首先我们有一个Person类
import java.util.List;
import java.util.ArrayList;
import java.time.chrono.IsoChronology;
import java.time.LocalDate;
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
Person(String nameArg, LocalDate birthdayArg,
Sex genderArg, String emailArg) {
name = nameArg;
birthday = birthdayArg;
gender = genderArg;
emailAddress = emailArg;
}
public int getAge() {
return birthday
.until(IsoChronology.INSTANCE.dateNow())
.getYears();
}
public void printPerson() {
System.out.println(name + ", " + this.getAge());
}
public Sex getGender() {
return gender;
}
public String getName() {
return name;
}
public String getEmailAddress() {
return emailAddress;
}
public LocalDate getBirthday() {
return birthday;
}
public static int compareByAge(Person a, Person b) {
return a.birthday.compareTo(b.birthday);
}
public static List createRoster() {
List roster = new ArrayList<>();
roster.add(
new Person(
"Fred",
IsoChronology.INSTANCE.date(1980, 6, 20),
Person.Sex.MALE,
"[email protected]"));
roster.add(
new Person(
"Jane",
IsoChronology.INSTANCE.date(1990, 7, 15),
Person.Sex.FEMALE, "[email protected]"));
roster.add(
new Person(
"George",
IsoChronology.INSTANCE.date(1991, 8, 13),
Person.Sex.MALE, "[email protected]"));
roster.add(
new Person(
"Bob",
IsoChronology.INSTANCE.date(2000, 9, 12),
Person.Sex.MALE, "[email protected]"));
return roster;
}
}
我们先用这个简单的案例,然后逐步使用lambda表达式来完成示例的学习,该示例代码参考RosterTest
方法1:搜索年龄大于age的人员
public static void printPersonsOlderThan(List roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
方法2:搜索年龄在一个区间范围的人员
public static void printPersonsWithinAgeRange(
List roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}
如果你想要打印指定性别的成员,或指定性别和年龄范围的组合,该怎么办? 如果您决定更改Person类并添加其他属性(如关系状态或地理位置),该怎么办? 显然为每个可能的搜索查询创建单独的方法会导致很多臃肿的代码。 如果你学过设计模式的话,策略模式是比较适合这种情况的。
方法3:使用策略模式
1、UML图
2、接口
interface CheckPerson {
boolean test(Person p);
}
3、策略实现
CheckPersonEligibleForSelectiveService
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
4、caller
public static void printPersons(
List roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
5、测试
printPersons(roster, new CheckPersonEligibleForSelectiveService());
方法4:使用匿名类
方法3,可以依据条件随时定义新的策略,看起来应该都满足开闭原则,也能随时满足不断变化的需求了,但是代码似乎有点臃肿,那么匿名类似乎不用定义一个新的类了。
printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);
是不是简洁了许多
方法5:使用Lambda表达式
接口CheckPerson是一个 functional interface。 functional interface的定义是:接口包含且只包含一个抽象方法(functional interface可以包含一个或多个 default 方法 or static 方法.)
这种类型的接口也称为SAM接口,即Single Abstract Method interfaces。
由于functional interface只包含一个抽象方法,因此在实现该方法时可以省略该方法的名称。 因此, 我们可以使用 lambda expression代替匿名类,如下所示:
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
方法6:使用通用接口并使用Lambda表达式
我们回过头来再看一眼CheckPerson接口
interface CheckPerson {
boolean test(Person p);
}
这是一个非常简单的接口,这个接口仅仅包含了一个抽象方法,该方法接受一个参数,并返回一个布尔值。在应用程序中定义一个这个的接口并不值得。因此JDK在java.util.function包中定义了若干个标准的接口。
我们可以使用java.util.function.Predicate替代CheckPerson,该接口包含一个boolean test(T t)方法
interface Predicate {
boolean test(T t);
}
因此,以下方法调用和方法3调用printPersons是一样的效果
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 roster, Predicate tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
此方法是检查Person
集合中的实例是否满足Predicate
中的条件。如果Person
实例满足tester
指定的条件,则在Person实例上调用printPersron
方法。
我们如果想要再满足tester
条件下,执行不同的操作,而不仅仅是调用printPersron
方法,那么我们可以使用新的lambda表达式。如果要使用lambda表达式,需要创建一个functional interface,该函数接口包含一个Person类型的参数,并返回一个void类型。而jdk java.util.functionConsumer
已经帮我们实现了这样的接口,我们使用便是,Consumerp.printPerson()
替换为Consumeraccept
方法.
public static void processPersons(
List roster,
Predicate tester,
Consumer block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}
因此,我们调用该方法和方法3一样,如下所示:
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);
看起来我们已经很完美的解决了一些问题,那么新的需求又来了,讨厌的产品经理总是不让人省心一点,他不要求打印Person了,要求如果通过验证了,打印Person的其他信息。幸好jdk的java.util.function.Function
帮我们写好了相关的函数接口,Function
包含一个R apply(T t)
方法,该方法指定一个mapper参数,然后返回mapper加工后的数据,然后调用Consumer block消费R值。如下所示:
public static void processPersonsWithFunction(
List roster,
Predicate tester,
Function mapper,
Consumer block) {
for (Person p : roster) {
if (tester.test(p)) {
String data = mapper.apply(p);
block.accept(data);
}
}
}
调用方式如下,我们获取人员的Email联系信息,然后将联系方式打印出来
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
方法8:更广泛地使用泛型
我们再次改造一下processPersonsWithFunction,将参数类型变成更加通用的泛型,可以接受任何类型的参数,而不仅仅是Person。如下所示:
public static void processElements(
Iterable source,
Predicate tester,
Function mapper,
Consumer block) {
for (X p : source) {
if (tester.test(p)) {
Y data = mapper.apply(p);
block.accept(data);
}
}
}
调用方式如下所示:
processElements(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
该方法调用执行如下操作:
1、从集合中获取元素。该事例中表示的是Person对象的List集合。请注意:List类型的集合是Iterable类型的子类。
2、使用Predicate对象过滤Person对象。在此示例中,Predicate对象是一个lambda表达式,匹配相关对象是否符合条件。
3、将每个过滤出来的对象传递赋值到Function mapper对象,在此示例中,Function对象是一个lambda表达式,它返回Person的email属性。
4、对Consumer对象进行消费操作。 在此示例中,Consumer对象是一个lambda表达式,用于输出字符串,该字符串是Function对象返回的email值。
你可以使用聚合操作替换每一个操作。
方法9:使用Lambda表达式作为聚合操作的参数
使用聚合操作来打印集合中符合条件的Person的email地址,如下所示:
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执行的每个操作映射到相应的聚合操作:
processElements 操作 | 聚合操作 |
---|---|
获取集合对象 | Stream |
Predicate 对象过滤器 | Stream |
*Function *将对象转化为其他值 | |
*Consumer *消费对象 | void forEach(Consumer super T> action) |
filter, map,forEach都属于聚合操作,聚合操作处理流中的元素,而不是直接从集合中获取元素。流是元素的一个序列,不同于集合,它不是存储元素的数据结构。流通过管道携带来自源(例如集合)的值。 管道是一系列流操作,在此示例中为filter-map-forEach。 此外,聚合操作通常接受lambda表达式作为参数,你也可以自定义聚合操作。
该章节主要论述lamabda表达式,以后会更加深入的讲解stream的相关知识。
Lambda 表达式在GUI应用中的使用
在 GUI 应用程序中的事件(例如键盘操作,鼠标操作和滚动操作),通常会创建事件处理程序,这通常涉及实现特定接口。 通常,事件处理程序接口是functional interfaces; 他们往往只有一种方法。
在示例HelloWorld.java
中的原来使用的匿名内部类,我们可以使用lamabda替代。
btn.setOnAction(new EventHandler() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
方法btn.setOnAction的参数EventHandler的作用是指定响应方法,该 EventHandler
btn.setOnAction(
event -> System.out.println("Hello World!")
);
Lambda表达式的语法
lambda表达式包含以下内容:
- 括号中用逗号分隔的形式参数列表。 CheckPerson.test方法包含一个参数p,它表示Person类的实例。
Note: 你可以省略lambda表达式中参数的数据类型。 此外,如果只有一个参数,则括号也可以省略。 例如,以下lambda表达式也是有效的:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
- 箭头标记,->
- 一个body,由单个表达式或语句块组成。 此示例使用以下表达式:
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表达式中,必须将语句括在大括号({})中。 但是,没必要在大括号中包含只有一条void方法调用。 例如,以下是有效的lambda表达式:
email -> System.out.println(email)
Note: lambda表达式看起来很像方法声明; 您可以将lambda表达式视为匿名方法 - 没有名称的方法。
下面的示例Calculator.java
是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
局部变量的访问界限
Shadowing
如果特定范围(例如内部类或方法定义)中的类型声明(例如成员变量或参数名称)与封闭范围中的另一个声明具有相同的名称,则声明将隐藏声明封闭范围。 你不能仅通过其名称引用带Shadowing的声明。 以下ShadowTest.java
示例说明了这一点:
public class ShadowTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
System.out.println("x = " + x);
System.out.println("this.x = " + this.x);
System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
}
}
public static void main(String... args) {
ShadowTest st = new ShadowTest();
ShadowTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
输出如下:
x = 23
this.x = 1
ShadowTest.this.x = 0
此示例定义了三个名为x
的变量:类ShadowTest的成员变量,内部类FirstLevel的成员变量,以及methodInFirstLevel方法中的参数。 方法methodInFirstLevel的参数变量x
,隐藏了内部类FirstLevel的变量。 因此,当你在方法methodInFirstLevel中使用变量x
时,它引用的是方法参数。 要引用内部类FirstLevel的成员变量,需要使用关键字this来确认范围:
System.out.println("this.x = " + this.x);
关于更大范围的外部类的成员变量,如果要在方法methodInFirstLevel中访问的话,需要使用如下语句:
System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
和本地和匿名类一样,lambda表达式也可以捕获变量; 它们对封闭范围的局部变量具有相同的访问权限。 但是,与本地和匿名类不同,lambda表达式没有任何Shadowing问题。 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 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
如果在lambda表达式myConsumer的声明中用参数x代替y,会发生什么呢?:
Consumer myConsumer = (x) -> {
// ...
}
编译器会报错:"variable x is already defined in method methodInFirstLevel(int)",因为lambda表达式不会引入新的范围。因此,你可以直接访问封闭范围的字段,方法和局部变量。 例如,lambda表达式直接访问methodInFirstLevel方法的参数x
。如果要访问内部类中的变量,需要使用关键字this
。 在此示例中,this.x
引用成员变量FirstLevel.x
。
与本地和匿名类一样,lambda表达式访问局部变量和参数只能属于final
类型的。 例如,假设在methodInFirstLevel语句之后立即添加以下赋值语句:
void methodInFirstLevel(int x) {
x = 99;
// ...
}
因为x=99
这条赋值语句,变量FirstLevel.x
就不再是effectively final
的了,结果就是,编译器会再如下语句的位置(lambda表达式myConsumer尝试访问FirstLevel.x变量:),抛出error--"local variables referenced from a lambda expression must be final or effectively final".
System.out.println("x = " + x);
目标类型确定
如何确定lambda表达式的类型?我们回顾一下上面得例子:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
这个表达式在下面两个地方用到了:
public static void printPersons(List
#方法3roster, CheckPerson tester) public void printPersonsWithPredicate(List
#方法6roster, Predicate tester)
当虚拟机调用printPersons
方法时,它期望CheckPerson的数据类型,因此lambda表达式属于这种类型。但是,当调用方法printPersonsWithPredicate
时,它期望数据类型为Predicate
- 变量声明
- 赋值声明
- 返回声明
- 数组初始化
- 方法参数或构造函数参数
- Lambda表达体
- 条件表达式,?:
- 转换表达式
目标类型和方法参数
对于方法参数,Java编译器使用两种语言特性确定目标类型:重载解析和类型参数推断。
例如以下两个功能接口(java.lang.Runnable
和java.util.concurrent.Callable
):
public interface Runnable {
void run();
}
public interface Callable {
V call();
}
方法Runnable.run
没有返回值,而Callable
则有返回值。
假设您已按如下方式重载方法调用:
void invoke(Runnable r) {
r.run();
}
T invoke(Callable c) {
return c.call();
}
那么下面得语句将会调用那个方法呢?
String s = invoke(() -> "done");
方法invoke(Callable
将会被调用。在这种情况下,lambda表达式() -> "done"
返回 "done"字符串,所以根据返回值推导,目标类型为Callable
.