Java 中文官方教程 2022 版(三)(4)

简介: Java 中文官方教程 2022 版(三)

Java 中文官方教程 2022 版(三)(3)https://developer.aliyun.com/article/1486283


方法 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:在本地类中指定搜索条件代码

以下方法打印符合您指定搜索条件的成员:

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

此方法检查roster参数中包含的每个Person实例是否满足CheckPerson参数tester中指定的搜索条件,方法是调用tester.test方法。如果tester.test方法返回true值,则在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实现了一个接口,您可以使用匿名类代替本地类,避免为每个搜索声明一个新类的需要。

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

有关如何定义 Lambda 表达式的语法,请参阅 Lambda 表达式的语法。

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

第六种方法:使用 Lambda 表达式与标准函数式接口

重新考虑CheckPerson接口:

interface CheckPerson {
    boolean test(Person p);
}

这是一个非常简单的接口。它是一个函数式接口,因为它只包含一个抽象方法。这个方法接受一个参数并返回一个boolean值。这个方法如此简单,以至于在你的应用程序中定义一个可能不值得。因此,JDK 定义了几个标准的函数式接口,你可以在java.util.function包中找到。

例如,你可以在CheckPerson的位置使用Predicate接口。这个接口包含方法boolean test(T t)

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

接口Predicate是一个泛型接口的示例。(有关泛型的更多信息,请参阅泛型(更新)课程。)泛型类型(如泛型接口)在尖括号(<>)内指定一个或多个类型参数。这个接口只包含一个类型参数T。当你声明或实例化一个带有实际类型参数的泛型类型时,你就有了一个参数化类型。例如,参数化类型Predicate如下所示:

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

这个参数化类型包含一个与CheckPerson.boolean test(Person p)具有相同返回类型和参数的方法。因此,你可以像下面的方法演示的那样使用Predicate来替代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 表达式的方式。

第七种方法:在整个应用程序中使用 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实例是否满足tester参数指定的条件。如果Person实例确实满足tester指定的条件,则在Person实例上调用printPerson方法。

你可以指定一个不同的操作来执行那些满足tester指定的条件的Person实例,而不是调用printPerson方法。你可以用 lambda 表达式指定这个操作。假设你想要一个类似于printPerson的 lambda 表达式,一个接受一个参数(一个Person类型的对象)并返回void的。记住,要使用 lambda 表达式,你需要实现一个函数式接口。在这种情况下,你需要一个包含可以接受一个Person类型参数并返回void的抽象方法的函数式接口。Consumer接口包含方法void accept(T t),具有这些特征。以下方法用一个调用accept方法的Consumer实例替换了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接口包含方法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:更广泛地使用泛型

重新考虑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. 过滤与tester对象匹配的对象。在这个例子中,Predicate对象是一个指定哪些成员符合应征条件的 lambda 表达式。
  3. 将每个经过筛选的对象映射到由mapper对象指定的值。在这个例子中,Function对象是一个返回成员电子邮件地址的 lambda 表达式。
  4. 根据Consumer对象block指定的操作对每个映射对象执行动作。在此示例中,Consumer对象是一个打印字符串的 Lambda 表达式,该字符串是由Function对象返回的电子邮件地址。

您可以用聚合操作替换每个这些操作。

方法 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操作 聚合操作
获取对象源 Stream<E> **stream**()
过滤与Predicate对象匹配的对象 Stream<T> **filter**(Predicate<? super T> predicate)
根据Function对象将对象映射到另一个值 <R> Stream<R> **map**(Function<? super T,? extends R> mapper)
根据Consumer对象指定的操作执行动作 void **forEach**(Consumer<? super T> action)

操作filtermapforEach聚合操作。聚合操作处理来自流的元素,而不是直接来自集合(这就是为什么此示例中调用的第一个方法是stream的原因)。是元素的序列。与集合不同,它不是存储元素的数据结构。相反,流通过管道从源(例如集合)传递值。管道是一系列流操作,本示例中是filter-map-forEach。此外,聚合操作通常接受 Lambda 表达式作为参数,使您能够自定义它们的行为。

对于更深入讨论聚合操作,请参阅聚合操作课程。

