Java 进阶 -- Lambda 表达式

news2024/12/26 23:41:23

Lambda Expressions

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

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

本节涵盖以下主题:

1、Lambda表达式的理想用例

函数式接口总结:

Consumer<T>

// 表示接受单个输入参数且不返回结果的操作。与大多数其他功能接口不同,Consumer 期望通过副作用来操作。
// 这是一个函数式接口,其函数式方法是accept(Object)。

@FunctionalInterface
public interface Consumer<T> {
	// 对给定参数执行此操作。
	void accept(T t);
	
	// 返回一个组合的Consumer,该Consumer按顺序执行此操作,然后执行after操作。
	// 如果执行任何操作都会抛出异常,则将其传递给组合操作的调用者。如果执行此操作会引发异常,
	// 则不会执行after操作。
	default Consumer<T> andThen(Consumer<? super T> after) {}
}

Function<T, R>

// 表示接受一个参数并产生结果的函数。

@FunctionalInterface
public interface Function<T, R> {
	// 将此函数应用于给定参数。
	R apply(T t);
	
	// 返回一个组合函数,该函数首先将before函数应用于其输入,然后将此Function应用于结果。
	// 如果对任何一个函数的求值引发异常,则将其传递给组合函数的调用者。
	default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {}
		
	// 返回一个组合函数,该函数首先将此函数应用于其输入,然后将after函数应用于结果。
	// 如果对任何一个函数的求值引发异常,则将其传递给组合函数的调用者。
	default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {}
	
	// 返回一个总是返回其输入参数的function
	static <T> Function<T, T> identity() {}
}

BiFunction<T, U, R>

表示接受两个参数并产生结果(result)的函数。这是函数的二元特化。

这是一个功能接口,其功能方法是apply(Object, Object)


public interface BiFunction<T, U, R> {
	// 将此函数应用于给定的参数。
	// t – the first function argument
	// u – the second function argument
	// Returns: the function result
	R apply(T t, U u);
}

Comparator<T>

一个比较函数,它对一些对象集合施加一个总排序。比较器可以传递给排序方法(比如Collections.sort Arrays.sort)以允许对排序顺序进行精确控制。比较器还可用于控制某些数据结构的顺序(例如 sorted sets sorted maps),或为没有自然排序的对象集合提供排序。
比较器c对一个set S施加的排序,当且仅当c.compare(e1, e2)==0 对于S中的每个e1e2具有与e1.equals(e2)相同的布尔值时,称为与equals一致。

public interface Comparator<T> {
	// 比较它的两个参数的顺序。当第一个参数小于、等于或大于第
	// 二个参数时,返回负整数、零或正整数。
	// 在前面的描述中,符号`sgn(expression)`表示数学上的signum函数,
	// 它被定义为根据表达式的值是负、零还是正返回-1、0或1中的一个。
	int compare(T o1, T o2);
}

Supplier<T>

表示结果的提供者。
不要求每次调用供应商时都返回一个新的或不同的结果。
这是一个函数式接口,其函数式方法是 get()

public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

ToIntFunction<T>

表示产生int值结果的函数。这是Function的产生int的原语专门化。

这是一个函数式接口,其函数式方法是applyAsInt(Object)

@FunctionalInterface
public interface ToIntFunction<T> {

    /**
     * Applies this function to the given argument.
     *
     * @param value the function argument
     * @return the function result
     */
    int applyAsInt(T value);
}

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

在这里插入图片描述
假设这个社交网络应用程序的成员由以下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。集合是将多个元素组合成单个单元的对象。集合用于存储、检索、操作和通信聚合数据。有关集合的详细信息,请参阅Collections 跟踪。

