corejava11(6.2 Lambda表达式)

6.2 Lambda表达式

在下面的部分中,您将学习如何使用lambda表达式以简洁的语法定义代码块,以及如何编写使用lambda表达式的代码。

6.2.1 为什么使用lambda表达式?

lambda表达式是可以传递的代码块,以便以后执行一次或多次。在进入语法(甚至好奇的名字)之前,让我们后退一步,观察我们在Java中使用了哪些代码块。

在第310页的第6.1.7节“接口和回调”中,您看到了如何在时间间隔内完成工作。将工作放入ActionListener的actionPerformed方法中:

class Worker implements ActionListener
{
    public void actionPerformed(ActionEvent event)
    {
        // do some work
    }
}

然后,当您想要重复执行这个代码时,您可以构造一个Worker类的实例。然后将实例提交给Timer对象。

关键点是actionPerformed方法包含了稍后要执行的代码。

或者考虑使用自定义比较器进行排序。如果要按长度而不是默认字典顺序对字符串排序,可以将Comparator对象传递给sort方法:

class LengthComparator implements Comparator<String>
{
    public int compare(String first, String second)
    {
        return first.length() - second.length();
    }
}
. . .
Arrays.sort(strings, new LengthComparator());

compare方法不会立即调用。相反,sort方法不断调用compare方法,如果元素顺序不对,则重新排列元素,直到数组排序完毕。你给sort方法一段代码来比较元素,代码被集成到排序逻辑的其余部分,你可能不想重新实现。

这两个例子都有一些共同之处。一块代码被传递了——一个计时器或sort方法。稍后调用了该代码块。

到目前为止,传递一块代码在Java中并不容易。你不能只传递代码块。Java是面向对象语言,因此必须构造属于具有需要代码的方法的类的对象。

在其他语言中,可以直接使用代码块。Java设计者长期以来一直拒绝添加这个特性。毕竟,Java的强大之处在于它的简单性和一致性。如果一种语言包含了每一个产生稍微简洁的代码的特性,它可能会变成一个无法修复的混乱。然而,在这些其他语言中,生成线程或注册按钮单击处理程序不仅更容易;它们的大量API更简单、更一致、更强大。在Java中,人们可以编写类似的API来获取实现特定接口的类的对象,但是这样的API将是令人不快的使用。

一段时间以来,问题不在于是否要增加Java的功能性编程,而在于如何做到这一点。在设计出适合Java的设计之前,需要几年的实验。在下一节中,您将看到如何在Java中使用代码块。

6.2.2 lambda表达式的语法

再次考虑前面部分中的排序示例。我们传递的代码检查一个字符串是否比另一个短。我们计算

first.length() - second.length()

first和second是什么?它们都是字符串。Java是一种强类型语言,我们还必须指定它:

(String first, String second)
    -> first.length() - second.length()

您刚刚看到了第一个lambda表达式。这样的表达式只是一个代码块,以及必须传递给代码的任何变量的规范。

为什么叫这个名字?许多年前,在没有计算机之前,逻辑学家阿隆佐·丘奇(Alonzo Church)就想将数学函数有效计算的意义正式化。(奇怪的是,有已知存在的函数,但没有人知道如何计算它们的值。)他使用希腊字母lambda(λ)标记参数。如果他知道JavaAPI,他会写

λfirst.λsecond.first.length() - second.length()

注意

为什么是字母λ ?Church的字母表上其他字母用完了吗?事实上,著名的数学原理使用^重音来表示自由变量,这启发了Church使用大写的lambda∧作为参数。但最后,他换成了小写。此后,带有参数变量的表达式被称为lambda表达式。

您刚刚在Java中看到了lambda表达式的一种形式:参数、->箭头和表达式。如果代码执行的计算不适合于单个表达式,那么编写它就像编写方法一样:用括起来,并使用显式返回语句。例如,

(String first, String second) ->
    {
        if (first.length() < second.length()) return -1;
        else if (first.length() > second.length()) return 1;
        else return 0;
    }

