Java Stream API:寻找一种优雅的过滤和映射方式

7

过滤和映射流的默认“最佳实践”是

Stream<T> source;
// ...
Predicate<T> predicate; // = ...
Function<T, U> mapper; // = ...
Stream<U> dst = source
         .filter(predicate)
         .map(mapper);

在许多软件项目中,你会遇到同样的过滤和映射操作需要应用于多个流的情况。例如,一个T类对象的集合应该被转换为U类对象的List,其中U是T的子类,并且我们只想要U的实例。因此,可以这样写:

Collection<T> source;
// ...
List<U> dst = source.stream()
            .filter(U.class::isInstance)
            .map(U.class::cast)
            .collect(Collectors.toList());

为了概括这个内容,我编写了一个帮助方法,称为onlyInstancesOf
static <T, U> Function<T, Stream<U>> onlyInstancesOf(Class<U> clazz) {
    return t -> clazz.isInstance(t)
            ? Stream.of(clazz.cast(t))
            : Stream.empty();
}

这个方法旨在和 flatMap 一起使用:

List<U> dst = source.stream()
            .flatMap(onlyInstancesOf(U.class))
            .collect(Collectors.toList());

我经常使用的另一个函数是optionalPresent,用于处理包含可选值的流:

static <T> Function<Optional<T>, Stream<T>> optionalPresent() {
    return t -> t.map(Stream::of).orElse(Stream.empty());
}

使用方法:

Collection<Optional<T>> source;
// ...
List<T> dst = source.stream()
        .flatMap(optionalPresent())
        .collect(Collectors.toList());

这些解决方案看起来很优雅,但是它们有一个很大的缺点:它们比先过滤再映射的“传统”解决方案慢了10倍以上。

您会建议如何处理这些经常使用的过滤和映射习惯用语而不违反DRY原则?


3
在Java 9中,你可以使用stream.flatMap(Optional::stream)来处理Optional类型的情况。 - marstran
3
“10倍慢”是根据什么来说的? - Eugene
还有,为什么onlyInstancesOf只接受一个输入而不是该类的Stream - Eugene
@Eugene,使用stream.flatMap(onlyInstancesOf(U.class))stream.filter(U.class::isInstance).map(U.class::cast)慢大约10倍(至少我用一个小程序测量了这个结果,该程序创建了1000万个类T的实例,在随机情况下每秒钟创建一次类U的实例,然后收集所有类U的对象)。 - Torsten Fehre
4
我的基准测试表明flatmap版本的速度大约慢了1.8倍。你有读过https://dev59.com/hHRB5IYBdhLWcg3wz6UK吗? - Jorn Vernee
显示剩余2条评论
1个回答

8
您可以使用一个收集器(因为您总是在收集)来过滤特定类的实例:
static <T, U extends T> Collector<T, ?, List<U>> onlyInstancesOfCollector(Class<U> clazz) {
    return Collector.of(
            ArrayList::new,
            (acc, e) -> {
                if(clazz.isInstance(e)) {
                    acc.add(clazz.cast(e));
                }
            },
            (a, b) -> {
                a.addAll(b);
                return a;
            });
}

...

List<U> dst = source.stream()
    .collect(onlyInstancesOfCollector(U.class));

哪个具有更好的性能特点:
Benchmark           Mode  Cnt  Score   Error  Units
Tests.collector     avgt   10  0.171 ± 0.003   s/op
Tests.filterAndMap  avgt   10  0.203 ± 0.005   s/op
Tests.flatmap       avgt   10  0.375 ± 0.012   s/op

完整的jmh基准测试:
@BenchmarkMode({ Mode.AverageTime })
@Warmup(iterations = 25)   
@Measurement(iterations = 10)    
@State(Scope.Benchmark)
public class Tests {

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
            .include(Tests.class.getSimpleName())
            .build();
        new Runner(opt).run();
    }

    List<A> input;

    @Setup
    public void setup() {
        Random r = new Random();
        input = new ArrayList<>();
        for(int i = 0; i < 10_000_000; i++) {
            input.add(r.nextInt(2) == 0 ? new A() : new B());
        }
    } 

    @Fork(1)
    @Benchmark
    public List<B> filterAndMap() {
        return input.stream()
            .filter(B.class::isInstance)
            .map(B.class::cast)
            .collect(Collectors.toList());
    }

    @Fork(1)
    @Benchmark
    public List<B> flatmap() {
        return input.stream()
            .flatMap(onlyInstancesOf(B.class))
            .collect(Collectors.toList());
    }

    @Fork(1)
    @Benchmark
    public List<B> collector() {
        return input.stream()
            .collect(onlyInstancesOfCollector(B.class));
    }

    static <T, U> Function<T, Stream<U>> onlyInstancesOf(Class<U> clazz) {
        return t -> clazz.isInstance(t)
                ? Stream.of(clazz.cast(t))
                : Stream.empty();
    }

    static <T, U extends T> Collector<T, ?, List<U>> onlyInstancesOfCollector(Class<U> clazz) {
        return Collector.of(
                ArrayList::new,
                (acc, e) -> {
                    if(clazz.isInstance(e)) {
                        acc.add(clazz.cast(e));
                    }
                },
                (a, b) -> {
                    a.addAll(b);
                    return a;
                });
    }

}

class A {}
class B extends A {}

太棒了!我本想发布类似的内容,但这份甚至更好,点赞! - Eugene
2
当涉及到最小的性能差异时,您可以将Stream.empty()替换为null,因为flatMap API指定它应该被视为一个空流。然后,有一个选项可以使用黑暗面并进行未经检查的转换,在紧接着的Class.isInstance测试之前,这可能被认为是可以接受的... - Holger
1
@Holger,这确实有所帮助,我得分为:0.309 ± 0.007 - Jorn Vernee

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