Java8————Lambda表达式(二)

译者注:文中内容均来自于官方教程《Lambda Expressions》,但是由于英汉语言的差异,部分语句官方描述过于冗余,因此译者根据通常状况的理解做了微调,但不会影响表达的含义。比如:

原文:You want to create a feature that enables an administrator to perform any kind of action, such as sending a message, on members of the social networking application that satisfy certain criteria. 

精准翻译:你想要创建一个可以让管理员表现某种行为的功能,比如发送一个消息给你的社交网络应用中满足具体条件的用户。

译者翻译: 你希望开发一个可以让管理员执行某种行为的功能,比如发送消息给那些满足某种条件的用户。


 Lambda表达式

使用匿名类的时候有一个问题是,如果你的匿名类(译者注:匿名内部类就是为了实现某些接口而存在的)实现非常简单,比如一个只包含一个方法的接口,那么匿名类的语法可能会有些笨拙和不清晰。这种情况下,通常要尝试传入一个函数作为另一个方法的参数,比如,当某人点击一个按钮时会执行什么动作?Lambda表达式允许你将一个函数作为方法的参数,或以代码作为数据。

在前面的部分,Anonymous Classes,展示了如何以不命名的方式实现一个基础类。尽管这比一个已命名的类更加简洁,但对于仅有一个方法的类,即便是匿名类似乎也有点冗余和笨重。Lambda表达式可以让你更简洁的描述一个“单方法(single-method)”类的实例。

这部分涵盖了以下主题:

一、理想的Lambda表达式使用情况

方案1:创建一个用于搜索满足某一特征的成员的方法

方案2:创建一个更通用的搜索方法

方案3:在本地类(Local Class)中指定一段搜索条件代码

方案4:在匿名类中指定一段搜索条件代码

方案5:通过Lambda表达式指定一段搜索条件代码

方案6:使用标准函数接口(Functionl Interfaces)的Lambda表达式

方案7:在你的应用中全面使用Lambda表达式

方案8:更广泛的使用通用化

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

二、GUI应用中的Lambda表达式

三、Lambda表达式语法

四、在封闭域中访问局部变量

五、目标类型

        目标类型和方法参数

六、序列化

一、理想的Lambda表达式使用情况

假设你正在开发一个社交网络的应用。你希望开发一个可以让管理员执行某种行为的功能,比如发送消息给那些满足某种条件的用户。下面的表格描述了这种使用情况的细节。

属性   描述
名称         给选中的用户执行动作
主要执行人Administrator
先决条件             Administrator已经登录
后验条件    动作只对满足特定条件的用户生效
成功的关键
  1. Administrator规定用于执行某一动作的用户需满足的条件
  2. Administrator规定要对选中的用户执行的动作。
  3. Administrator选择提交按钮
  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表达式的方案为结尾。更多信息请查看:RosterTest

方案1:创建一个用于搜索满足某一特征的成员的方法

一个最傻瓜式的方案是创建多个方法,每个方法用于搜索符合一种特征的用户,比如性别或者年龄。下面的方法会打印出大于某个指定年龄的所有成员。

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

注意:List 是一个有序的Collection,Collection是一个可以把许多元素放入到单独的单元中的对象。Collection用于存储、检索、操作、传递集合数据。Collection相关的更多信息,请参考Collection系列。

这种解决方案可能使你的应用非常脆弱,由于某些修改的加入(比如新的数据类型)就可能会使应用无法正常工作。假设你升级了你的应用,并改变了Person类的结构,比如它包含了不同的成员变量,再或者采用了一种不同的数据类型或算法来记录和比较年龄。你就不得不重写大量的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();
        }
    }
}

要是你想要打印指定性别的成员,或指定性别和年龄范围的组合条件呢?要是你决定改变Person类并且加入其他的属性比如情感状况或地理位置呢?尽管这种方法比printPersonsOlderThan更通用一些,但创建一个单独的方法来满足每个可能的查询仍然会导致脆弱的代码。你可以在另外一个类中为你希望搜索的条件单独编码。

方案3:在本地类(Local Class)中指定一段搜索条件代码

下面的方法打印出满足指定搜索条件的所有成员。

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

这个方法通过调用参数tester的test()方法校验roster中的每一个Person对象不论它是否满足已经被指定在CheckPerson中的搜索条件。如果tester.test()返回一个true,那么Person对象就会调用printPerson。

为了指定一个搜索条件,你需要实现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;
    }
}

为了使用这个类,你创建了它的新的实例,并且调用了printPerson方法。

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 接口是一个functional interface(函数接口),函数接口是仅有一个抽象方法的任意接口。(一个函数接口可能包含一个或多个default方法(译者注:default方法是java8 加入的默认方法,它是一个在接口中存在的非抽象方法)或静态方法)因为函数接口仅有一个抽象方法,你可以在实现这个接口的时候省略方法名。为了做到这一点,你是用了一个Lambda表达式,而不是一个匿名内部类表达式,正如下面高亮部分所示:

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

参考《Java8————Lambda表达式(一)》来获得更多关于如何定义Lambda表达式的信息。

你可以使用标准的函数接口来取代CheckPerson的实例,这样会大大将降低所需的代码量。

方案6:使用标准函数接口(Functionl Interfaces)的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表达式

