Java Stream:按布尔谓词将列表分成两个。

40

我有一个employees列表。它们有一个isActive布尔字段。我想将employees分成两个列表:activeEmployeesformerEmployees。是否可以使用Stream API来实现?最复杂的方法是什么?


2
可能是如何使用Java8按谓词对列表进行分区?的重复问题。 - Malte Hartwig
@MalteHartwig 注意,重复的 says 是“谓词”,但 OP 实际上是在询问按函数分组。如果以那种方式关闭,我不会重新打开它。 - Andy Turner
5个回答

51

Collectors.partitioningBy:

Map<Boolean, List<Employee>> partitioned = 
    listOfEmployees.stream().collect(
        Collectors.partitioningBy(Employee::isActive));

生成的地图包含两个列表,对应于谓词是否匹配:
List<Employee> activeEmployees = partitioned.get(true);
List<Employee> formerEmployees = partitioned.get(false);

使用partitioningBy而不是groupingBy有几个原因(如Juan Carlos Mendoza所建议的):
首先,groupingBy的参数是一个Function<Employee, Boolean>(在这种情况下),因此有可能传递一个可以返回null的函数,如果其中任何一个雇员的函数返回null,则会导致第三个分区。partitioningBy使用Predicate<Employee>,因此它只能返回2个分区。否则收集器将抛出NullPointerException:虽然没有明确记录,但显然对于null键会抛出异常,这可能是由于Map.computeIfAbsent的行为导致的,即“如果函数返回null,则不记录任何映射”,这意味着元素将被静默丢弃(感谢lczapski指出这一点)。
其次,使用partitioningBy在结果映射中获得两个列表(*);而使用groupingBy,您只能获得元素映射到给定键的键/值对。
System.out.println(
    Stream.empty().collect(Collectors.partitioningBy(a -> false)));
// Output: {false=[], true=[]}

System.out.println(
    Stream.empty().collect(Collectors.groupingBy(a -> false)));
// Output: {}

(*) 这种行为在Java 8 Javadoc中没有记录,但是它在Java 9中被添加了。


第三,您获得的Map在内部进行了优化,仅保留两个键。 - Eugene
1
我对这个问题很好奇:“传递一个可以返回null的函数,意味着如果该函数返回null,则会有第三个分区”。我已经创建了一段代码来返回null Stream.of(1,2,3,4).collect(groupingBy(x -> x == 3 ? null : x >= 3)),但执行后返回了一个异常:“java.lang.NullPointerException: element cannot be mapped to a null key”。所以这是不可能的。 - lczapski
@lczapski 很有趣,我会更新答案。不过这实际上并没有记录在案 - Andy Turner
@lczapski 我猜这个限制是从 Map.computeIfAbsent 隐含地传递过来的,它说:“如果函数返回 null,则不记录任何映射”。 - Andy Turner

8

在这种情况下,您还可以使用groupingBy,因为有两个分组可能性(活动和非活动员工):

Map<Boolean, List<Employee>> grouped = employees.stream()
                .collect(Collectors.groupingBy(Employee::isActive));

List<Employee> activeEmployees = grouped.get(true);
List<Employee> formerEmployees = grouped.get(false);

5
+1,但请注意在使用此方法时应略微小心:groupingBy 的参数是 Function<Employee, Boolean>,因此有可能传递一个可以返回 null 的函数,这意味着如果该函数对任何员工返回 null,则会有第三个分区。 partitioningBy 使用 Predicate,因此它只能返回2个分区。 - Andy Turner
1
我刚刚进行了一些实验,并发现不使用groupingBy的其他原因-请查看我回答的编辑部分。(抱歉,我绝对不是在试图撕裂你的答案,我实际上通过尝试这两个方法学到了东西!) - Andy Turner
@AndyTurner 谢谢。对于这种情况,我假设 isActive 不会返回 null(就像使用原始布尔值一样)。 - Juan Carlos Mendoza
1
我也认为是这样。我只是指出通过groupingBy有可能实现。 - Andy Turner

5

什么是最复杂的方法?

当然是使用新的Collectors::teeing功能的Java 12。

List<List<Employee>> divided = employees.stream().collect(
      Collectors.teeing(
              Collectors.filtering(Employee::isActive, Collectors.toList()),
              Collectors.filtering(Predicate.not(Employee::isActive), Collectors.toList()),
              List::of
      ));

System.out.println(divided.get(0));  //active
System.out.println(divided.get(1));  //inactive

3
如果您愿意使用第三方库,可以使用Collectors2.partition来自Eclipse Collections。该方法可以实现按条件分组。
PartitionMutableList<Employee> partition =
        employees.stream().collect(
                Collectors2.partition(Employee::isActive, PartitionFastList::new));

List<Employee> activeEmployees = partition.getSelected();
List<Employee> formerEmployees = partition.getRejected();

您还可以使用ListIterate来简化事情。

PartitionMutableList<Employee> partition =
        ListIterate.partition(employees, Employee::isActive);

List<Employee> activeEmployees = partition.getSelected();
List<Employee> formerEmployees = partition.getRejected();

PartitionMutableList 是一种继承自PartitionIterable的类型。每个PartitionIterable的子类型都有一个用于存储正结果的集合getSelected()和负结果的集合getRejected()

注意:我是Eclipse Collections的committer。


0
一个更简单、更干净的方法是使用 stream.filter()collect(),就像这样:
activeEmployees = employees.stream().filter(Employee::isActive).collect(Collectors.toList());
formerEmployees = employees.stream().filter(employee -> !employee.isActive()).collect(Collectors.toList());

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接