java官网教程(进阶篇)—— Lambda 表达式

目录

  • lambda表达式
    • Lambda表达式的理想用例
      • 方法1:创建方法,搜索匹配一个特征的成员
      • 方法2:创建更多的通用搜索方法
      • 方法3:在本地类中指定搜索条件代码
      • 方法4:在匿名类中指定搜索条件代码
      • 方法5:使用Lambda表达式指定搜索标准代码
      • 方法6:使用Lambda表达式的标准功能接口
      • 方法7:在整个应用程序中使用Lambda表达式
      • 方法8:更广泛地使用泛型
      • 方法9:使用接受Lambda表达式作为参数的聚合操作
    • GUI应用中的Lambda表达式
    • Lambda表达式的语法
    • 访问封闭作用域的局部变量
    • 目标类型
      • 目标类型和方法参数
    • 序列化
    • 方法引用

lambda表达式

匿名类的一个问题是,如果您的匿名类的实现非常简单,比如一个只包含一个方法的接口,那么匿名类的语法可能会显得笨拙和不清晰。在这些情况下,您通常试图将功能作为参数传递给另一个方法,例如当有人单击按钮时应该采取什么操作。Lambda表达式允许您这样做,将功能视为方法参数,或将代码视为数据。

前一节“匿名类”向您展示了如何在不给基类命名的情况下实现基类。尽管这通常比命名类更简洁,但对于只有一个方法的类,甚至匿名类看起来也有点多余和麻烦。Lambda表达式让你更紧凑地表达单方法类的实例。

Lambda表达式的理想用例

假设您正在创建一个社交网络应用程序。您希望创建一个特性,使管理员能够对满足特定条件的社交网络应用程序的成员执行任何类型的操作,比如发送消息。下表详细描述了这个用例:

Field Description
Name 对选定的成员执行操作
Primary Actor 管理员
Preconditions 管理员成功登录系统。
Postconditions 仅对符合指定条件的成员执行操作。
Main Success Scenario 1.管理员指定执行特定操作的成员的标准。
2.管理员指定要对这些选定成员执行的操作。
3.管理员选择Submit按钮。
4.系统查找符合指定条件的所有成员。
5.系统对所有匹配的成员执行指定的动作。
Extensions 管理员可以选择在指定要执行的操作或选择Submit按钮之前预览那些符合指定条件的成员。
Extensions 一天中很多次。

假设这个社交网络应用程序的成员由以下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中找到本节中描述的代码摘录。

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

一个简单的方法是创建几个方法;每种方法都会搜索匹配某一特征的成员,比如性别或年龄。下面的方法打印出比指定年龄更老的成员:

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

这种方法可能会使您的应用程序变得脆弱,这是由于引入更新(例如较新的数据类型)而导致应用程序无法工作的可能性。假设你升级了你的应用程序,改变了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:在本地类中指定搜索条件代码

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

public static void printPersons(
    List<Person> 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);
}

下面的类通过指定方法测试的实现来实现CheckPerson接口。这个方法过滤了符合美国选择性服务资格的成员:如果Person参数为男性且年龄在18到25岁之间,则返回真值:

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岁之间的男性:年龄在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,这进一步减少了所需的代码量。

方法6:使用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是泛型接口的一个例子。泛型类型(如泛型接口)在尖括号(<>)中指定一个或多个类型参数。该接口只包含一个类型参数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表达式的其他方法。

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

重新考虑printPersonsWithPredicate方法,看看你还可以在哪里使用lambda表达式:

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

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

而不是调用方法printPerson,您可以指定一个不同的操作来执行那些满足测试人员指定的标准的Person实例。您可以使用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)。以下方法获取由参数映射器指定的数据,然后对其执行由参数块指定的操作:

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:更广泛地使用泛型

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

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

要打印符合选择服务资格的成员的电子邮件地址,调用proceselements方法如下:

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