这种方法可能会使您的应用程序变得脆弱,即应用程序可能因为引入了更新(例如更新的数据类型)而无法正常工作。假设您升级了应用程序并更改了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返回一个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接口是一个函数接口(functional interface)。函数接口是只包含一个抽象方法的接口。(函数接口可能包含一个或多个默认方法或静态方法。)因为函数式接口只包含一个抽象方法,所以在实现该方法时可以省略该方法的名称。要做到这一点,您可以使用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:在本地类中指定搜索条件代码以获得有资格获得选举服务的成员时调用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参数roster 中包含的每个Person实例是否满足Predicate 参数tester中指定的标准。如果Person实例满足测试人员指定的标准,则在Person实例上调用printPerson方法。

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

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获取对象源。在本例中,它从集合花名册中获取Person对象的源。注意,集合花名册是List类型的集合,也是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));

下表将proceselement方法执行的每个操作与相应的聚合操作进行了映射:
在这里插入图片描述
操作filtermapforEach是聚合操作(aggregate operations)。聚合操作处理来自流(stream)的元素,而不是直接来自集合(这就是为什么在本例中调用的第一个方法是stream)。流(stream)是元素的序列。与集合不同,它不是存储元素的数据结构。相反,流通过管道携带来自源(如集合)的值。管道(pipeline)是流操作的序列,在本例中是filter- map-forEach。此外,聚合操作通常接受Lambda表达式作为参数,使您能够自定义它们的行为方式。

有关聚合操作的更详细讨论,请参阅聚合操作一课。

1.1 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!")
        );

1.2 Lambda表达式的语法

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

1. 用逗号分隔的、用圆括号括起来的形式参数列表。

CheckPerson.test方法包含一个参数p,它表示Person类的一个实例。
注意:可以省略lambda表达式中参数的数据类型。此外,如果只有一个参数,可以省略括号。例如,下面的lambda表达式也是有效的:

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

2. 箭头标记->

3. 主体,由单个表达式或语句块组成。

这个例子使用了下面的表达式:

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

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

像局部类和匿名类一样,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);
    }
}

该示例生成以下输出:

在这里插入图片描述
如果你在lambda表达式myConsumer的声明中用参数x代替y,那么编译器会生成一个错误:

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

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

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

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

由于这个赋值语句,变量z实际上不再是final了。结果,Java编译器生成类似于“在封闭作用域中定义的局部变量z必须是final或有效final”的错误消息。

1.4 目标类型

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

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

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

  • public static void printPersons(List<Person> roster, CheckPerson tester) (方法3:在局部类中指定搜索标准代码)
  • public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) (方法6:使用标准函数接口和Lambda表达式)

当Java运行时调用printPersons方法时,它期望的数据类型是CheckPerson,因此lambda表达式是这种类型。但是,当Java运行时调用printPersonsWithPredicate方法时,它期望的数据类型是Predicate<Person>,因此lambda表达式是这种类型。这些方法所期望的数据类型被称为目标类型(target type)。为了确定一个lambda表达式的类型,Java编译器使用对象类型的上下文或情况,其中显示了lambda表达式。接下来,您只能在Java编译器可以确定目标类型的情况下使用lambda表达式:

  • Variable declarations

  • Assignments

  • Return statements

  • Array initializers

  • Method or constructor arguments

  • Lambda expression bodies

  • Conditional expressions, ?:

  • Cast expressions

目标类型和方法参数

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

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

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>

1.5 序列化

如果lambda表达式的目标类型及其捕获的参数是可序列化的,则可以序列化该表达式。但是,与内部类一样,强烈不鼓励对lambda表达式进行序列化

2. 方法引用

可以使用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<Person>.compare,即(Person, Person)
  • 它的主体调用Person.compareByAge方法

方法引用的种类:

方法引用有四种:

在这里插入图片描述
下面的示例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::compareByAgeMethodReferencesExamples::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表达式调用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);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/615003.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spark RDD计算总分与平均分

文章目录 一&#xff0c;提出任务二&#xff0c;实现思路三&#xff0c;准备工作1、启动HDFS服务2、启动Spark服务3、在本地创建成绩文件4、将成绩文件上传到HDFS 四&#xff0c;完成任务1、在Spark Shell里完成任务&#xff08;1&#xff09;读取成绩文件&#xff0c;生成RDD&…

