不要使用 Stream 的 filter().map(),试试 mapMulti()-Java专区论坛-技术-SpringForAll社区

不要使用 Stream 的 filter().map(),试试 mapMulti()

让我们来探讨一下使用 filter() 和 map() 的代码在某些用例中是否可能效率低下,以及为什么 mapMulti() 可能是更好的替代方案。

什么是 mapMulti()?

1) 这是一个一对多的中间操作。每个元素可以转换为 0 个或多个元素。这意味着它可以用来过滤元素并对其进行转换。这就是为什么我们可以选择它,而不是使用 filter() 和 map() 链接在一起。

2) 它与 flatMap() 方法不同,因为它不需要返回 Stream。这就是为什么在我们还需要过滤元素时,它比 flatMap() 更高效。

现在我们将尝试使用 mapMulti() 为问题提供替代解决方案。然后我们将使用 JMH 比较这些解决方案的性能。

图片[1]-不要使用 Stream 的 filter().map(),试试 mapMulti()-Java专区论坛-技术-SpringForAll社区

问题定义

给定一个表示数字的字符串列表,
从列表中仅提取偶数。

一些例子:

[“1”, “error”, “42”, “3”, “banana”, “4”] -> [42, 4]
[“1”, “error”, “3”, “banana”] -> []

解决方案 1:filter()和 map()

每个人都会写的标准代码可能看起来像这样使用 filter() :

var evenNumbers = lines.stream()
      .filter(line -> {
            try {
                return Integer.parseInt(line) % 2 == 0;
            } catch (NumberFormatException e) {
                return false;
            }
        })
        .map(Integer::parseInt) 
        .toList();

第一个 filter() 解析字符串以检查它是否为偶数。然后 map() 再次解析相同的字符串以将其转换为整数。这导致了冗余工作。我们为每个有效元素调用了两次 parseInt()方法。

解决方案 2:flatMap()

flatMap() 方法避免了冗余解析,但引入了另一种低效性:

var evenNumbers = lines.stream()
        .flatMap(line -> {
            try {
                if(Integer.parseInt(line) % 2 == 0) {
                    return Stream.of(Integer.parseInt(line));
                }
                return Stream.empty();
            } catch (NumberFormatException e) {
                return Stream.empty();
            }
        })
        .toList();

虽然它解决了双重解析问题,但它为每一行创建了一个新的流,即使是无效条目也是如此。由于重复分配流对象和对数据的额外迭代,这增加了开销。

解决方案 3:mapMulti()

使用 mapMulti() ,我们可以通过避免双重解析和无用的流创建来解决这两个问题:

var evenNumbers = lines.stream()
        .<Integer>mapMulti((line, consumer) -> {
            try {
                int number = Integer.parseInt(line);
                if (number % 2 == 0) {
                    consumer.accept(number);
                }
            } catch (NumberFormatException ignored) { }
        })
        .toList();

这段代码相当复杂,但却是最高效的解决方案。它避免了为每一行创建新的流,并消除了双重解析的需求。

性能比较:

我们将使用 JMH 来比较这三种解决方案的性能。

@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
public class ProblemBenchmark {

    private List<String> lines;

    @Setup
    public void prepare() {
        Random random = new Random();
        lines = Stream.generate(() -> {
            if (random.nextInt(10) == 0) {
                return "error";
            } else {
                return String.valueOf(random.nextInt(1000));
            }
        }).limit(10000).toList();
    }

    @Benchmark
    public List<Integer> filterMap() {
        return lines.stream()
                .filter(line -> {
                    try {
                        return Integer.parseInt(line) % 2 == 0;
                    } catch (NumberFormatException e) {
                        return false;
                    }
                })
                .map(Integer::parseInt)
                .toList();
    }

    @Benchmark
    public List<Integer> flatMap() {
        return lines.stream()
                .flatMap(line -> {
                    try {
                        int number = Integer.parseInt(line);
                        if (number % 2 == 0) {
                            return Stream.of(number);
                        }
                        return Stream.empty();
                    } catch (NumberFormatException e) {
                        return Stream.empty();
                    }
                })
                .toList();
    }

    @Benchmark
    public List<Integer> mapMulti() {
        return lines.stream()
                .<Integer>mapMulti((line, consumer) -> {
                    try {
                        int number = Integer.parseInt(line);
                        if (number % 2 == 0) {
                            consumer.accept(number);
                        }
                    } catch (NumberFormatException ignored) { }
                })
                .toList();
    }

}