GUI 应用程序中的 Lambda 表达式

要处理图形用户界面(GUI)应用程序中的事件,例如键盘操作、鼠标操作和滚动操作,通常需要创建事件处理程序,这通常涉及实现特定的接口。通常,事件处理程序接口是函数式接口;它们往往只有一个方法。

在 JavaFX 示例HelloWorld.java(在上一节匿名类中讨论)中,您可以在此语句中用 Lambda 表达式替换突出显示的匿名类:

btn.setOnAction(new EventHandler<ActionEvent>() {
            @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 运行时将评估该表达式,然后返回其值。或者,您可以使用一个返回语句:
p -> {
    return p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25;
}
  • 返回语句不是一个表达式;在 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));    
    }
}

方法operateBinary对两个整数操作数执行数学运算。操作本身由IntegerMath的实例指定。该示例使用 lambda 表达式定义了两个操作,additionsubtraction。该示例打印如下内容:

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) {
            int z = 2;
            Consumer<Integer> 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 表达式myConsumer的声明中,将参数x替换为y,那么编译器会生成一个错误:

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

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

然而,与本地和匿名类一样,lambda 表达式只能访问封闭块的局部变量和参数,这些变量必须是 final 或有效 final。在这个例子中,变量z是有效 final;在初始化后其值不会改变。然而,假设在 lambda 表达式myConsumer中添加以下赋值语句:

Consumer<Integer> myConsumer = (y) -> {
    z = 99;
    // ...
}

由于这个赋值语句,变量z不再是有效 final。因此,Java 编译器生成类似于“定义在封闭范围中的局部变量 z 必须是 final 或有效 final”的错误消息。

目标类型

如何确定 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)
  • 方法 6:使用标准函数接口和 Lambda 表达式 中的 public void printPersonsWithPredicate(List roster, Predicate tester)

当 Java 运行时调用方法 printPersons 时,它期望的数据类型是 CheckPerson,因此 lambda 表达式就是这种类型。然而,当 Java 运行时调用方法 printPersonsWithPredicate 时,它期望的数据类型是 Predicate,因此 lambda 表达式就是这种类型。这些方法期望的数据类型称为目标类型。为了确定 lambda 表达式的类型,Java 编译器使用 lambda 表达式所在上下文或情况的目标类型。由此可知,只能在 Java 编译器能够确定目标类型的情况下使用 lambda 表达式:

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

目标类型和方法参数

对于方法参数,Java 编译器使用另外两个语言特性来确定目标类型:重载解析和类型参数推断。

考虑以下两个函数式接口(java.lang.Runnablejava.util.concurrent.Callable):

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

方法Runnable.run不返回值,而Callable.call返回值。

假设您已经重载了方法invoke如下(有关重载方法的更多信息,请参见定义方法):

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

在下面的语句中将调用哪个方法?

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

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

序列化

如果 lambda 表达式的目标类型和捕获的参数都是可序列化的,则可以对其进行序列化。然而,与内部类一样,强烈不建议序列化 lambda 表达式。