从Java BIO到NIO再到多路复用,看这篇就够了

从一次优化说起 近期优化了一个老的网关系统&#xff0c;在dubbo调用接口rt1000ms时吞吐量提升了25倍&#xff0c;而线程数却由64改到8。其他的优化手段不做展开&#xff0c;比较有意思的是为什么线程数减少&#xff0c;吞吐量却可以大幅提升&#xff1f;这就得从IO模型说起&a…

消息队列kafka使用技巧和常见问题

目录 【消息队列概述】 【kafka】 消息丢失问题 消息重复问题 消费顺序问题 消息积压问题 kafka集群部署 【消息队列概述】 消息队列主要解决应用耦合、异步消息、流量削锋等问题&#xff0c;是大型分布式系统不可缺少的中间件。消息生产者 只管把消息发布到 MQ 中而不…

【CMake 入门与进阶(4)】 CMakeLists.txt 语法规则基础及部分常用指令-续(附使用代码)

由于篇幅问题本篇接着上文继续介绍 CMakeLists.txt 语法规则基础及常用指令。 aux_source_directory aux_source_directory 命令会查找目录中的所有源文件&#xff0c;其命令定义如下&#xff1a; aux_source_directory(<dir> <variable>)从指定的目录中查找所有…

开发者工具调试

Console控制台 F12打开控制台 选择其他tab面板时&#xff0c;ESC打开Console面板enter直接执行Console的代码&#xff0c;shiftEnter输入多行代码 Source面板 左键单机行号设置断点&#xff0c;或在代码中添加debugger;右键单机行号设置条件断点&#xff08;条件表达式为tr…

PowerShell install 一键部署mariadb10.11

mariadb MariaDB数据库管理系统是MySQL的一个分支&#xff0c;主要由开源社区在维护&#xff0c;采用GPL授权许可 MariaDB的目的是完全兼容MySQL&#xff0c;包括API和命令行&#xff0c;使之能轻松成为MySQL的代替品。在存储引擎方面&#xff0c;使用XtraDB来代替MySQL的Inno…

ChatGPT 国内镜像网站大全(含GPT-4.0版本)之什么年代还在写传统文章。

前言&#xff1a; 临近期末&#xff0c;大量水课的节课作业都是论文&#xff0c;一篇就是几千字&#xff0c;这对于还要复习专业课的我们可以说是压力巨大&#xff1a;心理健康论文&#xff0c;安全教育论文&#xff0c;大学语文论文&#xff0c;书法赏析论文&#xff0c;劳动…

小议C++函数签名与模板返回类型

题记&#xff1a;什么事情都要追问一个为什么&#xff0c;真正理解了为什么&#xff0c;才能活学活用。 代码1 下面的代码能编译通过吗&#xff1f; #include <stdio.h> #include <stdlib.h>class X { public:int *get() { return new int(); }double *get() { r…

MATLAB矩阵的分解函数与案例举例

系列文章目录 MATLAB当中线性方程组、不定方程组、奇异方程组、超定方程组的介绍 MATLAB语句实现方阵性质的验证 MATLAB绘图函数的相关介绍——海底测量、二维与三维图形绘制​​​​​​ MATLAB求函数极限的简单介绍 文章目录 前言 1. 奇异值分解&#xff08;SVD&#x…

C++类和对象-4

在上篇C类和对象的博客中&#xff0c;我们讲述了析构函数、拷贝构造函数、浅拷贝和深拷贝的内容&#xff0c;我们紧接上文&#xff0c;开始讲述接下来的文章。 目录 1.this指针 1.1引入 1.2内容 1.3特征 1.4用法 2.静态成员 2.1内容 2.2静态数据成员 2.3静态成员函数…

Vue.js 中的国际化支持是什么?如何进行国际化支持?

Vue.js 中的国际化支持是什么&#xff1f;如何进行国际化支持&#xff1f; Vue.js 是一款流行的前端框架&#xff0c;它提供了许多方便的工具和 API&#xff0c;用于构建交互式的用户界面。其中&#xff0c;国际化支持是 Vue.js 中重要的一部分&#xff0c;它可以让我们轻松地…

