如何使用Java Streams API对两个维度相同的列表进行逐元素乘法?

3

我有两个列表:

List<Double> margins =  Arrays.asList(1.0,2.0,3.0,4.0,5.0);
List<Integer> quantity = Arrays.asList(1,2,3,4,5);

我想对它们进行逐元素乘法运算,我知道可以使用标准的for循环来完成,但我想知道是否可以通过Stream API实现相同的功能,使得这个过程更快、更省资源?
就像Python ML在NumPy数组上所做的那样;他们对事物进行向量化处理,而不是使用for循环,这样可以加速运算。

3
只是好奇:您是否有任何证据表明在您的情况下使用Java基于流的方法可能比“标准”for循环更“快速且资源消耗更少”?(顺便说一句,我不知道哪种方式更好 - 我认为任何试图微基准测试结果的尝试都可能具有挑战性)。 - andrewJames
顺便提一下,在Java 9+中,List.of - Basil Bourque
2个回答

3

类似于Python ML与numpy数组的操作;他们通过向量化而不是使用for循环来加速处理。

如果您对向量化感兴趣,则Stream API并不适合您。

自Java 16以来,我们拥有向量API作为孵化特性(这意味着它不是API的最终状态,它只是用于测试和收集反馈,您不应该在生产中使用它)。

为了在Java中使用孵化特性,您需要做一些额外的工作,以获取从孵化模块导入的文件。

确保导入jdk.incubator.vector包中的类的方法之一是创建一个模块,以明确指定它需要此包。

考虑一个具有以下文件夹结构的简单测试项目:

- [src]
  - [main]
    - [java]
      - module-info.java
      - [vectorization.test]
        - Main.java
      - resources

// other things

module-info.java - 在这里我们请求 Vector API 的文件:

module vectorization.test {
    requires jdk.incubator.vector;
}

Main.java

package vectorization.test;

import jdk.incubator.vector.DoubleVector;
import jdk.incubator.vector.VectorSpecies;

import java.util.Arrays;

public class Main {
    
    public static void main(String[] args) {
        double[] margins = {1.0, 2.0, 3.0, 4.0, 5.0};
        double[] quantity = {1, 2, 3, 4, 5};
        double[] result = new double[margins.length];
    
        multiply(margins, quantity, result);
    
        System.out.println(Arrays.toString(result));
    }
    
    public static final VectorSpecies<Double> SPECIES = DoubleVector.SPECIES_PREFERRED;
    
    public static void multiply(double[] margins, double[] quantity, double[] result) {
        int i = 0;
        for (; i < SPECIES.loopBound(margins.length); i += SPECIES.length()) {
            DoubleVector marginsVector = DoubleVector.fromArray(SPECIES, margins, i);
            DoubleVector quantityVector = DoubleVector.fromArray(SPECIES, quantity, i);
            DoubleVector resultVector = marginsVector.mul(quantityVector);
            resultVector.intoArray(result, i);
        }
        for (; i < margins.length; i++) {
            result[i] = margins[i] * quantity[i];
        }
    }
}

输出:

[1.0, 4.0, 9.0, 16.0, 25.0]

想了解有关 Vector API 的更多信息,请查看JEP 426

说到 Streams,它们并不比普通循环更高效(在顺序流的情况下),事实上它们更慢,因为流需要创建额外的对象来执行迭代、转换和累加结果。这里没有什么魔法。

并行流可能更快,但这是一个需要谨慎使用的工具,因为你也可能得到相反的效果。你需要让你的任务可以并行化,有空闲的 CPU 核心让你的线程完成工作,并且数据量应该足够大,以使使用并行流变得合理(还需要测量性能,找出是否在你的情况下有所不同)。

引入 Java 中的函数式编程特性的主要目标之一是提供一种简洁而易于阅读的代码结构方式。

例如,您可以使用 Arrays.setAll() 来填充产品的结果数组:
Arrays.setAll(result, i -> margins[i] * quantity[i]);

1
哇,这是我学到的新东西。他们应该使向量化长列表的功能稳定。这将真正有助于加快速度。而你所说的流API是我学到的新东西。我以为他们引入流是为了使数组或向量操作更快。 - Soumalya Bhattacharya

2
您可以使用流来将数组元素相乘,但这并不比传统循环更有效。然而,对于这么小的数据集来说,开销非常小,不会有任何区别。
以下是通过流实现的示例。基本上,您可以使用IntStream类从0到最小列表的大小(不包括)迭代两个列表。然后,将每个整数索引映射到一个Double对象,该对象表示margins列表的第i个元素与quantity列表的第i个元素的乘积,最后将结果收集到List<Double>中。
List<Double> margins = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
List<Integer> quantity = Arrays.asList(1, 2, 3, 4, 5);

List<Double> res = IntStream.range(0, Math.min(margins.size(), quantity.size()))
        .mapToObj(i -> margins.get(i) * quantity.get(i))
        .collect(Collectors.toList());

我有一些包含大约1000个元素的长列表。为了演示目的,我提供了一些虚拟值,但感谢你提供的解决方案。 - Soumalya Bhattacharya
1
@SoumalyaBhattacharya 我相当确定这仍然不会有太大的差别。 - Dan
2
那么,按照传统方式进行至少是熟悉的。 - Soumalya Bhattacharya

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