1. 概述

Java 8 中的一个重要新特性是**流(Stream API)**。流让我们能够方便地从各种来源处理元素,如数组或集合。

此外,通过使用Stream.collect()方法配合相应的收集器(Collectors),我们可以将元素重组到不同的数据结构中,比如Set(树集)MapList等。

在这个教程中,我们将探讨如何将流中的元素收集到一个TreeSet中。

2. 使用自然排序收集到TreeSet

简单来说,TreeSet是一个有序的Set。TreeSet中的元素根据它们的自然排序或提供的**比较器(Comparator)**进行排列。

我们首先来看看如何根据自然排序收集流元素。然后,我们将关注使用自定义比较器的情况。

为了简化,我们将使用单元测试断言来验证是否得到了预期的TreeSet结果。

2.1. 将字符串收集到TreeSet

由于String实现了Comparable接口,我们先以String为例,看看如何将其收集到TreeSet中:

String kotlin = "Kotlin";
String java = "Java";
String python = "Python";
String ruby = "Ruby";
TreeSet<String> myTreeSet = Stream.of(ruby, java, kotlin, python).collect(Collectors.toCollection(TreeSet::new));
assertThat(myTreeSet).containsExactly(java, kotlin, python, ruby);

如上述测试所示,要将流元素收集到TreeSet中,只需*将TreeSet的默认构造函数作为方法引用(method reference)或lambda表达式传递给Collectors.toCollection()*方法**。

如果执行这个测试,它会通过。

接下来,让我们看一个使用自定义类的类似例子。

2.2. 使用自然排序收集Players

首先,让我们看一下我们的Player类:

public class Player implements Comparable<Player> {
    private String name;
    private int age;
    private int numberOfPlayed;
    private int numberOfWins;

    public Player(String name, int age, int numberOfPlayed, int numberOfWins) {
        this.name = name;
        this.age = age;
        this.numberOfPlayed = numberOfPlayed;
        this.numberOfWins = numberOfWins;
    }

    @Override
    public int compareTo(Player o) {
        return Integer.compare(age, o.age);
    }

    // getters are omitted
}

如上所示,Player类实现了Comparable接口,并在compareTo()方法中定义了其自然排序:玩家的年龄。

现在,让我们创建几个Player实例:

/*                          name  |  age  | num of played | num of wins
                           --------------------------------------------- */
Player kai = new Player(   "Kai",     26,       28,            7);
Player eric = new Player(  "Eric",    28,       30,           11);
Player saajan = new Player("Saajan",  30,      100,           66);
Player kevin = new Player( "Kevin",   24,       50,           49);

由于我们将使用这四个玩家对象进行后续演示,我们以表格形式呈现代码,以便轻松查看每个玩家的属性值。

现在,让我们按自然顺序收集它们,并验证是否得到预期结果:

TreeSet<Player> myTreeSet = Stream.of(saajan, eric, kai, kevin).collect(Collectors.toCollection(TreeSet::new));
assertThat(myTreeSet).containsExactly(kevin, kai, eric, saajan);

如我们所见,代码与将字符串收集到TreeSet非常相似。由于Player的compareTo()方法指定了“age”属性作为其自然排序,我们通过按升序排列玩家来验证结果(myTreeSet)。

值得一提的是,我们使用了*AssertJ的containsExactly()*方法来验证TreeSet精确包含给定元素的顺序,且无其他内容**。

接下来,我们将看看如何使用自定义比较器收集这些玩家到TreeSet中。

3. 使用自定义比较器收集到TreeSet

我们已经看到Collectors.toCollection(TreeSet::new)允许我们在流中按自然顺序收集元素到TreeSet。TreeSet提供了另一个接受比较器对象作为参数的构造函数:

public TreeSet(Comparator<? super E> comparator) { ... }

因此,如果我们希望TreeSet对元素应用不同的排序,我们可以创建一个比较器对象并将其传递给上面提到的构造函数

接下来,让我们按胜场次数而不是年龄收集这些玩家到TreeSet中:

TreeSet<Player> myTreeSet = Stream.of(saajan, eric, kai, kevin)
  .collect(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparingInt(Player::getNumberOfWins))
));
assertThat(myTreeSet).containsExactly(kai, eric, kevin, saajan);

这次,我们使用了一个lambda表达式来创建TreeSet实例。此外,我们使用Comparator.comparingInt()传递了自己的比较器,以便根据玩家的胜场次数进行排序。

Player::getNumberOfWins引用了我们需要比较的属性值。

当运行测试时,它会通过。

然而,所需的比较逻辑并不总是像示例中那样简单,例如可能需要比较额外计算的结果。

最后,让我们再次收集这些玩家到TreeSet中。但这次,我们想要按照胜率(胜场数/总比赛数)进行排序:

TreeSet<Player> myTreeSet = Stream.of(saajan, eric, kai, kevin)
  .collect(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(player -> BigDecimal.valueOf(player.getNumberOfWins())
    .divide(BigDecimal.valueOf(player.getNumberOfPlayed()), 2, RoundingMode.HALF_UP)))));
assertThat(myTreeSet).containsExactly(kai, eric, saajan, kevin);

如上述测试所示,我们使用了Comparator.comparing(Function keyExtractor)方法来指定可比较的排序键。在这个例子中,keyExtractor函数是一个lambda表达式,用于计算玩家的胜率。

如果运行测试,它会通过。于是,我们得到了预期的TreeSet。

4. 总结

在这篇文章中,我们通过示例详细讨论了如何根据自然排序和自定义比较器将流元素收集到TreeSet中。

如往常一样,所有示例的完整源代码可在GitHub上获取。