相关文章
|
4天前
|
前端开发 Java Maven
【前端学java】全网最详细的maven安装与IDEA集成教程!
【8月更文挑战第12天】全网最详细的maven安装与IDEA集成教程!
21 2
【前端学java】全网最详细的maven安装与IDEA集成教程!
|
9天前
|
存储 网络协议 Oracle
java教程
java教程【8月更文挑战第11天】
14 5
|
1月前
|
SQL 安全 Java
「滚雪球学Java」教程导航帖(更新2024.07.16)
《滚雪球学Spring Boot》是一个面向初学者的Spring Boot教程,旨在帮助读者快速入门Spring Boot开发。本专通过深入浅出的方式,将Spring Boot开发中的核心概念、基础知识、实战技巧等内容系统地讲解,同时还提供了大量实际的案例,让读者能够快速掌握实用的Spring Boot开发技能。本书的特点在于注重实践,通过实例学习的方式激发读者的学习兴趣和动力,并引导读者逐步掌握Spring Boot开发的实际应用。
42 1
「滚雪球学Java」教程导航帖(更新2024.07.16)
WXM
|
25天前
|
Oracle Java 关系型数据库
Java JDK下载安装及环境配置超详细图文教程
Java JDK下载安装及环境配置超详细图文教程
WXM
129 3
|
1月前
|
测试技术 API Android开发
《手把手教你》系列基础篇(九十七)-java+ selenium自动化测试-框架设计篇-Selenium方法的二次封装和页面基类(详解教程)
【7月更文挑战第15天】这是关于自动化测试框架中Selenium API二次封装的教程总结。教程中介绍了如何设计一个支持不同浏览器测试的页面基类(BasePage),该基类包含了对Selenium方法的二次封装,如元素的输入、点击、清除等常用操作,以减少重复代码。此外,页面基类还提供了获取页面标题和URL的方法。
44 2
|
1月前
|
Web App开发 XML Java
《手把手教你》系列基础篇(九十六)-java+ selenium自动化测试-框架之设计篇-跨浏览器(详解教程)
【7月更文挑战第14天】这篇教程介绍了如何使用Java和Selenium构建一个支持跨浏览器测试的自动化测试框架。设计的核心是通过读取配置文件来切换不同浏览器执行测试用例。配置文件中定义了浏览器类型(如Firefox、Chrome)和测试服务器的URL。代码包括一个`BrowserEngine`类,它初始化配置数据,根据配置启动指定的浏览器,并提供关闭浏览器的方法。测试脚本`TestLaunchBrowser`使用`BrowserEngine`来启动浏览器并执行测试。整个框架允许在不同浏览器上运行相同的测试,以确保兼容性和一致性。
47 3
|
1月前
|
存储 Web App开发 Java
《手把手教你》系列基础篇(九十五)-java+ selenium自动化测试-框架之设计篇-java实现自定义日志输出(详解教程)
【7月更文挑战第13天】这篇文章介绍了如何在Java中创建一个简单的自定义日志系统,以替代Log4j或logback。
136 5
|
1月前
|
Java 数据安全/隐私保护
Java无模版导出Excel 0基础教程
经常写数据导出到EXCEL,没有模板的情况下使用POI技术。以此作为记录,以后方便使用。 2 工具类 样式工具: 处理工具Java接口 水印工具 导出Excel工具类 3 测试代码 与实际复杂业务不同 在此我们只做模拟 Controller Service 4 导出测试 使用Postman进行接口测试,没接触过Postman的小伙伴可以看我这篇博客Postman导出excel文件保存为文件可以看到导出很成功,包括水印 sheet页名称自适应宽度。还有一些高亮……等功能可以直接搜索使用
Java无模版导出Excel 0基础教程
|
1月前
|
设计模式 测试技术 Python
《手把手教你》系列基础篇(九十二)-java+ selenium自动化测试-框架设计基础-POM设计模式简介(详解教程)
【7月更文挑战第10天】Page Object Model (POM)是Selenium自动化测试中的设计模式,用于提高代码的可读性和维护性。POM将每个页面表示为一个类,封装元素定位和交互操作,使得测试脚本与页面元素分离。当页面元素改变时,只需更新对应页面类,减少了脚本的重复工作和维护复杂度,有利于团队协作。POM通过创建页面对象,管理页面元素集合,将业务逻辑与元素定位解耦合,增强了代码的复用性。示例展示了不使用POM时,脚本直接混杂了元素定位和业务逻辑,而POM则能解决这一问题。
43 6
|
1月前
|
设计模式 Java 测试技术
《手把手教你》系列基础篇(九十四)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-下篇(详解教程)
【7月更文挑战第12天】在本文中,作者宏哥介绍了如何在不使用PageFactory的情况下,用Java和Selenium实现Page Object Model (POM)。文章通过一个百度首页登录的实战例子来说明。首先,创建了一个名为`BaiduHomePage1`的页面对象类,其中包含了页面元素的定位和相关操作方法。接着,创建了测试类`TestWithPOM1`,在测试类中初始化WebDriver,设置驱动路径,最大化窗口,并调用页面对象类的方法进行登录操作。这样,测试脚本保持简洁,遵循了POM模式的高可读性和可维护性原则。
27 2