Java8java进阶干货Java学习笔记

java8官方文档—Lambda表达式

2017-07-21  本文已影响162人  猴子小皮球

声明:本文翻译自The Java™ Tutorials(官方文档)

简述

匿名类有一个问题,如果匿名类的实现非常简单,比如一个只包含一个方法的接口,那么这样的匿名类的语法是笨拙和不清晰的。在这些情况下,你通常会尝试将功能作为参数传递给另一个方法,比如某人点击按钮时,它将发生的具体行为。lambda表达式允许你这样做,将功能作为方法的参数,或者把代码当作数据。

上一节,Anonymous Classes ,为你展示了如何实现一个匿名的基础类。虽然它通常比有名称的类更加简洁,对于只有一个方法的类,甚至是匿名类,依然显得有点过于笨拙。lambda表达式可以让你更加简洁的表达这种只有单一方法类的实例。

Lambda表达式使用示例

假如你正在开发一个社交应用。你想开发一个功能,使管理员可以执行任何类型的动作,如发送消息给某些满足一定条件的社交应用成员。下面的表格描述了这个应用的细节:

字段 描述
名称 对选定的成员执行操作
主要角色 管理员
前置条件 管理员已经登录系统
后置条件 只对符合条件的成员进行操作
主要场景 1.管理员指定执行某个操作的成员的标准
2.管理指定对选定成员执行的操作
3.管理员选择提交按钮
4.系统查找所有符合条件的成员
5.系统对符合条件的成员执行指定的操作
扩展 管理员可以选择在指定要执行的操作之前或在选择提交按钮之前,对符合指定条件的成员进行预览
发生频率 一天多次

假如社交应用的成员通过下面的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表达式以一种简明的方式来完成。本节中的代码摘录自例子RosterTestPerson

方式1:创建搜索匹配一个特征的成员的方法

以一种简单的方式创建几个方法,每个方法查找匹配某一特征的成员,比如性别或者年龄。下面的方法打印出大于指定年龄的成员:

public static void printPersonsOlderThan(List<Person> roster, int age) {
    for (Person p : roster) {
        if (p.getAge() >= age) {
            p.printPerson();
        }
    }
}

注意List是一个有序的集合。集合是将多个元素组合成一个单元的对象。集合用于存储、检索、操作和传递聚集数据。更多关于集合的信息,查看Collections路径。

这种方式可能使得你的应用很脆弱,当你对类进行更新时(比如一个新的数据类型)可能导致应用无法工作。假设你更新应用,改变Person类的结构,可能是增加一个不同的成员变量,也可能是用一个新的数据类型或算法来记录年龄信息。为了适应这些变化,你必须要重写大量的API。另外,如果你想打印一个小于指定年龄的成员,这个类也将给你不必要的限制。这该如何解决呢?

方式2:创建更通用的搜索方法

下面的方法比pringPersonsOlderThan更通用。它打印年龄在一定范围内的成员:

public static void printPersonsWithinAgeRange(
    List<Person> roster, int low, int high) {
    for (Person p : roster) {
        if (low <= p.getAge() && p.getAge() < high) {
            p.printPerson();
        }
    }
}

如果你想要打印指定性别的成员,或者是指定性别和年龄范围的组合,该怎么办?当你决定修改Person类,添加其他的属性比如关系状态或者地理定位,该怎么办?虽然这个方法比pringPersonsOlderThan更通用,但尝试为每个可能的搜索查询创建一个单独的方法仍然会导致代码的脆弱。相反,你可以将按指定条件搜索的代码分开在不同的类中。

方式3:在局部类中指定搜索条件代码

下面的方法打印符合搜索条件的成员:

public static void printPersons(
    List<Person> roster, CheckPerson tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

这个方法通过调用CheckPerson参数tester中的tester.test方法,来检查每一个包含在List参数roster中的Person实例。如果方法tester.test返回一个true值,那么Person实例中的printPersons方法将会被调用。

你通过实现CheckPerson接口,来指定查询条件:

interface CheckPerson {
    boolean test(Person p);
}

下面的类实现CheckPerson接口,来为test方法提供一个实现。这个方法筛选出了有资格为美国服役的成员:如果它的Person参数是男性并且年龄介于18到25之间,它将会返回true值:

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());

虽然这个方式比前面的方式要好一点——当改变Person的结构时,你不需要重写方法——但你仍然有一些额外的多余的代码:每一个你计划要在应用中执行的查询,都需要一个新的接口和局部类。因为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
);

您可以使用标准的函数式接口来代替接口CheckPerson,这将进一步减少所需的代码量。

方式6:使用带有标准函数式接口的Lambda表达式

重新考虑CheckPerson接口:

interface CheckPerson {
    boolean test(Person p);
}

