6.7 并行化数组操作

Java 8还引入了一些针对数组的并行操作,脱离流框架也可以使用Lambda表达式。像流框架上的操作一样,这些操作也都是针对数据的并行化操作。让我们看看如何使用这些操作解决那些使用流框架难以解决的问题。

这些操作都在工具类Arrays中,该类还包括Java以前版本中提供的和数组相关的有用方法,表6-1总结了新增的并行化操作。

表6-1:数组上的并行化操作

方法名 操作
parallelPrefix 任意给定一个函数,计算数组的和
parallelSetAll 使用Lambda表达式更新数组元素
parallelSort 并行化对数组元素排序

读者可能以前写过类似例6-7的代码,使用一个for循环初始化数组。在这里,我们用数组下标初始化数组中的每个元素。

例6-7 使用for循环初始化数组

  1. public static double[] imperativeInitilize(int size) {
  2. double[] values = new double[size];
  3. for(int i = 0; i < values.length;i++) {
  4. values[i] = i;
  5. }
  6. return values;
  7. }

使用parallelSetAll方法能轻松地并行化该过程,代码如例6-8所示。首先提供了一个用于操作的数组,然后传入一个Lambda表达式,根据数组下标计算元素的值。在该例中,数组下标和元素的值是一样的。使用这些方法有一点要小心:它们改变了传入的数组,而没有创建一个新的数组。

例6-8 使用并行化数组操作初始化数组

  1. public static double[] parallelInitialize(int size) {
  2. double[] values = new double[size];
  3. Arrays.parallelSetAll(values, i -> i);
  4. return values;
  5. }

parallelPrefix操作擅长对时间序列数据做累加,它会更新一个数组,将每一个元素替换为当前元素和其前驱元素的,这里的“和”是一个宽泛的概念,它不必是加法,可以是任意一个BinaryOperator

使用该方法能计算的例子之一是一个简单的滑动平均数。在时间序列上增加一个滑动窗口,计算出窗口中的平均值。如果输入数据为0、1、2、3、4、3.5,滑动窗口的大小为3,则简单滑动平均数为1、2、3、3.5。例6-9展示了如何计算滑动平均数。

例6-9 计算简单滑动平均数

  1. public static double[] simpleMovingAverage(double[] values, int n) {
  2. double[] sums = Arrays.copyOf(values, values.length);
  3. Arrays.parallelPrefix(sums, Double::sum);
  4. int start = n - 1;
  5. return IntStream.range(start, sums.length)
  6. .mapToDouble(i -> {
  7. double prefix = i == start ? 0 : sums[i - n];
  8. return (sums[i] - prefix) / n;
  9. })
  10. .toArray();
  11. }

这段代码有点复杂,我会分步介绍它是如何工作的。参数n是时间窗口的大小,我们据此计算滑动平均值。由于要使用的并行操作会改变数组内容,为了不修改原有数据,在➊处复制了一份输入数据。

在➋处执行并行操作,将数组的元素相加。现在sums变量中保存了求和结果。比如输入0、1、2、3、4、3.5,则计算后的值为0.0、1.0、3.0、6.0、10.0、13.5。

现在有了和,就能计算出时间窗口中的和了,减去窗口起始位置的元素即可,除以n即得到平均值。可以使用已有的流中的方法计算该值,那就让我们来试试吧!使用Intstream.range得到包含所需元素下标的流。

在➍处使用总和减去窗口起始值,然后再除以n得到平均值。最后在➎处将流转换为数组。