javaSE_8系列博客——Java语言的特性(三)--类和对象(19)--嵌套类(Lambda 表达式)

匿名类的一个问题是,如果您的匿名类的实现非常简单,例如仅包含一个方法的接口,则匿名类的语法可能看起来很笨重且不清楚。在这些情况下,您通常会尝试将function作为参数传递给另一种方法,例如当有人点击按钮时应该采取什么措施。 Lambda表达式使您能够执行此操作,将function视为方法参数或代码作为数据。 上一节“匿名类”介绍如何实现基类而不给它一个名称。虽然这通常比命名类更简洁,对于只有一种方法的类,即使是匿名类似乎有点过于繁琐。 Lambda表达式可以更紧凑地表示单一方法类的实例。 本节包括以下主题:

lambda表达式的应用场景

  • 创建搜索符合一个特征的会员的方法
  • 创建更广泛的搜索方法
  • 在局部类中指定搜索条件代码
  • 在匿名类中指定搜索条件代码
  • 使用Lambda表达式指定搜索条件代码
  • 使用具有Lambda表达式的标准功能接口
  • 在您的应用程序中使用Lambda表达式
  • 更广泛地使用泛型
  • 使用接受Lambda表达式作为参数的聚合操作

假设您正在创建一个社交网络应用程序。您想要创建一个功能,使管理员可以在符合特定条件的社交网络应用程序的成员上执行任何类型的操作(例如发送消息)。下表详细描述了这种用例:
javaSE_8系列博客——Java语言的特性(三)--类和对象(19)--嵌套类(Lambda 表达式)_第1张图片
假设这个社交网络应用程序的成员由以下Person类表示:

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    public int getAge() {
        // ...
    }

    public void printPerson() {
        // ...
    }
}

假设您的社交网络应用程序的成员存储在List 实例中。 本节首先介绍了这种用例的天真的方法。它使用本地和匿名类改进了这种方法,然后使用lambda表达式使用高效简明的方法来完成。查找示例中本节中描述的RosterTest(点击下载)

方法一:创建搜索符合一个特征的成员的方法

一种简单的方法是创建几种方法;每个方法搜索符合一个特征的成员,如性别或年龄。以下方法打印比指定年龄更早的成员:

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

注意:

列表是有序集合。集合是将多个元素组合成一个单元的对象。集合用于存储,检索,操纵和传达聚合数据。有关集合的更多信息,请参阅集合跟踪。 这种方法可能会使您的应用程序变得脆弱,这是因为引入更新(例如较新的数据类型)而导致应用程序无法正常工作的可能性。假设您升级应用程序并更改Person类的结构,使其包含不同的成员变量;可能是使用不同数据类型或算法的类记录和测量年龄。您将不得不重写很多API以适应这种变化。此外,这种方法是不必要的限制;例如,如果你想打印比一定年龄小的成员怎么办?

方法二:创建更广泛的搜索方法

以下方法比printPersonsOlderThan更通用;会在特定范围内打印成员:

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

如果要打印指定性别的成员或指定的性别和年龄范围的组合,该怎么办?如果您决定更改Person类并添加其他属性(如关系状态或地理位置),该怎么办?虽然这种方法比printPersonsOlderThan更通用,但是为每个可能的搜索查询创建一个单独的方法仍然可能导致脆弱的代码。您可以将指定要在其他类中搜索的条件的代码分开。

方法三:在局部类中指定搜索条件代码

以下方法打印与您指定的搜索条件匹配的成员:

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

该方法通过调用方法tester.test来检查List参数列表中包含的每个Person实例是否满足CheckPerson参数测试器中指定的搜索条件。如果方法tester.test返回真值,则在Person实例上调用printPersons方法。

要指定搜索条件,您可以实现CheckPerson接口:

interface CheckPerson {
    boolean test(Person p);
}

以下类通过指定方法test的实现来实现CheckPerson接口。该方法过滤在美国有资格选择性服务的成员:如果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实现了一个接口,您可以使用匿名类而不是本地类,并绕过为每个搜索声明一个新类的需要。

方法四:在匿名类中指定搜索条件代码

printPersons(
    roster,
    new CheckPerson() {
        public boolean test(Person p) {
            return p.getGender() == Person.Sex.MALE
                && p.getAge() >= 18
                && p.getAge() <= 25;
        }
    }
);