这是一个非常简单的接口。它是一个函数式接口,因为它只包含一个抽象方法。这个方法带有一个参数并且返回一个boolean值。这个方法式如此的简单,所以它可能没有必要定义在你的应用中。因此JDK定义了几个标准的函数式接口,你可以在java.util.function包中找到它们。

例如,你可以使用Predicate<T>接口代替CheckPerson。这个接口包含一个方法boolean test(T t):

interface Predicate<T> {
    boolean test(T t);
}

接口Predicate<T>是一个泛型接口的例子。(更多关于泛型的知识,请看Generics (Updated)。)泛型类型(比如泛型接口)使用尖括号(<>)指定一个或者多个参数。这个接口只有一个类型参数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();
        }
    }
}

因此,下面的方法调用和你在方式3中调用printPersons一样:

printPersonsWithPredicate(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

在这个方法中,这并不是使用lambda表达式的惟一可能的地方。下面介绍其他使用Lambda表达式的地方。

方式7:在整个应用中使用Lambda表达式

重新考虑printPersonWithPredicate方法中其他可以使用Lambda表达式的位置:

public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

该方法检查List参数roster中的每个Person实例,通过Predicate参数tester测试Person实例是否满足条件。如果满足,就调用Person实例的printPerson方法。

除了调用printPerson方法,你可以通过Lambda表达式指定在Person实例上进行的操作。假设你需要一个类似printPerson功能的Lambda表达式,接收一个参数(Person类型的对象)并返回void。请记住,要使用lambda表达式,你必须实现一个函数式接口。在这里,你需要一个包含一个抽象方法的函数式接口,该抽象方法接收一个Person类参数并返回voidConsumer<T>接口包含了方法void accept<T t>,恰好符合要求。下面的方法将使用Consumer<Person>accept方法代替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);
            }
        }
}

因此,下面的方法调用和你在方式3中调用printPersons效果是一样的:

processPersons(
     roster,
     p -> p.getGender() == Person.Sex.MALE
         && p.getAge() >= 18
         && p.getAge() <= 25,
     p -> p.printPerson()
);

如果你需要对成员信息做更多的操作而非只是打印。假设你需要验证成员的个人信息或者获取他们的联系方式,该怎么做?在这种情况下,你需要一个函数式接口,该接口包含了一个能返回某值的抽象方法。Function<T, R>接口包含了R apply(T t)方法。下面的方法获取了mapper参数指定的数据,并对其执行由block参数指定的操作:

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.获取对象集合。在本例中,获取了Person对象的集合roster。值得注意的是集合roster是属于List类型的集合同时也是Iterable类型的对象。
2.筛选出满足Predicate对象tester指定条件的对象。本例中,Predicate对象由一个Lambda表达式提供,表达式指定了筛选成员的条件。
3.通过Function对象将筛选出的对象映射为一个值。本例中,Function对象由一个返回成员邮箱的Lambda表达式提供。
4.在每一个映射后的值上执行由Consumer对象block指定的操作。本例中,Consumer对象是一个Lambda表达式,表达式打印由Function对象返回的电子邮箱地址。

你还可以将这每个操作替换为聚集操作(aggregate operation)。

方式9:使用接受Lambda表达式作为参数的聚合操作

接下来的示例使用了聚集操作完成了方案8中相同的工作:

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 Action Aggregate Operation
Obtain a source of objects Stream<E> stream()
Filter objects that match a Predicate object Stream<T> filter(Predicate<? super T> predicate)
Map objects to another value as specified by a Function object <R> Stream<R> map(Function<? super T,? extends R> mapper)
Perform an action as specified by a Consumer object void forEach(Consumer<? super T> action)

filter,map以及forEach操作都是聚集操作。聚集操作从一个流中获取需要操作的元素,而不是直接从集合中获取(这就是开始需要调用stream的原因)。一个流是元素的序列,不同于集合,它不是一个存储元素的数据结构。相反,流通过管道从源(例如集合)中运输数据。管道是一系列的流操作,本例中就是filter-map-forEach。除此之外,聚集操作一般使用Lambda表达式作为其参数,从而达到自定义的目的。

更深入的关于聚合操作的讨论,请看Aggregate Operations课程。

在GUI程序中应用Lambda表达式

在图形用户界面的应用中响应诸如键盘事件、鼠标事件或者滚动事件的时候,你一般都需要创建事件处理器,而这通常涉及到实现某个接口。通常情况下,事件处理接口都是包含一个方法的函数式接口。

在匿名类一节中讨论了javaFX示例HelloWorld.java,你可以使用Lambda表达式替代匿名类:

btn.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent event) {
                System.out.println("Hello World!");
            }
        });

