根据独立谓词流式传输集合并收集到多个结果

Ast*_*isk 4 java functional-programming java-8 java-stream

我正盯着一些命令式的代码,我试图将其转换为纯粹的功能性风格.基本上有一个迭代for循环inputSet,其中我检查3个谓词并outputSets根据哪个谓词匹配填充3 .输出集可以重叠.如何使用Java 8流/ map/filter /等以纯函数方式执行此操作?

Tag*_*eev 11

最简单的解决方案(除了保留更简单的一切)是创建三个独立的流:

Set<MyObj> set1 = inputSet.stream().filter(pred1).collect(Collectors.toSet());
Set<MyObj> set2 = inputSet.stream().filter(pred2).collect(Collectors.toSet());
Set<MyObj> set3 = inputSet.stream().filter(pred3).collect(Collectors.toSet());
Run Code Online (Sandbox Code Playgroud)

如果您有谓词列表,则可以创建相应的集合列表:

List<Predicate<MyObj>> predicates = Arrays.asList(pred1, pred2, pred3);
List<Set<MyObj>> result = predicates.stream()
        .map(pred -> inputSet.stream().filter(pred).collect(Collectors.toSet()))
        .collect(Collectors.toList());
Run Code Online (Sandbox Code Playgroud)

这里结果列表中的第一个对应于第一个谓词,依此类推.

如果你真的想一次性处理输入(无论出于何种原因),你可以为此编写一个特殊的收集器.这是一个非常普遍的:

public static <T, A, R> Collector<T, ?, List<R>> multiClassify(
        List<Predicate<T>> predicates, Collector<? super T, A, R> downstream) {
    Supplier<A> dsSupplier = downstream.supplier();
    BiConsumer<A, ? super T> dsAccumulator = downstream.accumulator();
    BinaryOperator<A> dsCombiner = downstream.combiner();

    Supplier<List<A>> supplier = () -> Stream.generate(dsSupplier)
            .limit(predicates.size()).collect(Collectors.toList());

    BiConsumer<List<A>, T> accumulator = (list, t) -> IntStream
            .range(0, predicates.size()).filter(i -> predicates.get(i).test(t))
            .forEach(i -> dsAccumulator.accept(list.get(i), t));

    BinaryOperator<List<A>> combiner = (l1, l2) -> IntStream.range(0, predicates.size())
            .mapToObj(i -> dsCombiner.apply(l1.get(i), l2.get(i)))
            .collect(Collectors.toList());

    Characteristics[] dsCharacteristics = downstream.characteristics().toArray(
            new Characteristics[0]);
    if (downstream.characteristics().contains(Characteristics.IDENTITY_FINISH)) {
        @SuppressWarnings("unchecked")
        Collector<T, ?, List<R>> result = (Collector<T, ?, List<R>>) (Collector<T, ?, ?>) 
            Collector.of(supplier, accumulator, combiner, dsCharacteristics);
        return result;
    }
    Function<A, R> dsFinisher = downstream.finisher();
    Function<List<A>, List<R>> finisher = l -> l.stream().map(dsFinisher)
           .collect(Collectors.toList());
    return Collector.of(supplier, accumulator, combiner, finisher, dsCharacteristics);
}
Run Code Online (Sandbox Code Playgroud)

它采用谓词列表并返回每个谓词的下游收集器结果列表.用法示例:

List<String> input = asList("abc", "ade", "bcd", "cc", "cdac");

List<Predicate<String>> preds = asList(
        s -> s.length() == 3, 
        s -> s.startsWith("a"), 
        s -> s.endsWith("c"));
List<Set<String>> result = input.stream().collect(multiClassify(preds, Collectors.toSet()));
// [[bcd, abc, ade], [abc, ade], [cc, abc, cdac]]
Run Code Online (Sandbox Code Playgroud)

  • @Asterisk,您可以使用谓词列表压缩结果列表(如`IntStream.range(0,preds.size()).boxed().collect(toMap(preds :: get,result :: get))`) .但是我不喜欢使用函数作为映射键.这很脆弱,因为函数对象标识是一个可疑的东西. (2认同)
  • 我经常面临同样的困境。直到现在,为了可读性,我一直选择第一个解决方案。是的,复杂性正在从 O(n) 变为 O(cn)(其中 c 是您要创建的列表的数量),但假设 c 是固定的,这仍然是线性的。在实践中,这并没有导致任何性能问题。即使这样做了,重构现有循环所获得的性能改进远远超过了这几个需要多个列表作为输出的强制循环的性能损失。 (2认同)

Fed*_*ner 8

另一种方法是使用该Consumer.andThen(anotherConsumer)方法创建一个由按顺序执行的内部使用者组成的组合使用者。这些内部消费者中的每一个都会测试每个谓词并根据它们是否匹配对元素进行分类。

public static <T> Consumer<T> classify(Predicate<T> predicate, Consumer<T> action) {
    return elem -> Optional.ofNullable(elem)
        .filter(predicate)
        .ifPresent(action);
}
Run Code Online (Sandbox Code Playgroud)

此实用程序方法返回一个消费者,该消费者将对正在消费的元素执行给定的操作,只要true该元素的谓词返回即可。

测试:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);

Set<Integer> set1 = new LinkedHashSet<>();
Set<Integer> set2 = new LinkedHashSet<>();
Set<Integer> set3 = new LinkedHashSet<>();

// Here's the composed consumer, made of inner consumers
Consumer<Integer> multiClassifier = classify(n -> n % 2 == 0, set1::add)
        .andThen(classify(n -> n % 3 == 0, set2::add))
        .andThen(classify(n -> n % 5 == 0, set3::add));

// Here the stream is consumed by the composed consumer
stream.forEach(multiClassifier);
Run Code Online (Sandbox Code Playgroud)

每个内部消费者都是用上面定义的实用程序方法创建的,它接收一个独立的谓词,当匹配时,会将流的元素添加到给定的集合中,即如果流的元素是 3 的倍数,它将是添加到set2.

最后,流被这个组合消费者消费,因此流被独立的谓词分类:

System.out.println(set1); // [2, 4, 6, 8, 10, 12]
System.out.println(set2); // [3, 6, 9, 12]
System.out.println(set3); // [5, 10]
Run Code Online (Sandbox Code Playgroud)

  • @Asterisk 你知道,为了让它看起来更实用,你可以在一行中编写相同的代码,除了三个集合声明。我知道你想要一个纯粹的函数式风格,所以我必须警告你......当心收藏家,因为他们在这方面可能具有欺骗性:) 我的意思是他们执行*可变减少*,可变是这里的关键部分。`collect()` 返回一个数据结构,但减少不是以纯函数风格执行的,因为在每个减少步骤中,部分收集结果都会在数据结构中累积和组合,即保持状态。 (2认同)