如何在流分组或分区时忽略objects?

How to neglect objects during stream grouping or partitioning?

Collectors.groupingByCollectors.partitioningBy期间可以忽略某些元素吗?

当然我知道我可以在流中的任何位置放置 .filter()。但我的问题是,我必须 运行 一个相当复杂和昂贵的评估,来决定我的 objects 应该被划分到哪个“组”。

而且,总有很多objects我在收集的时候想忽略。

示例:想象一个 List<Foo>,我想分成 2 个列表。这很简单,但是我怎么能另外忽略所有不符合我评估条件的 objects 呢?

var map = foos.stream().collect(Collectors.groupingBy(
    foo -> {
        int bar = complexEvaluation(foo);
        if (bar > 1000) return true;
        if (bar < 0) return false;
        //TODO how to neglect the rest between 0-1000
    },
    Collectors.mapping(foo -> foo.id, Collectors.toSet())
));

要在 filtergroupingBy 中重用 complexEvaluation 的结果,您可以在过滤之前调用并将结果存储在包装器中 class。

foos.stream()
    .map(foo -> {
        int bar = complexEvaluation(foo);
        if (bar > 1000) Pair.of(foo, true);
        if (bar < 0) Pair.of(foo, false);
        return Pair.of(foo, null);
    )
    .filter(fooPair -> fooPair.getRight() != null)
    .collect(Collectors.groupingBy(
        Pair::getRight(),
        Collectors.mapping(fooPair -> fooPair.getLeft().id, Collectors.toSet()
    );

但是,这仅适用于您出于某种原因坚持使用 groupingBy 的情况。

使用 foreach 的替代方案会更容易阅读:

Map<Boolean, Set<Foo>> groups = new HashMap<>();
foos.stream()
    .forEach(foo -> {
        int bar = complexEvaluation(foo);
        if (bar > 1000) groups.computeIfAbsent(true, k->new HashSet<>()).add(foo);
        if (bar < 0)    groups.computeIfAbsent(false, k->new HashSet<>()).add(foo);
   })
    

如果我正确理解您的意图,您希望在收集值时过滤掉一些值。 Collectors.filtering().

是可行的

请注意,filtering() 可以消除某些存储桶的所有值,但不会导致删除空存储桶。在下面的示例中,1、2、3、4、5 的存储桶将为空。

    public static void main(String[] args) {
        var foos = List.of(-100, 1, 2, 3, 4, 5, 1001, 1002, 1003);
        
        var map = foos.stream()
                .collect(Collectors.groupingBy(
                            UnaryOperator.identity(),
                            Collectors.filtering(foo -> foo < 0 || foo > 1000,
                                    Collectors.toSet())));

        System.out.println(map);
    }

地图

{1=[], 2=[], 3=[], -100=[-100], 4=[], 5=[], 1001=[1001], 1002=[1002], 1003=[1003]}

更新

我修改了这个问题,下面提供的解决方案预先计算了值。

在此版本中,流中不需要的条目被过滤掉,这使得 Collector 中的代码更易于阅读。我希望使用整数而不是 foo 对象不是问题。

    public static void main(String[] args) {
        List<Integer> foos = List.of(-100, 1, 2, 3, 4, 5, 101, 102, 103);

        Map<Integer, Integer> fooToValue = getFooToValueMap(foos);

        Map<Boolean, Set<Integer>> map = getFoosMap(fooToValue);

        System.out.println(map);
    }
    private static Map<Boolean, Set<Integer>> getFoosMap(Map<Integer, Integer> fooToValue) {
        return fooToValue.entrySet().stream()
                .filter(entry -> entry.getValue() < 0 || entry.getValue() > 1000)
                .collect(Collectors.partitioningBy(
                            entry -> entry.getValue() > 1000,
                            Collectors.mapping(Map.Entry::getKey, Collectors.toSet())
                ));
    }
    private static Map<Integer, Integer> getFooToValueMap(List<Integer> list) {
        return list.stream()
                .collect(Collectors.toMap(UnaryOperator.identity(), foo -> complexEvaluation(foo)));
    }
    private static int complexEvaluation(int foo) {
        return (int) Math.signum(foo) * foo * foo;
    }

地图

{false=[-100], true=[101, 102, 103]}

只需使用 enum 来定义您的 3 个案例:

enum Categories {
    HIGH, LOW, NEGATIVE
}

var map = foos.stream().collect(Collectors.groupingBy(
    foo -> {
        int bar = complexEvaluation(foo);
        if (bar > 1000) return HIGH;
        if (bar < 0) return NEGATIVE;
        return LOW;
    },
    Collectors.mapping(foo -> foo.id, Collectors.toSet())
));

如果不需要,请忽略或删除 LOW。它还具有为您的类别赋予更多意义而不是仅仅命名它们的额外好处 true/false,并且如果您将来需要更多类别,可以更轻松地重构。

唯一的缺点是它构建了一个无用的 LOW 集合,但这只是一个问题,如果它与其他集合和 complexEvaluation() 操作相比真的很大。

如果你想避免临时存储,你必须实现自己的收集器:

var map = foos.parallelStream().collect(
    () -> Map.of(true, new HashSet<ID>(), false, new HashSet<ID>()),
    (o, foo) -> {
        int bar = complexEvaluation(foo);
        ID id = foo.id;
        if (bar > 1000) o.get(true).add(id);
        else if (bar < 0) o.get(false).add(id);
    },
    (a, b) -> { a.get(true).addAll(b.get(true)); a.get(false).addAll(b.get(false)); }
);

此示例与 partitioningBy 具有相同的行为,始终为 truefalse 创建条目。

ID 是您未包含在问题中的 foo.id 类型的占位符。