btn.setOnAction方法指定了当btn代表的按钮被触发时需要执行的操作。这个方法需要一个EventHandler<ActionEvent>类型的对象。EventHandler<ActionEvent>接口只包含了一个方法,void handle(T event)。这是一个函数接口,因此你可以如下使用Lambda表达式代替:

 btn.setOnAction(
          event -> System.out.println("Hello World!")
        );

Lambda表达式的语法

一个lambda表达式由以下几个部分组成:

注意:lambda表达式的形参类型是可以省略的。除此之外,如果只有一个形参,小括号也可以省略。例如下面的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语句由于不是一个表达式,因而你必须将它放置在一对花括号({})中。然而你不必将返回void的方法调用放到花括号中。例如,下面是一个合法的Lambda表达式:

email -> System.out.println(email)

鉴于Lambda看起来和方法声明很相似;你可以将Lambda表达式看成是匿名方法。

接下来的示例,Calculator展示了接收一个以上形参的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定义了两个运算,additionsubtraction。以下是示例的输出:

40 + 2 = 42
20 - 10 = 10

访问闭包环境的局部变量

和局部类及匿名类一样,Lambda表达式也可以捕获变量;他们对于局部变量有相同的访问权限。然而和局部及匿名类不同的是,Lambda表达式不存在任何隐匿问题(shadowing issues)。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

如果将Lambda表达式myConsumer中的参数y替换为x,编译器会提示错误:

Consumer<Integer> myConsumer = (x) -> {
    // ...
}

编译器提示错误“变量x已经在方法methodInFirstLevel(int)定义了”,因为Lambda表达式并没有引入新的作用域。因此,你可以直接访字段、方法以及外部作用域中的局部变量。例如Lambda表达式可以直接访问methodInFirstLevel的参数x。需要访问外部类的变量,需使用 this关键字。在该示例中,this.x指向的是成员变量FirstLevel.x

然而如同局部类及匿名类,Lambda表达式只可以访问final或者effectively final类型的局部变量和外部作用域参数。假如在methodFirstLevel的开始处加入如下的赋值语句:

void methodInFirstLevel(int x) {
  x = 99;
  // ...
}

由于这个赋值语句,变量FirstLevel.x不再是effectively final了。此时编译器就会在Lambda表达式试图访问FirstLevel.x变量的地方输出错误信息,“Lambda表达式所访问的局部变量必须是final或者effectively final”。

System.out.println("x = " + x);

** 译者注:** 关于effectively final作一点补充,在 Java SE 7 中,编译器对内部类中引用的外部变量(即捕获的变量)要求非常严格:如果捕获的变量没有被声明为 final 就会产生一个编译错误。Java SE 8现在放宽了这个限制——对于 Lambda 表达式和内部类,允许在其中捕获那些符合有效只读Effectively final)的局部变量。简单的说,如果一个局部变量在初始化后从未被修改过,那么它就符合有效只读的要求,换句话说,加上final 后也不会导致编译错误的局部变量就是有效只读变量。

确定Lambda的类型

我们是怎么决定一个lambda表达式的类型的呢?回忆一下前面筛选男性并且年龄在18到25岁之间的成员的Lambda表达式示例:

p -> p.getGender() == Person.Sex.MALE
    && p.getAge() >= 18
    && p.getAge() <= 25

这个Lambda表达式用在以下的两个方法中:

当java运行调用printPersons时,它期望一个CheckPerson数据类型,因而Lambda表达式就是这个类型了。然而,当Java运行时调用printPersonsWithPredicate的时候,它期望一个Predicate<Person>类型,因而Lambda表达式就成为了这个类型的实例。这些方法期望的数据类型被称为目标类型(target type)。为了决定一个Lambda的类型,当java编译器发现一个Lambda表达式的时候,它会使用当前上下文或者环境的目标类型。这也就意味着你只能在编译器能够确定目标类型的情形下使用Lambda表达式:

目标类型与方法参数

对于方法参数,java编译器使用两个特性来进行决定目标类型:重载决议(overload resolution)和类型推导(argument inference)。如下面两个函数接口(java.lang.Runnablejava.util.concurrent.Callable<V>):

public interface Runnable {
    void run();
}

public interface Callable<V> {
    V call();
}

方法Runnable.run没有返回值,然而Callable<V>.call有。假设你重载了invoke方法:

void invoke(Runnable r) {
    r.run();
}
<T> T invoke(Callable<T> c) {
    return c.call();
}

那么如下语句将会调用那个函数?

String s = invoke(() -> "done");

invoke(Callable<T>)方法将会被调用,因为该方法有返回值而invoke(Runnable)没有。在这种情况下,Lambda表达式()->"done"的类型为Callable<T>

序列化

如果Lambda表达式的目标参数和捕获参数都是可以可序列化的,那么该Lambda表达式也是可以序列化的。然而,正如内部类一样,对其序列化是极不提倡的。

上一篇下一篇

猜你喜欢

热点阅读