重新思考printPersonsWithPredicate 方法,看看Lambda表达式还可以用在什么地方。

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

这个方法会校验roster中的每个Person实例,不论它是否满足在Predicate类型参数tester中指定的条件。如果Person实例满足条件,那么printPersron 方法将会被调用。

除了调用printPerson方法,你可以为那些满足条件的Person对象指定不同的行为。你可以使用Lambda表达式来指定这个行为。假设你想要一个和printPerson类似的Lambda表达式,一个只接收一个参数,且没有返回值的Lambda表达式。记住,要想使用Lambda表达式,必须实现一个函数接口。为此,你需要一个仅包含一个抽象方法的函数接口,这个方法可以接受一个Person实例作为对象,并且没有返回值。Consumer<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 是一样的。用于打印成员的Lambda表达式已经被高亮显示:

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

下面的方法获取每一个在roster中符合义务兵役的成员的电子邮件地址,然后打印他们。

processPersonsWithFunction(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

方案8:更广泛的使用通用化方案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、用source集合中获得了对象的数据源。在这个例子中,它从roster集合中取得一个Person的数据源。注意roster集合,这个List类型的集合,其本身也是一个Iterable类型的对象。

2、筛选出与Predicate类型对象tester相匹配的对象。在这个例子中,Predicate对象是一个用来指定哪些成员符合义务兵役条件的Lambda表达式。

3、将每一个筛选出的对象映射成一个由Function对象mapper指定的值。在这个例子中,Function对象是一个返回成员电子邮件地址的Lambda表达式。

4、对每个已经映射的对象执行一个由Consumer对象block指定的动作。在这个例子中,Consumer对象是一个打印由Function对象返回的电子邮件地址字符串的Lambda表达式。

你可以用聚合操作来代替每一个动作。

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

下面的例子使用了聚合操作来打印集合roster中的每一个符合义务兵役的成员的电子邮件地址。

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

 操作filter,map和forEach都是聚合操作。聚合操作从一个stream中处理每一个元素,而不是直接在集合中(这也是为什么在这个例子中第一个调用的方法是stream())Stream是元素的一个序列。与集合不同,它不是一个数据结构,并不存储元素。它通过一个管道从一个数据源中(如Collection)搬运每一个值。pipeline 是Stream操作的一个序列,就像上面例子中的filter-map-forEach。另外,聚合操作可以接收Lambda表达式作为参数,允许你自定义他们的表现行为。

二、GUI应用中的Lambda表达式(略)

三、Lambda表达式语法

参考《Java8————Lambda表达式(一)

四、在封闭域中访问局部变量

如同大多数本地和匿名类,Lambda表达式可以捕捉变量,它们有相同的访问途径访问封闭域中的局部变量。然而,与本地和匿名类不同的是,Lambda表达式不会有任何覆盖问题(shadowing issues)(参考Shadowing)。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

如果你在myConsumer中的声明里用 y 来代替参数 x ,那么编译器会产生一个错误。

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

编译器会产生一个错误“variable x is already defined in method methodInFirstLevel(int)”因为Lambda表达式不会引入一个新的级别的域。因此,你可以直接访问封闭域中的属性,方法,局部变量。例如,Lambda表达式可以直接访问methodInFirstLevel方法的参数 x 。要访问内部类中的变量,需使用this关键字。在这个例子中,this.x 引用的是成员变量:FirstLevel.x

然而,和本地及匿名类一样,Lambda表达式只能访问由final或Effectively final修饰的局部变量和参数。例如,假设你定义了methodInFirstLevel 方法后立即增加了如下赋值语句:

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

因为这个赋值语句,变量FirstLevel.x 就不再是一个effectively final了。结果,Java编译器会在Lambda表达式myConsumer尝试访问FirstLevel.x变量时产生一个错误:"local variables referenced from a lambda expression must be final or effectively final" 。

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

五、目标类型

你如何决定Lambda表达式的类型呢?再次调用Lambda表达式来筛选年龄在15到25岁之间的男性成员:

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

这个Lambda表达式在下面两个方法中使用到:

1、public static void printPersons(List<Person> roster, CheckPerson tester) 在方案3中。

2、public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) 在方案6中。

当Java运行时调用printPersons方法,期望的类型是CheckPerson,因此Lambda表达式就对应这个类型。然而,当Java运行时调用printPersonsWithPredicate方法时,它期望的类型是Predicate<Person>,那么Lambda表达式就对应这个类型。这些方法期望的数据类型就叫做目标类型。为了决定Lambda表达式的类型,Java编译器会根据Lambda表达式使用处的上下文或环境来使用目标类型。由此你只需使用Lambda表达式即可,Java编译器会帮你决定它的目标类型。

1、变量声明

2、赋值

3、返回语句

4、数组初始化

5、方法或构造器参数

6、Lambda表达式体

7、条件表达式,?:(译者注:即三目运算符)

8、计算表达式(Cast Expression)

目标类型和方法参数

对于方法参数,Java编译器使用两种语言特性来决定目标类型:重载解决和类型参数推断

考虑如下两个函数接口:

public interface Runnable {
    void run();
}

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

方法Runnable.run 没有返回值,Callable<V>.call有返回值。

假设你有一个重载的方法如下:

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表达式强烈不推荐!

 

 

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页