如果lambda表达式没有参数,则仍然提供空括号,就像无参数方法一样:

() -> { for (int i = 100; i >= 0; i--)
System.out.println(i); }

如果可以推断lambda表达式的参数类型,则可以省略它们。例如,

Comparator<String> comp
    = (first, second) // same as (String first, String second)
        -> first.length() - second.length();

在这里,编译器可以推断第一个和第二个必须是字符串,因为lambda表达式被分配给字符串比较器。(我们将在下一节中更详细地了解这项任务。)

如果一个方法只有一个具有推断类型的参数,则甚至可以省略括号:

ActionListener listener = event ->
    System.out.println("The time is "
        + Instant.ofEpochMilli(event.getWhen()));
    // instead of (event) -> . . . or (ActionEvent event) -> . . .

从不指定lambda表达式的结果类型。它总是从上下文推断出来的。例如,表达式

(String first, String second) -> first.length() - second.length()

不能在需要int类型的结果的上下文中使用。

注意

lambda表达式在某些分支中返回值而在其他分支中不返回值是非法的。例如,`(int x) -> { if (x >= 0) return 1; }是非法的。

清单6.6中的程序展示了如何将lambda表达式用于比较器和动作侦听器。

清单6.6 lambda/LambdaTest.java

package lambda;
 
import java.util.*;
 
import javax.swing.*;
import javax.swing.Timer;
 
/**
* This program demonstrates the use of lambda expressions.
* @version 1.0 2015-05-12
* @author Cay Horstmann
*/
public class LambdaTest
{
   public static void main(String[] args)
   {
      var planets = new String[] { "Mercury", "Venus", "Earth", "Mars",
         "Jupiter", "Saturn", "Uranus", "Neptune" };
      System.out.println(Arrays.toString(planets));
      System.out.println("Sorted in dictionary order:");
      Arrays.sort(planets);
      System.out.println(Arrays.toString(planets));
      System.out.println("Sorted by length:");
      Arrays.sort(planets, (first, second) -> first.length() - second.length());
      System.out.println(Arrays.toString(planets));
 
      var timer = new Timer(1000, event ->
         System.out.println("The time is " + new Date()));
      timer.start();  
 
      // keep program running until user selects "OK"
      JOptionPane.showMessageDialog(null, "Quit program?");
      System.exit(0);        
   }
}

6.2.3 功能接口

正如我们所讨论的,Java中存在许多封装代码块的接口,例如ActionListener或Comparator。lambda与这些接口兼容。

只要需要具有单个抽象方法的接口对象,就可以提供lambda表达式。这种接口称为功能接口。

注意

您可能想知道为什么一个函数接口必须有一个抽象方法。不是所有的方法都是抽象的吗?

实际上,接口总是可以从Object类(如toString或clone)重新声明方法,并且这些声明不会使方法抽象化。(JavaAPI中的一些接口重新声明Object方法以附加javadoc注释。例如,请查看Comparator API。)更重要的是,正如您在第307页第6.1.5节“默认方法”中看到的,接口可以声明非抽象方法。

要演示到函数接口的转换,请考虑Arrays.sort方法。它的第二个参数需要一个Comparator实例,一个带有单个方法的接口。只需提供一个lambda:

Arrays.sort(words,
    (first, second) -> first.length() - second.length());

在幕后,Arrays.sort方法接收某个类的对象,该类实现Comparator<String>。对该对象调用compare方法将执行lambda表达式的主体。这些对象和类的管理完全依赖于实现,并且比使用传统的内部类更有效。最好将lambda表达式看作一个函数,而不是一个对象,并接受它可以传递给函数接口。

这种到接口的转换使lambda表达式如此引人注目。语法简短。下面是另一个例子:

var timer = new Timer(1000, event ->
    {
        System.out.println("At the tone, the time is "
            + Instant.ofEpochMilli(event.getWhen()));
        Toolkit.getDefaultToolkit().beep();
    });

这比使用实现ActionListener接口的类的替代方法更容易阅读。

事实上,转换到函数接口是在Java中使用lambda表达式所能做的唯一事情。在其他支持函数文本的编程语言中,可以声明函数类型,如(String, String) -> int,声明这些类型的变量,并使用变量保存函数表达式。然而,Java设计者决定使用熟悉的接口概念,而不是将函数类型添加到语言中。

注意

甚至不能将lambda表达式赋给类型为Object——Object不是函数接口的变量。

Java API定义了很多通用函数接口在java.util.function包。其中一个接口BiFuction<T, U, R>描述参数类型为T和U的函数,返回类型为R。您可以将字符串比较lambda保存在该类型的变量中:

BiFunction<String, String, Integer> comp
    = (first, second) -> first.length() - second.length();

但是,这对排序没有帮助。没有需要BiFunctionArray.sort方法。如果您以前使用过函数式编程语言,您可能会发现这很奇怪。但是对于Java程序员来说,这是很自然的。Comparator之类的接口有特定的用途,而不仅仅是具有给定参数和返回类型的方法。当您想对lambda表达式执行某些操作时,您仍然需要记住表达式的用途,并为它提供一个特定的函数接口。

java.util.function包中特别有用的接口是Predicate

public interface Predicate<T>
{
    boolean test(T t);
    // additional default and static methods
}

ArrayList类有一个removeIf方法,该方法的参数是Predicate。它是专门为传递lambda表达式而设计的。例如,以下语句从数组列表中删除所有空值:

list.removeIf(e -> e == null);

另一个有用的功能接口是Supplier<T>

public interface Supplier<T>
{
    T get();
}

一个supplier没有参数,在调用它时会生成类型T的值。Suppliers被用于惰性计算。例如,考虑调用

LocalDate hireDay = Objects.requireNonNullOrElse(day,
    new LocalDate(1970, 1, 1));

这不是最佳的。我们希望这一天很少为空,因此我们只希望在必要时构造默认的LocalDate。通过使用supplier,我们可以推迟计算:

LocalDate hireDay = Objects.requireNonNullOrElseGet(day,
    () -> new LocalDate(1970, 1, 1));

requireNonNullOrElseGet方法仅在需要值时调用supplier。

6.2.4 方法引用

有时,lambda表达式涉及单个方法。例如,假设您只想在计时器事件发生时打印事件对象。当然,你可以调用

var timer = new Timer(1000, event ->
    System.out.println(event));

如果您能将println方法传递给Timer构造函数就更好了。以下是您的方法:

var timer = new Timer(1000, System.out::println);

表达式System.out::println是一个方法引用。它指示编译器生成函数接口的实例,重写接口的单个抽象方法以调用给定的方法。在本例中,将生成一个ActionListener,其actionPerformed(ActionEvent e)方法将调用System.out.println(e)

注意

与lambda表达式类似,方法引用不是对象。当分配给类型为函数接口的变量时,它会产生一个对象。

注意

PrintStream类中有十个重载的println方法(其中System.out是一个实例)。编译器需要根据上下文确定要使用哪一个。在我们的示例中,方法引用System.out::println必须转换为带有方法的ActionListener实例

void actionPerformed(ActionEvent e)

println(Object x)方法是从十个重载的println方法中选择的,因为ObjectActionEvent的最佳匹配项。调用actionPerformed方法时,将打印事件对象。

现在假设我们将相同的方法引用分配给不同的函数接口:

Runnable task = System.out::println;

Runnable函数接口有一个没有参数的抽象方法

void run()

在这种情况下,将选择不带参数的println()方法。调用task.run()将在System.out中打印一行空白。

作为另一个示例,假设您希望对字符串进行排序,而不考虑字母大小写。可以传递此方法表达式:

Arrays.sort(strings, String::compareToIgnoreCase)

从这些示例中可以看到,::运算符将方法名与对象或类的名称分隔开。有三种变体:

  1. object::instanceMethod
  2. Class::instanceMethod
  3. Class::staticMethod

在第一个变量中,方法引用等效于lambda表达式,该表达式的参数将传递给该方法。对于System.out::println,对象是System.out,方法表达式相当于x -> System.out.println(x)

在第二个变量中,第一个参数成为方法的隐式参数。例如,String::compareToIgnoreCase(x, y) -> x.compareToIgnoreCase(y)相同。

在第三个变量中,所有参数都传递给静态方法:Math::pow等价于(x, y) -> Math.pow(x, y)

表6.1为您介绍了其他示例。

表6.1 方法参考示例

方法参考 等效lambda表达式 注释
separator::equals x -> separator.equals(x) 这是一个带有对象和实例方法的方法表达式。lambda参数作为方法的显式参数传递。
String::trim x -> x.trim() 这是一个带有类和实例方法的方法表达式。lambda参数变为隐式参数。
String::concat (x, y) -> x.concat(y) 同样,我们有一个实例方法,但是这次,它有一个显式参数。和以前一样,第一个lambda参数成为隐式参数,其余的参数则传递给该方法。
Integer::valueOf x -> Integer::valueOf(x) 这是带有静态方法的方法表达式。lambda参数传递给静态方法。
Integer::sum (x, y) -> Integer::sum(x, y) 这是另一个静态方法,但这次有两个参数。两个lambda参数都传递给静态方法。integer.sum方法是专门创建的,用作方法引用。作为一个lambda,你可以只写(x,y)->x+y。
Integer::new x -> new Integer(x) 这是一个构造引用,见第6.2.5节。lambda参数将传递给构造函数。
Integer[]::new n -> new Integer[n] 这是一个数组构造函数引用 —— 参见第6.2.5节。lambda参数是数组长度。

请注意,只有当lambda表达式的主体调用单个方法而不执行任何其他操作时,才能将lambda表达式重写为方法引用。考虑lambda表达式

s -> s.length() == 0

只有一个方法调用。但是这里也有比较,所以这里不能使用方法引用。

注意

当存在多个同名的重载方法时,编译器将尝试从上下文中找到您所指的方法。例如,Math.max方法有两个版本,一个用于integer,一个用于double。选择哪一个取决于将Math::max转换到的函数接口的方法参数。与lambda表达式一样,方法引用并不孤立地存在。它们总是被转换成功能接口的实例。

注意

有时,API包含专门用作方法引用的方法。例如,Objects类有一个方法isNull来测试对象引用是否为空。乍一看,这似乎不太有用,因为测试obj==null比对象更容易读取。但可以将方法引用传递给带有Predicate参数的任何方法。例如,要从列表中删除所有null引用,可以调用

list.removeIf(Objects::isNull);
// A bit easier to read than list.removeIf(e -
> e == null);

注意

带有对象的方法引用与其等效lambda表达式之间存在微小差异。考虑一个方法引用,如separator::equals。如果separatornull,则形成separator::equals将立即引发NullPointerException。lambda表达式x -> separator.equals(x)仅在调用时引发NullPointerException

可以在方法引用中捕获this参数。例如,this::equalsx -> this.equals(x)相同。使用super也是有效的。方法表达式

super::instanceMethod

this用作目标并调用给定方法的超类版本。下面是一个人工例子,展示了力学:

class Greeter
{
    public void greet(ActionEvent event)
    {
        System.out.println("Hello, the time is "
            + Instant.ofEpochMilli(event.getWhen()));
    }
}
class RepeatedGreeter extends Greeter
{
    public void greet(ActionEvent event)
    {
        var timer = new Timer(1000, super::greet);
        timer.start();
    }
}

RepeatedGreeter.greet方法启动时,将构造一个计时器,在每个计时器标记上执行super::greet方法。

6.2.5 构造函数引用

构造函数引用与方法引用一样,只是方法的名称是new。例如,Person::new是对Person构造函数的引用。哪个构造函数?这取决于上下文。假设您有一个字符串列表。然后,通过对每个字符串调用构造函数,通过以下调用,可以将其转换为一个Person对象数组:

ArrayList<String> names = . . .;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());

我们将在第二卷的第一章讨论streammapcollect方法的细节。现在,重要的是map方法调用每个list元素的Person(String)构造函数。如果有多个Person构造器,编译器会选择带有String参数的构造器,因为它从上下文推断出用字符串调用构造器。

可以使用数组类型来形成构造函数引用。例如,int[]::new是一个带有一个参数的构造函数引用:数组的长度。它等效于lambda表达式x -> new int[x]

数组构造函数引用有助于克服Java的局限性。无法构造泛型类型T的数组。表达式new T[n]是一个错误,因为它将被擦除为new Object[n]。这对库的作者来说是个问题。例如,假设我们想要一个Person对象数组。Stream接口具有返回Object数组的toArray方法:

Object[] people = stream.toArray();

但这并不令人满意。用户需要一个对Person的引用数组,而不是对Object的引用。流库用构造函数引用解决了这个问题。传递Person[]::newtoArray方法:

Person[] people = stream.toArray(Person[]::new);

toArray方法调用this构造函数以获取正确类型的数组。然后它填充并返回数组。

6.2.6 变量范围

通常,您希望能够从lambda表达式中的封闭方法或类访问变量。考虑这个例子:

public static void repeatMessage(String text, int delay)
{
    ActionListener listener = event ->
        {
            System.out.println(text);
            Toolkit.getDefaultToolkit().beep();
        };
    new Timer(delay, listener).start();
}

考虑一个调用

repeatMessage("Hello", 1000); // prints Hello every 1,000 milliseconds

现在看看lambda表达式中的变量text。注意,lambda表达式中没有定义此变量。相反,它是repeatMessage方法的参数变量。

如果你考虑一下,这里会发生一些不明显的事情。lambda表达式的代码可能在对repeatMessage的调用返回并且参数变量消失后长时间运行。text变量如何保持?

为了理解正在发生的事情,我们需要改进对lambda表达式的理解。lambda表达式有三个成分:

  1. 一块代码
  2. 参数
  3. 自由变量的值——即不是参数且未在代码中定义的变量

在我们的示例中,lambda表达式有一个自由变量text。在我们的例子中,表示lambda表达式的数据结构必须存储自由变量的值,即字符串“Hello”。我们说这些值是由lambda表达式捕获的。(这是如何实现的一个实现细节。例如,可以使用单个方法将lambda表达式转换为对象,以便将自由变量的值复制到该对象的实例变量中。)

注意

技术术语代码块和自由变量是闭包。如果有人幸灾乐祸,他们的语言有闭包,放心,Java也有。在Java中,lambda表达式是闭包。

正如您所看到的,lambda表达式可以捕获封闭范围内变量的值。在Java中,为了确保捕获的值是明确定义的,存在一个重要的限制。在lambda表达式中,只能引用值不变的变量。例如,以下内容是非法的:

public static void countDown(int start, int delay)
{
    ActionListener listener = event ->
        {
            start--; // ERROR: Can't mutate captured variable
            System.out.println(start);
        };
    new Timer(delay, listener).start();
}

这种限制是有原因的。当同时执行多个操作时,lambda表达式中的变量可变是不安全的。这不会发生在我们迄今为止看到的各种行动中,但一般来说,这是一个严重的问题。有关这个重要问题的更多信息,请参阅第12章。

在lambda表达式中,引用在外部变异的变量也是非法的。例如,以下内容是非法的:

public static void repeat(String text, int count)
{
    for (int i = 1; i <= count; i++)
    {
        ActionListener listener = event ->
            {
                System.out.println(i + ": " + text);
                // ERROR: Cannot refer to changing i
            };
        new Timer(1000, listener).start();
    }
}

规则是lambda表达式中捕获的任何变量都必须是有效的final。一个有效的final变量是一个变量,它在初始化后永远不会被赋予新的值。在我们的例子中,text总是引用同一个String对象,捕获它是可以的。然而,i的值是变异的,因此i不能被捕获。

lambda表达式的主体与嵌套块具有相同的作用域。同样的名称冲突和shadowing规则也适用。在lambda中声明与局部变量同名的参数或局部变量是非法的。

Path first = Path.of("/usr/bin");
Comparator<String> comp
    = (first, second) -> first.length() - second.length();
    // ERROR: Variable first already defined

在方法内部,不能有两个同名的局部变量,因此也不能在lambda表达式中引入此类变量。

在lambda表达式中使用this关键字时,将引用创建lambda的方法的this参数。例如,考虑

public class Application
{
    public void init()
    {
        ActionListener listener = event ->
            {
                System.out.println(this.toString());
                . . .
            }
        . . .
    }
}

表达式this.toString()调用Application对象的toString方法,而不是ActionListener实例。在lambda表达式中使用this没有什么特别的。lambda表达式的作用域嵌套在init方法中,并且在该方法的任何地方this都具有相同的含义。

6.2.7 处理lambda表达式

到目前为止,您已经了解了如何生成lambda表达式并将其传递给需要函数接口的方法。现在让我们看看如何编写可以使用lambda表达式的方法。

使用lambda的目的是延迟执行。毕竟,如果您现在想执行一些代码,您可以这样做,而不需要将它包装在lambda中。以后执行代码有很多原因,例如:

  • 在单独的线程中运行代码
  • 多次运行代码
  • 在算法中的正确点运行代码(例如,排序中的比较操作)
  • 在发生事情时运行代码(单击按钮、数据到达等)
  • 仅在必要时运行代码

让我们来看一个简单的例子。假设您想重复一个操作n次。操作和计数将传递给重复方法:

repeat(10, () -> System.out.println("Hello, World!"));

为了接受lambda,我们需要选择(或者在极少数情况下提供)一个功能接口。表6.2列出了Java API中提供的最重要的功能接口。在这种情况下,我们可以使用可运行的接口:

接口6.2 通用功能接口

功能接口 参数类型 返回类型 抽象方法名称 描述
Runnable none void run 运行不带参数或返回值的操作
Supplier<T> none T get 提供T类型的值
Consumer<T> T void accept 使用T类型的值
BiConsumer<T, U> T, U void accept 使用类型T和U的值
Function<T, R> T R apply 参数类型为T的函数
BiFunction<T, U, R> T, U R apply 带有T和U类型参数的函数
UnaryOperator<T> T T apply 类型T上的一元运算符
BinaryOperator<T> T, T T apply 类型T上的二进制运算符
Predicate<T> T boolean test 布尔值函数
BiPredicate<T, U> T, U boolean test 带两个参数的布尔值函数
public static void repeat(int n, Runnable action)
{
	for (int i = 0; i < n; i++) action.run();
}

注意,lambda表达式的主体在调用action.run()时执行。

现在让我们让这个例子更复杂一些。我们想告诉在哪个迭代中它发生了。为此,我们需要选择一个函数接口,该接口具有一个带有int参数和void返回的方法。处理int值的标准接口是

public interface IntConsumer
{
	void accept(int value);
}

下面是repeat方法的改进版本:

public static void repeat(int n, IntConsumer action)
{
	for (int i = 0; i < n; i++) action.accept(i);
}

以下是你如何调用它:

repeat(10, i -> System.out.println("Countdown: " + (9 - i)));

表6.3列出了原语类型int、long和double的34个可用专门化。正如您将在第8章中看到的,使用这些专门化比使用通用接口更有效。出于这个原因,我在前一节的示例中使用了IntConsumer而不是Consumer<Integer>。

表6.3 基本类型p,q是int,long,double的函数接口;P,Q是Int,Long,Double

功能接口 参数类型 返回类型 抽象方法名称
BooleanSupplier none boolean getAsBoolean
PSupplier none p getAsP
PConsumer p void accept
ObjPConsumer<T> T, p void accept
PFunction<T> p T apply
PToQFunction p q applyAsQ
ToPFunction<T> T p applyAsP
ToPBiFunction<T,U> T, U p applyAsP
PUnaryOperator p p applyAsP
PBinaryOperator p, p p applyAsP
PPredicate p boolean test

提示

最好尽可能使用表6.2或6.3中的接口。例如,假设您编写了一个方法来处理符合特定条件的文件。有一个传统的接口java.io.Filefilter,但是最好使用标准Predicate<file>。唯一不这样做的原因是,如果您已经有了许多生成FileFilter实例的有用方法。

注意

大多数标准功能接口都有产生或组合函数的非抽象方法。例如,Predicate.isEqual(a)a::equals相同,但如果a为空,它也可以工作。组合谓词有默认方法and,or,negate方法。例如,Predicate.isEqual(a).or(Predicate.isEqual(b))x -> a.equals(x) || b.equals(x)相同。

注意

如果使用单个抽象方法设计自己的接口,则可以使用@FunctionalInterface注释对其进行标记。这有两个优点。如果不小心添加了另一个抽象方法,编译器将给出错误消息。JavaDoc页面包含一条语句,说明您的接口是一个功能性接口。

不需要使用注释。根据定义,任何具有单个抽象方法的接口都是功能接口。但是使用@FunctionalInterface注释是一个好主意。

6.2.8 关于比较器的更多信息

Comparator接口有许多方便的静态方法来创建比较器。这些方法用于lambda表达式或方法引用。

静态comparing方法采用“键提取器”函数,将类型T映射到可比较的类型(如字符串)。该函数应用于要比较的对象,然后对返回的键进行比较。例如,假设您有一个Person对象数组。以下是如何按名称对其进行排序:

Arrays.sort(people, Comparator.comparing(Person::getName));

这肯定比手工实现Comparator容易得多。此外,代码更清晰,因为很明显我们希望按名字比较人。

你可以使用链式比较器thenComparing方法。例如,

Arrays.sort(people, Comparator
	.comparing(Person::getLastName)
	.thenComparing(Person::getFirstName));

如果两个人姓氏相同,则使用第二个比较器。

这些方法有一些变化。您可以指定一个比较器,用于comparingthenComparing方法提取的键。例如,这里我们根据人名的长度对人们进行排序:

Arrays.sort(people, Comparator.comparing(Person::getName,
	(s, t) -> Integer.compare(s.length(), t.length())));

此外,comparing方法和thenComparing方法都有不同的变体,可以避免对int、long或double值进行装箱。产生上述操作的一种简单方法是

Arrays.sort(people, Comparator.comparingInt(p -> p.getName().length()));

If your key function can return null, you will like the nullsFirst and
nullsLast adapters. These static methods take an existing comparator and
modify it so that it doesn’t throw an exception when encountering null
values but ranks them as smaller or larger than regular values. For example,

suppose getMiddleName returns a null when a person has no middle
name. Then you can use
Comparator.comparing(Person::getMiddleName(),
Comparator.nullsFirst(. . .)).

如果键函数可以返回null,则需要nullsFirstnullsLast适配器。这些静态方法采用现有的比较器并对其进行修改,以便在遇到null值时不会引发异常,而是将其列为比常规值小或大的值。例如,假设一个人没有中间名时,getMiddleName返回一个null。然后可以使用Comparator.comparising(person::getMiddleName(),Comparator.nullsFirst(...))

在这种情况下,nullsFirst方法需要一个比较器,用于比较两个字符串。naturalOrder方法使任何实现了Comparable的类都具有可比性。Comparator.<String>naturalOrder()是我们需要的。下面是按可能为空的中间名排序的完整调用。我使用java.util.Comparator.*静态导入,使表达式更加清晰。注意,naturalOrder的类型是推断出来的。

Arrays.sort(people, comparing(Person::getMiddleName, nullsFirst(naturalOrder())));

静态的reverseOrder方法给出了与自然顺序相反的结果。要反转任何比较器,请使用reversed的实例方法。例如,naturalOrder().reversed()reverseOrder()相同。

猜你喜欢

转载自blog.csdn.net/nbda1121440/article/details/91299508