正如你所想象的,结果显示 mapMulti() 比 filter().map() 和 flatMap() 更快,而且更不容易出错。

 

| Benchmark | Mode | Cnt  | Score | Error   | Units  |
|-----------|------|------|-------|---------|--------|
| mapMulti  | avgt | 5    | 0,690 | ± 0,017 | ms/op  |
| flatMap   | avgt | 5    | 0,722 | ± 0,018 | ms/op  |
| filterMap | avgt | 5    | 0,758 | ± 0,037 | ms/op  |

Filter() + Map() 固有的(但较小的)低效性

当你使用 .filter().map() 时,操作会作为单流管道的一部分延迟应用。流本身不会创建新流,而是将操作链接起来。然而,每个操作都会增加一层处理,可能会引入开销。

  • • 每个 .filter() 和 .map() 调用都会在管道中添加一个单独的步骤。
  • • 每个元素流经所有中间操作,增加了一层间接性。
  • • 每一步都会增加函数调用的开销,即使函数很简单。

即使有 JIT 优化,开销虽小,但仍然存在。

如果我们尝试运行一个基准测试来比较 filter().map() 与 mapMulti() 的性能,在没有执行相同操作的额外逻辑的情况下,我们可以看到 mapMulti() 更快。

基准测试:

 

filterMap: Average Time = 143.193 microseconds ± 2.278
mapMulti: Average Time = 129.308 microseconds ± 0.796

在这种情况下, mapMulti() 比 filter().map() 快约 10%。在大型数据集上,性能差异可能非常显著。此外, mapMulti() 的误差范围更小。这是因为 mapMulti() 避免了链接多个步骤的开销,并减少了函数调用的次数。

结论

虽然 filter() + map() 和 flatMap() 很有用,但 mapMulti() 提供了一种更高效、更简化的方式来过滤和转换 Java Streams 中的数据。

下次在处理过滤和映射重叠的转换时,考虑使用 mapMulti() 来编写更简洁、更高效的代码。它减少了冗余操作,避免了额外的流创建,并简化了转换逻辑。

何时使用 filter()呢?

1) 当你只需要过滤元素而不进行转换或添加额外逻辑时

2) 当流数据集较小时(绝大多数标准使用场景)

3) 当您希望确保代码更具可读性和可维护性时。mapMulti 代码很糟糕。

4) 当你不关心性能差异时(谁会在意呢?)

5) 当你不想使用一个你还不知道的新方法时

替代且更快的解决方案

在这段代码中,我们仅考虑了 filter() 和 mapMulti() ,但还有其他方法可以解决这个问题。这并非本文的范围,但这里有一些例子:

var evenNumbers = lines.stream()
    .filter(line -> line.matches("\\d+") && Integer.parseInt(line) % 2 == 0)
    .map(Integer::parseInt)
    .toList();

普通的旧式基本 For 循环:

var result = new java.util.ArrayList<Integer>();
for(String line: lines) {
    try {
        int number = Integer.parseInt(line);
        if (number % 2 == 0) {
            result.add(number);
        }
    } catch (NumberFormatException ignored) { }
}
return result;

为了完整性,我也对这些解决方案进行了基准测试。以下是结果:

 

| Benchmark | Mode | Cnt | Score  | Error   | Units |
|-----------|------|-----|--------|---------|-------|
| regex     | avgt |  5  | 0.513  | ± 0.109 | ms/op |
| forLoop   | avgt |  5  | 0.610  | ± 0.015 | ms/op |

并行流如何

使用并行流时, filter().map() 的开销减少了,性能差异比以前小得多。当我们拥有多个核心时,链式操作的开销可以忽略不计。

 

| Benchmark             | Mode | Cnt | Score | Error   | Units |
|-----------------------|------|-----|-------|---------|-------|
| mapMultiParallel      | avgt |  5  | 0.132 | ± 0.235 | ms/op |
| filterMapParallel     | avgt |  5  | 0.138 | ± 0.004 | ms/op |
| flatMapParallel       | avgt |  5  | 0.139 | ± 0.002 | ms/op |
| regexSolutionParallel | avgt |  5  | 0.151 | ± 0.008 | ms/op |

请登录后发表评论

    没有回复内容