这个方法调用执行以下操作:

  1. 从集合源获取对象的源。在本例中,它从集合roster中获取Person对象的一个源。请注意,集合roster是List类型的集合,也是Iterable类型的对象。
  2. 筛选与Predicate对象tester匹配的对象。在本例中,Predicate对象是一个lambda表达式,它指定哪些成员有资格使用选择服务。
  3. 将每个过滤的对象映射到函数对象映射器指定的值。在本例中,Function对象是一个lambda表达式,它返回成员的电子邮件地址。
  4. 对每个映射对象执行由Consumer对象块指定的操作。在这个例子中,Consumer对象是一个lambda表达式,它输出一个字符串,即Function对象返回的电子邮件地址。

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

方法9:使用接受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方法执行的每个操作和相应的聚合操作:

processElements Action Aggregate Operation
获取对象的源 Stream stream()
筛选与Predicate对象匹配的对象 Stream filter(Predicate predicate)
将对象映射到另一个由Function对象指定的值 Stream map(Function mapper)
执行Consumer对象指定的操作 void forEach(Consumer action)

操作filter、map和forEach都是聚合操作。聚合来自流的操作处理元素,而不是直接来自集合(这就是为什么本例中调用的第一个方法是流的原因)。流是元素的序列。与集合不同,它不是存储元素的数据结构。相反,流通过管道携带来自源(如集合)的值。管道是一系列流操作,在这个例子中是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运行时将计算该表达式,然后返回其值。另外,你也可以使用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));    
    }
}

方法operateBinary对两个整数操作数执行数学运算。操作本身由IntegerMath的一个实例指定。这个例子定义了两个带有lambda表达式的操作,加法和减法。示例打印如下:

40 + 2 = 42
20 - 10 = 10

访问封闭作用域的局部变量

与局部类和匿名类一样,lambda表达式可以捕获变量;它们对封闭作用域的局部变量具有相同的访问权限。然而,与本地类和匿名类不同,lambda表达式没有任何shadowing问题(有关更多信息,请参阅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) {

            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表达式的类型?回想一下lambda表达式,它选择了年龄在18到25岁之间的男性成员:

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

这个lambda表达式在以下两种方法中使用:

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

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

  • 变量声明
  • Assignments
  • 返回语句
  • 数组初始化函数
  • 方法或构造函数参数
  • Lambda expression bodies
  • Conditional expressions, ?:
  • Cast expressions

目标类型和方法参数

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

考虑以下两个功能接口 ( java.lang.Runnable and java.util.concurrent.Callable):

public interface Runnable {
    void run();
}

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

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

假设你已经按如下方式重载了方法调用:

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

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

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

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

方法调用(Callable)将被调用,因为该方法返回一个值;方法调用(Runnable)没有。在本例中,lambda表达式的类型() -> "done"是Callable.

序列化

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

方法引用

您可以使用lambda表达式来创建匿名方法。然而,有时lambda表达式只是调用一个现有的方法。在这些情况下,通常通过名称来引用现有的方法会更清楚。方法引用使你能够做到这一点;对于已经有名称的方法,它们是紧凑、易于阅读的lambda表达式。

再次考虑Lambda表达式一节中讨论的Person类:

public class Person {

    // ...
    
    LocalDate birthday;
    
    public int getAge() {
        // ...
    }
    
    public LocalDate getBirthday() {
        return birthday;
    }   

    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }
    
    // ...
}

假设您的社交网络应用程序的成员包含在一个数组中,您希望按照年龄对该数组进行排序。您可以使用以下代码在示例MethodReferencesTest中找到本节中描述的代码摘录):

Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);

class PersonAgeComparator implements Comparator<Person> {
    public int compare(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}
        
Arrays.sort(rosterAsArray, new PersonAgeComparator());

这个sort调用的方法签名如下:

static <T> void sort(T[] a, Comparator<? super T> c)

注意,接口Comparator是一个功能性接口。因此,你可以使用lambda表达式,而不是先定义然后创建一个实现Comparator的类的新实例:

Arrays.sort(rosterAsArray,
    (Person a, Person b) -> {
        return a.getBirthday().compareTo(b.getBirthday());
    }
);

但是,用于比较两个Person实例的出生日期的方法已经作为Person. comparebyage存在。你可以在lambda表达式的主体中调用这个方法:

Arrays.sort(rosterAsArray,
    (a, b) -> Person.compareByAge(a, b)
);

因为这个lambda表达式调用了一个现有的方法,所以你可以使用一个方法引用而不是lambda表达式:

Arrays.sort(rosterAsArray, Person::compareByAge);

方法引用Person::compareByAge在语义上与lambda表达式(a, b) -> Person.compareByAge(a, b)相同。每个方法都具有以下特征:

  • 它的形式参数列表是从Comparator.compare中复制的,后者是(Person, Person)。
  • 它的主体调用Person.compareByAge方法。

各种方法引用

方法引用有四种:

Kind Syntax Examples
对静态方法的引用 ContainingClass::staticMethodName Person::compareByAge
MethodReferencesExamples::appendStrings
引用特定对象的实例方法 containingObject::instanceMethodName myComparisonProvider::compareByName
myApp::appendStrings2
对特定类型任意对象的实例方法的引用 ContainingType::methodName String::compareToIgnoreCase
String::concat
对构造函数的引用 ClassName::new HashSet::new

下面的例子,MethodReferencesExamples,包含前三种方法引用的例子:

import java.util.function.BiFunction;

public class MethodReferencesExamples {
    
    public static <T> T mergeThings(T a, T b, BiFunction<T, T, T> merger) {
        return merger.apply(a, b);
    }
    
    public static String appendStrings(String a, String b) {
        return a + b;
    }
    
    public String appendStrings2(String a, String b) {
        return a + b;
    }

    public static void main(String[] args) {
        
        MethodReferencesExamples myApp = new MethodReferencesExamples();

        // Calling the method mergeThings with a lambda expression
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", (a, b) -> a + b));
        
        // Reference to a static method
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", MethodReferencesExamples::appendStrings));

        // Reference to an instance method of a particular object        
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", myApp::appendStrings2));
        
        // Reference to an instance method of an arbitrary object of a
        // particular type
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", String::concat));
    }
}

所有的System.out.println()语句都输出相同的内容:Hello World!

BiFunction是java.util.function包中的许多功能接口之一。双功能函数接口可以表示接受两个参数并产生结果的lambda表达式或方法引用。

引用静态方法

法引用Person::compareByAge和MethodReferencesExamples:: appendstring是对静态方法的引用。

引用特定对象的实例方法

下面是一个引用特定对象的实例方法的例子:

class ComparisonProvider {
    public int compareByName(Person a, Person b) {
        return a.getName().compareTo(b.getName());
    }
        
    public int compareByAge(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);

方法引用myComparisonProvider::compareByName调用方法compareByName,它是对象myComparisonProvider的一部分。JRE推断方法类型参数,在本例中为(Person, Person)。

类似地,方法引用myApp::appendStrings2调用方法appendStrings2,它是对象myApp的一部分。JRE推断方法类型参数,在本例中为(String, String)。

引用特定类型的任意对象的实例方法

下面是一个引用特定类型任意对象的实例方法的例子:

String[] stringArray = { "Barbara", "James", "Mary", "John",
    "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

方法引用String::compareToIgnoreCase的等效lambda表达式将具有正式的参数列表(String a, String b),其中a和b是用来更好地描述这个示例的任意名称。方法引用将调用方法a.compareToIgnoreCase(b)。

类似地,方法引用String::concat将调用方法a.concat(b)。

引用构造函数

您可以使用名称new以与静态方法相同的方式引用构造函数。下面的方法将元素从一个集合复制到另一个集合:

public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
    DEST transferElements(
        SOURCE sourceCollection,
        Supplier<DEST> collectionFactory) {
        
    DEST result = collectionFactory.get();
    for (T t : sourceCollection) {
        result.add(t);
    }
    return result;
}

函数接口Supplier包含一个方法get,该方法不接受参数并返回一个对象。因此,你可以用lambda表达式调用transferelement方法,如下所示:

Set<Person> rosterSetLambda =
    transferElements(roster, () -> { return new HashSet<>(); });

你可以使用构造函数引用来代替lambda表达式,如下所示:

Set<Person> rosterSet = transferElements(roster, HashSet::new);

Java编译器推断您希望创建一个包含Person类型元素的HashSet集合。或者,你可以指定如下:

Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);

你可能感兴趣的:(java官网教程,java,后端,开发语言)