3

Lambda表达式

匿名类的一个问题是,如果匿名类的实现非常简单,例如只包含一个方法的接口,那么匿名类的语法可能看起来不实用且不清楚,在这些情况下,你通常会尝试将功能作为参数传递给另一个方法,例如当有人单击按钮时应采取的操作,Lambda表达式使你可以执行此操作,将功能视为方法参数,或将代码视为数据。

上一节匿名类向你展示了如何在不给它命名的情况下实现基类,虽然这通常比命名类更简洁,但对于只有一个方法的类,即使是匿名类也似乎有点过多和繁琐,Lambda表达式允许你更紧凑地表达单方法类的实例。

Lambda表达式的理想用例

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

字段 描述
名称 对选定的成员执行操作
主要角色 管理员
前提条件 管理员已登录系统
后置条件 仅对符合指定条件的成员执行操作
主要成功案例 1. 管理员指定要执行特定操作的成员的条件
2. 管理员指定要对这些选定成员执行的操作
3. 管理员选择Submit按钮
4. 系统查找符合指定条件的所有成员
5. 系统对所有匹配成员执行指定的操作
扩展 管理员可以选择在指定要执行的操作之前或选择Submit按钮之前预览符合指定条件的成员
发生频率 一天中很多次

假设此社交网络应用程序的成员由以下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是有序集合,集合是将多个元素组合到一个单元中的对象,集合用于存储、检索、操作和传递聚合数据,有关集合的更多信息,请参阅集合路径。

这种方法可能会使你的应用程序变得脆弱,这是由于引入了更新(例如更新的数据类型)导致应用程序无法工作的可能性。假设你升级应用程序并更改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中指定的搜索条件,如果方法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接口,从而进一步减少所需的代码量。

方法6:将标准功能性接口与Lambda表达式一起使用

重新考虑CheckPerson接口:

interface CheckPerson {
    boolean test(Person p);
}

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

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

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

接口Predicate<T>是泛型接口的示例(有关泛型的更多信息,请参阅泛型(更新)课程),泛型类型(例如泛型接口)在尖括号(<>)中指定一个或多个类型参数,该接口仅包含一个类型参数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:在局部类中指定搜索条件代码以获取有资格获得选择性服务的成员中调用printperson时相同:

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参数roster中包含的每个Person实例是否满足Predicate参数tester中指定的条件,如果Person实例满足tester指定的条件,则在Person实例上调用printPersron方法。

你可以指定一个不同的操作来执行那些满足tester指定的条件的Person实例,而不是调用printPerson方法,你可以使用lambda表达式指定此操作。假设你想要一个类似于printPerson的lambda表达式,它接受一个参数(Person类型的对象)并返回void,请记住,要使用lambda表达式,你需要实现一个功能性接口。在这种情况下,你需要一个包含抽象方法的功能性接口,该方法可以接受一个Person类型的参数并返回voidConsumer<T>接口包含void accept(T t)方法,它具有这些特性,以下方法将调用p.printPerson()替换为调用方法acceptConsumer<Person>实例:

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

重新考虑方法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对象的源,请注意,集合rosterList类型的集合,也是Iterable类型的对象。
  2. 过滤与Predicate对象tester匹配的对象,在此示例中,Predicate对象是一个lambda表达式,指定哪些成员有资格获得选择性服务。
  3. 将每个筛选对象映射到Function对象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<ActionEvent>类型的对象,EventHandler<ActionEvent>接口只包含一个方法,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表达式,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) {
            
            // 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的声明中用参数x代替y,则编译器会生成错误:

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

编译器生成错误“variable x is already defined in method methodInFirstLevel(int)”,因为lambda表达式不会引入新的作用域级别,因此,你可以直接访问封闭范围的字段、方法和局部变量。例如,lambda表达式直接访问methodInFirstLevel方法的参数x,要访问封闭类中的变量,请使用关键字this,在此示例中,this.x引用成员变量FirstLevel.x

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

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

由于这个赋值语句,变量FirstLevel.x不再是final,因此,Java编译器生成类似于“local variables referenced from a lambda expression must be final or effectively final”的错误消息,其中lambda表达式myConsumer尝试访问FirstLevel.x变量:

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

目标类型

你如何确定lambda表达式的类型?回想一下lambda表达式,它选择了男性和年龄在18到25岁之间的成员:

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

这个lambda表达式用于以下两种方法:

  • 方法3:在局部类中指定搜索条件代码中的public static void printPersons(List<Person> roster, CheckPerson tester)
  • 方法6:将标准功能性接口与Lambda表达式一起使用中的public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)

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

  • 变量声明
  • 赋值
  • Return语句
  • 数组初始化
  • 方法或构造函数参数
  • Lambda表达体
  • 条件表达式,?:
  • 转换表达式

目标类型和方法参数

对于方法参数,Java编译器使用另外两种语言功能确定目标类型:重载决策和类型参数推断。

考虑以下两个功能性接口(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表达式进行序列化。

方法引用

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

再次考虑Person类:

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

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

    public int getAge() {
        // ...
    }
    
    public Calendar 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());

此调用排序的方法签名如下:

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<Person>.compare复制的,它是(Person, Person)
  • 它的主体调用方法Person.compareByAge

各种方法引用

有四种方法引用:

种类 示例
引用静态方法 ContainingClass::staticMethodName
引用特定对象的实例方法 containingObject::instanceMethodName
引用特定类型的任意对象的实例方法 ContainingType::methodName
引用构造函数 ClassName::new
引用静态方法

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

引用特定对象的实例方法

以下是对特定对象的实例方法的引用示例:

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)

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

以下是对特定类型的任意对象的实例方法的引用示例:

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

方法引用String::compareToIgnoreCase的等效lambda表达式将具有形式参数列表(String a, String b),其中ab是用于更好地描述此示例的任意名称,方法引用将调用方法a.compareToIgnoreCase(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表达式调用方法transferElements,如下所示:

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

何时使用嵌套类、局部类、匿名类和Lambda表达式

如嵌套类一节所述,嵌套类使你能够对仅在一个地方使用的类进行逻辑分组,增加封装的使用,并创建更易读和可维护的代码。局部类、匿名类和lambda表达式也赋予这些优点,但是,它们旨在用于更具体的情况:

  • 局部类:如果你需要创建一个类的多个实例,访问其构造函数或引入新的命名类型(例如,你需要稍后调用其他方法),请使用它。
  • 匿名类:如果需要声明字段或其他方法,请使用它。
  • lambda表达式:

    • 如果要封装希望传递给其他代码的单个行为单元,请使用它,例如,如果要在集合的每个元素上执行某个操作,或者在流程完成时,或者在流程遇到错误时,你将使用lambda表达式。
    • 如果你需要功能性接口的简单实例并且不应用前述条件(例如,你不需要构造函数、命名类型、字段或其他方法),请使用它。
  • 嵌套类:如果你的要求与局部类的要求类似,则需要使用它,你希望更广泛地使用该类型,并且不需要访问局部变量或方法参数。

    • 如果需要访问封闭实例的非公共字段和方法,请使用非静态嵌套类(或内部类),如果不需要此访问权限,请使用静态嵌套类。

上一篇:匿名类
下一篇:枚举类型

博弈
2.5k 声望1.5k 粉丝

态度决定一切