如何强制删除文件夹?这样操作就能搞定!

案例&#xff1a;我想删掉一些没有用的文件夹&#xff0c;释放一些电脑内存&#xff0c;但是我发现&#xff0c;有些文件夹并不能直接被删除。怎样才能删除这些文件夹&#xff1f;有没有小伙伴有解决的办法。 在使用电脑过程中&#xff0c;我们可能会遇到一些无法正常删除文件夹…

空间计算时代来临:苹果Vision Pro震撼上市,探索真实与虚拟的新边界

目录 前言Vision Pro的外观设计Vision Pro的交互方式Vision Pro 硬件配置Vision Pro 上市时间及销售价格Vision Pro与传统XR设备不同点总结其它资料下载 前言 苹果公司在2023年6月6日的WWDC23主题演讲中正式发布了传闻已久的头显产品——Vision Pro。WWDC&#xff0c;全称为“…

LLM Accelerator:使用参考文本无损加速大语言模型推理

编者按&#xff1a;如今&#xff0c;基础大模型正在诸多应用中发挥着日益重要的作用。大多数大语言模型的训练都是采取自回归的方式进行生成&#xff0c;虽然自回归模型生成的文本质量有所保证&#xff0c;但却导致了高昂的推理成本和长时间的延迟。由于大模型的参数量巨大、推…

被App包围 苹果Vision Pro将你推入空间“大屏”

2小时&#xff0c;这是2023年苹果开发者大会&#xff08;WWDC&#xff09;首日发布会的直播总时长&#xff0c;仅YouTube上&#xff0c;就有483.9万次观看。发布会开启时&#xff0c;北京时间是6月6日凌晨1点&#xff0c;众多科技博主串流直播了这场发布会。 苹果CEO蒂姆库克引…

3.2 继续完善的Vue.js响应式系统

前文提要&#xff1a; 3.0 响应式系统的设计与实现 3.1 一个稍微完善的Vue.js响应式系统 1、解决副作用函数的死循环问题 在解决了分支的切换的问题&#xff0c;此时还有一个代码死循环的问题&#xff0c;其这个死循环很容易触发&#xff0c;如下代码&#xff1a; const dat…

Netty Incubator Codec QUIC 0.0.41.Final 发布

导读Netty Incubator Codec QUIC 是一款基于 QUIC 协议的编解码器&#xff0c;为 Netty 提供了 QUIC 协议的支持。 近日&#xff0c;该团队发布了 0.0.41.Final 版本&#xff0c;这是一个错误修复版本&#xff0c;主要包括以下变化: 允许在派发前通过添加到读完队列来合并刷新…

嵌入式软件测试笔记3 | 嵌入式软件测试开发的多V模型

3 | 嵌入式软件测试开发的多V模型 1 简单的多V模型2 迭代与并行开发2.1 开发模型2.2 嵌入式开发过程的复杂性 3 多V模型中的测试活动3.1 测试活动和因素3.2 模型开发周期中与测试相关的元素分配3.3 原型开发周期中与测试相关的元素分配3.4 最终产品开发周期中与测试相关的元素分…

NineData x 华为云正式上线

6月5日&#xff0c;NineData 企业级 SQL 开发平台正式成为华为云“联营联运”商品。通过联营联运模式&#xff0c;双方将在产品、解决方案和生态等多个方面开展深度合作&#xff0c;共同提供高效、智能、安全的数据管理服务&#xff0c;帮助客户轻松构建一站式云端数据库管理平…

【随想录】一篇水文

前排许愿池: 我是一个没有梦想的咸鱼捏 自从知道成电优营了也不给offer之后 遂开始摆烂了(哈哈) 以及看了一下数据 好像前期存的资本够多的话 后面还是能混混的 however,已经快过去2/3了 前排致谢: 感谢好人一姐的助力 果然人是靠别人活着的 或者说伟人是站在巨人…