这种方法减少了所需的代码量,因为您不必为每个要执行的搜索创建一个新类。然而,匿名类的语法是庞大的,因为CheckPerson接口只包含一种方法。在这种情况下,您可以使用lambda表达式而不是匿名类,如下一节所述。

方法五:使用Lambda表达式指定搜索条件代码

CheckPerson接口是一个功能接口。功能接口是只包含一个抽象方法的任何接口。 (功能接口可能包含一个或多个默认方法或静态方法)由于功能接口只包含一个抽象方法,因此在实现时可以省略该方法的名称。为此,您不必使用匿名类表达式,而是使用lambda表达式,该表达式在以下方法调用中突出显示:

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

有关如何定义lambda表达式的信息,请参阅Lambda表达式的语法。 您可以使用标准功能界面来代替CheckPerson接口,从而进一步减少了所需的代码量。

方法六:使用具有Lambda表达式的标准功能接口

重新考虑CheckPerson接口:

interface CheckPerson {
    boolean test(Person p);
}

这是一个非常简单的接口。它是一个功能接口,因为它只包含一个抽象方法。该方法采用一个参数并返回一个布尔值。该方法非常简单,可能不值得在应用程序中定义一个。因此,JDK定义了几个标准的功能接口,您可以在java.util.function包中找到它们。 例如,您可以使用Predicate 界面代替CheckPerson。此接口包含方法boolean test(T t):

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

接口 Predicate 是通用接口的示例。 (有关泛型的更多信息,请参阅Generics (Updated) 课程。) 通用类型(如通用接口)在尖括号(<>)中指定一个或多个类型参数。此接口只包含一个类型参数T.当您使用实际类型参数声明或实例化通用类型时,您具有参数化类型。例如,参数化类型Predicate如下:

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

参数化类型包含一个具有与CheckPerson.boolean test(Person p)相同的返回类型和参数的方法。因此,您可以使用Predicate 代替CheckPerson,如下所示:

public static void printPersonsWithPredicate(
    List roster, Predicate 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表达式的其他方法。

方法七:在您的应用程序中使用Lambda表达式
重新考虑printPersonsWithPredicate的方法,看看你可以在哪里使用lambda表达式:

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

此方法检查List参数名单中包含的每个Person实例是否满足Predicate参数测试器中指定的条件。如果Person实例满足测试者指定的条件,则在Person实例上调用printPersron方法。

您可以为满足测试者指定的条件的Person实例指定一个不同的操作,而不是调用printPerson方法。您可以使用lambda表达式指定此操作。假设你想要一个类似于printPerson的lambda表达式,其中一个参数是一个Person类型的对象并返回void。

记住,要使用lambda表达式,您需要实现一个函数接口。在这种情况下,您需要一个包含抽象方法的功能界面,该方法可以使用一个Person类型的参数,并返回void。 Consumer 接口包含具有这些特征的方法void accept(T t)。以下方法将调用p.printPerson()替换为调用方法accept的Consumer 实例:

public static void processPersons(
    List roster,
    Predicate tester,
    Consumer 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()
);

如果您想要更多地使用会员的个人资料,而不是打印出来。假设您要验证会员的个人资料或检索他们的联系信息?在这种情况下,您需要一个函数接口,其中包含一个返回值的抽象方法。函数

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

方法八:更广泛地使用泛型

重新考虑方法processPersonsWithFunction。以下是它的一般版本,它接受包含任何数据类型元素的集合作为参数

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方法,如下所示:

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类型的集合,也是Iterable类型的对象。
  2. 过滤与Predicate对象测试器匹配的对象。在此示例中,Predicate对象是一个lambda表达式,用于指定哪些成员将符合Selective Service的资格。
  3. 将每个过滤的对象映射到由Function对象映射器指定的值。在此示例中,Function对象是一个返回成员的电子邮件地址的lambda表达式。
  4. 对Consumer对象块指定的每个映射对象执行一个操作。在这个例子中,Consumer对象是一个lambda表达式,它打印一个字符串,它是Function对象返回的电子邮件地址。

您可以使用聚合操作替换这些操作

方式九:使用接受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执行的每个操作与相应的聚合操作:
javaSE_8系列博客——Java语言的特性(三)--类和对象(19)--嵌套类(Lambda 表达式)_第2张图片

filter、map和forEach都是聚合操作。来自流的聚合操作流程元素,而不是直接来自集合(这是为什么在此示例中调用的第一个方法是流)。流是一系列元素。与集合不同,它不是存储元素的数据结构。相反,流携带来自源的值,例如通过管道的收集。流水线是流操作的序列,在本例中为filter-map-forEach。另外,聚合操作通常接受lambda表达式作为参数,使您可以自定义它们的行为。

有关聚合操作的更全面的讨论,请参阅聚合操作的课程。

在GUI应用中的使用lambda

要在图形用户界面(GUI)应用程序(如键盘操作,鼠标操作和滚动操作)中处理事件,您通常会创建事件处理程序,通常涉及实现特定接口。事件处理接口通常是函数接口 ; 他们往往只有一种方法。 在JavaFX示例HelloWorld.java中(在上一节Anonymous Classes中讨论过),您可以在此语句中用突出显示的匿名类替换lambda表达式:

 btn.setOnAction(new EventHandler() {

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

方法调用btn.setOnAction指定当您选择由btn对象表示的按钮时会发生什么。此方法需要一个类型为EventHandler 的对象。 EventHandler 接口只包含一个方法,void handle(T event)。此接口是一个功能界面,因此您可以使用以下突出显示的lambda表达式来替换它:

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

Lambda 表达式的句法

lambda表达式由以下内容组成:

  • 用括号括起的逗号分隔的正式参数列表。 CheckPerson.test方法包含一个参数p,它表示Person类的一个实例。

    注意:您可以忽略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语句不是表达式 ; 在lambda表达式中,您必须用大括号({})括起来。但是,您不必在大括号中包含一个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));    
    }
}

