介绍关于 Lambda 表达式产生的背景及典型使用场景、解决的问题。
简单匿名类与 Lambda 表达式将功能代码像方法参数那样传来传去9 种 Lambda 典型使用场景在 GUI 编程中 Lambda 表达式的妙用访问闭包作用域的局部变量
通过结合具体应用场景,进一步加深 Lambda 的理解。
简单匿名类与 Lambda 表达式缘起匿名类
在很多场景下,使用匿名类可以使我们的代码看起来更加精简。它可以实现在声明类的同时实例化类的对象,假如你只需要使用一次某个类,这时你可以考虑运用匿名类来实现,相较于内部类而言,匿名类可以省略类名称。
匿名类的声明本质上是一个表达式语句,由以下几部分组成:
new 需要实现的接口或继承的基类名称(){
类定义语句块
};
例如:
HelloWorld frenchGreeting = new HelloWorld() {
String name = "tout le monde";
public void greet() {
greetSomeone("tout le monde");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Salut " + name);
}
};
我们在表达式中定义了一个实现了 HelloWorld 接口的匿名类,它还有一个属性和两个方法。
假如我们采用内部类来实现的话,我们首先要定义一个实现了接口 HelloWorld 的署名类,然后再 new 一个该署名类的对象,如下:
class EnglishGreeting implements HelloWorld {
String name = "world";
public void greet() {
greetSomeone("world");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Hello " + name);
}
}
HelloWorld englishGreeting = new EnglishGreeting();
我们可以看出,匿名类是对内部类的一种精简,它在定义类的同时直接 new 了一个该类的对象。
Lambda 表达式是对匿名类的精简
假设我们有这样一个需求:根据特定的查询标准返回人员列表中的数据。
public static void printPersons(
List roster, CheckPerson tester) ;
CheckPerson 是一个接口,如下:
interface CheckPerson {
boolean test(Person p);
}
该接口定义了一个 test 方法,如果返回 true 则表示当前对象符合筛选条件,否则不符合。
在实现时我们传入不同的匿名类来实现该接口,例如:
printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);
这里采用匿名类就避免了创建一个署名实现类,然后再 new 一个该实现类的对象,再将对象传入 printPersons 作为第二个参数的麻烦,我们在匿名类中一次性完成了所有这些操作。
经过仔细观察我们发现 CheckPerson 接口只有一个接口方法(这样的接口称为函数接口)。函数接口只含有一个抽象方法,可以进一步精简成 Lambda 表达式。
将功能代码像方法参数那样传来传去
上一节的 CheckPerson 接口中的抽象方法传入一个参数同时返回一个布尔值,我们完全可以使用 JDK 中为我们提供的标准函数接口而无需自己定义,这些标准函数接口位于 java.util.function 包下面。
我们可以在使用 CheckPerson 的地方直接使用 JDK 为我们提供的 Predicate 接口,该接口含有一个接口方法 boolean test(T t):
interface Predicate {
boolean test(T t);
}
该接口是一个泛型接口,其中 T 是类型参数,我们在使用的时候具体指定实际的类型,例如:
interface Predicate {
boolean test(Person t);
}
该接口含有和 CheckPerson 中的 test 方法完全相同的返回值类型。 我们可以在 使用 CheckPerson 的地方直接使用 Predicate 来代替。
public static void printPersonsWithPredicate(
List roster, Predicate tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
该方法定义了 2 个参数,第一个参数 roster 代表要处理的人员列表数据,第二个参数 tester 是一个函数接口,它包含了一个 test 方法用来在方法体中判断人员是否符合筛选条件。具体调用该方法的时候,我们只需要传入一个实现了该函数接口的 Lambda 表达式即可。
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
上述 Lambda 表达式筛选出了性别为男,岁数大于 18 小于 25 的人。 我们可以发现,Lambda 表达式本质上是对函数接口的一种实现,是对匿名类的简化,它可以非常方便的实现在一个方法的参数中传递一段功能代码。
Lambda 典型使用场景
Lambda 表达式还有更多的使用场景。
在上面的例子中,我们使用 Lambda 表达式指定了人员信息的筛选条件,其实我们深圳可以使用 Lambda 表达式指定一种具体的操作,因为 Lambda 表达式本质上是对一段功能代码的精简封装。我们可以定义一个函数接口,该函数接口含有一个方法 void doSomething(Person p); 然后我们使用 Lambda 表达式来实现该函数接口。
不过 JDK 中已经提供这样的函数接口,我们无需重复定义,可以直接拿来使用就可以了。在 JDK 中 Consumer 接口含有一个接口方法void accept(T t)闭包的使用场景,我们可以在该接口方法的实现中来完成具体的处理逻辑。
我们重构一下之前的方法,
public static void processPersons(
List roster,
Predicate tester,
Consumer block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}
我们除了定义了筛选条件,还定义了对筛选出来的数据进行处理的函数接口,并通过调用 accept 方法将筛选出来的数据传入进行处理。
这样,我们在具体调用该方法时,只需要传入表达筛选逻辑的 Lambda 表达式和表达处理逻辑的 Lambda 表达式即可。 假设我们的处理逻辑仅仅是打印人员信息。
那么我可以这样来调用:
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() p.printPerson()
);
其中 p -> p.printPerson() 就是表达处理逻辑的 Lambda 表达式。
不过真实使用场景中很少是只打印信息那么简单的,我们很可能要做更复杂的处理逻辑,例如我们需要返回每个人的联系方式等等,在这样的情况下,我们就需要我们的函数接口中的抽象方法具有返回值。同样 jdk 也为我们定义好了这样的函数接口 Function,该接口有一个接口方法 R apply(T t),该方法接收一个处理数据对象,并返回一个泛型处理结果。
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);
}
}
}
在上述定义中,我们定义了 mapper 接口,它的接口方法接受一个 Person 类型的参数并返回一个 String 类型的处理结果。
我们看到在方法体中,当筛选出了符合条件的人员数据以后,我们调用 mapper.apply(p); 来完成了处理,并将处理的结果保存到了 data 中。至于处理的具体逻辑是什么样的,取决于我们在调用该方法的时候传入的 Function
接口的具体实现,而这种函数接口的具体实现我们完全可以通过 Lambda 表达式来描述。
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() p.getEmailAddress(),
email -> System.out.println(email)
);
上面的例子中,我们通过 Lambda 表达式描述了我们的处理逻辑是获取每个符合筛选条件的人员的 email 地址。
我们通过上面的例子可以发现,在我们的编程实践中闭包的使用场景,我们都是针对接口和抽象来编程的,而具体的实现逻辑都是留到调用该方法的时候来描述的,而针对只含有一个抽象方法的函数接口,我们完全可以通过在方法调用中传入 Lambda 表达式的方式来描述我们具体的处理逻辑,这不仅仅在代码上实现了极大的精简,而且让我们的整个逻辑更加清晰了。
为了让这个处理更加通用,我们将参数类型使用泛型来代替,如下:
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() p.getEmailAddress(),
email -> System.out.println(email)
);
调用时我们首先从 roster 集合中获得 Person 的对象,这里需要注意的是 roster 集合好似 List 类型,它也实现了 Iterable 接口。接着采用 Lambda 表达式来过滤,然后才有 Lambda 表达式来对过滤后的对象进行处理,返回每个人的 email 地址。最后采用 Lambda 表达式打印出返回的 email 地址。
在 GUI 编程中 Lambda 表达式的妙用
在图形界面编程(GUI 编程)中,我们经常会遇到处理事件,例如处理键盘按键事件、鼠标事件,这个时候通常会要求我们创建事件处理函数,而且这种事件处理函数一般是要实现某个特定的接口的。通常事件处理接口都是函数接口,只有一个抽象方法。
例如在按钮选择事件的处理中,我们有如下的代码:
btn.setOnAction(new EventHandler() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
它指定了当选中按钮 btn 时要做什么事情,setOnAction 方法需要接收一个实现了 EventHandler() 接口的对象,而该接口只含有一个方法 void handle(T event)。所以除了采用上面的那种匿名类的方式来实现外,我们完全可以使用 Lambda 表达式来简化代码,其效果是一样的。 如下:
btn.setOnAction(
event -> System.out.println("Hello World!")
);
在 GUI 图形应用程序编程中,采用 Lambda 表达式来简化事件处理是非常常见的。
访问闭包作用域的局部变量
类似于匿名类,Lambda 表达式也可以捕获变量,Lambda 表达式可以访问所在外层作用域定义的变量,包括成员变量和局部变量。
访问成员变量
成员变量包括实例成员变量和静态成员变量。在 Lambda 表达式中可以访问这些成员变量,此时的 Lambda 表达式与普通方法一样,可以读取成员变量,也可以修改成员变量。
import java.util.function.Consumer;
public class LambdaScopeTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
int z = 2;
Consumer myConsumer = (y) ->
{
// The following statement causes the compiler to generate
// the error "Local variable z defined in an enclosing scope
// must be final or effectively final"
//
// z = 99;
System.out.println("x = " + x);
System.out.println("y = " + y);
System.out.println("z = " + z);
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
z = 2
this.x = 1
LambdaScopeTest.this.x = 0
综上所述,Lambda 表达式是函数接口实现类的精简写法,在很多场合下它可以很方便的代替匿名类,特别是当我们只需要一次性使用某些功能逻辑时,采用 Lambda 表达式是再合适不过了。可以大大减少我们的代码书写量。
限时特惠:本站每日持续更新海量设计资源,一年会员只需29.9元,全站资源免费下载
站长微信:ziyuanshu688