方法operatingBinary对两个整数操作数执行一个数学运算。操作本身由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 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) -> {
    // ...
}

编译器生成错误“变量x已经在方法methodInFirstLevel(int)中定义”,因为lambda表达式不会引入新的一级范围。因此,您可以直接访问封闭范围的字段,方法和局部变量。例如,lambda表达式直接访问methodInFirstLevel方法的参数x。要访问封闭类中的变量,请使用关键字this。在这个例子中,this.x指的是成员变量FirstLevel.x。

然而,像局部类和匿名类一样,lambda表达式只能访问最终或有效最终的封闭块的局部变量和参数。例如,假设您在methodInFirstLevel定义语句之后立即添加以下赋值语句:

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

由于这个赋值语句,变量FirstLevel.x 不再是有效的final 。因此,Java编译器会生成类似于“lambda表达式引用的本地变量必须是final”或“effectively final”的错误消息,其中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表达式用于以下两种方法:

  • 方法3中的public static void printPersons(List roster,CheckPerson tester)在本地类中指定搜索条件代码
  • public void printPersonsWithPredicate(List roster,Predicate tester)在方法6中:使用带有Lambda表达式的标准功能界面

当Java运行时调用printPersons方法时,它期待一种CheckPerson的数据类型,因此lambda表达式是这种类型的。但是,当Java运行时调用printPersonsWithPredicate方法时,它期待数据类型为Predicate ,因此lambda表达式属于此类型。

这些方法期望的数据类型称为目标类型。要确定lambda表达式的类型,Java编译器将使用上下文的目标类型或其中找到lambda表达式的情境。因此,您只能在Java编译器可以确定目标类型的情况下使用lambda表达式,如下情况:

  • 变量声明
  • 分配值
  • 返回语句
  • 数组初始化
  • 方法或者构造器的参数
  • Lambda表达式的语句体
  • 条件表达式,?:
  • 类型转换表达式

目标类型和方法参数

对于方法参数,Java编译器使用其他两种语言功能来确定目标类型:重载解析和类型参数推断。
考虑以下两个功能接口(java.lang.Runnable和java.util.concurrent.Callable ):

public interface Runnable {
    void run();
}

public interface Callable {
    V call();
}

方法Runnable.run不返回值,而Callable .call 返回值。 假设你已经按照以下方法重载了方法调用

void invoke(Runnable r) {
    r.run();
}

<T> T invoke(Callable<T> c) {
    return c.call();
}

在以下语句中将调用哪种方法?

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

调用方法invoke(Callable )将被调用,因为该方法返回一个值 ; 该方法调用(Runnable)不。在这种情况下,lambda表达式() - >“done”的类型是Callable

序列化

如果lambda表达式的目标类型及其捕获的参数是可序列化的,则可以序列化它。然而,像内部类一样,强烈地不鼓励lambda表达式的序列化。

你可能感兴趣的:(●编程语言,------【面向对象】,------【Java】,javaSE_8,lambda,java,se)