MapReduce是一种编程模型,用于在分布式环境中使用大量在集群中工作的机器来处理非常大的数据集。 这种编程模型有如下两种操作:
- **Map:**此操作将原始元素筛选和转换成更适合归约操作的形式
- **Reduce:**此操作生成来自所有元素的汇总结果,例如数字值的和或平均值。
这种编程模型已经在函数式编程世界中得到广泛的应用。在Java生态系统中,Apache软件基金会的Hadoop项目提供此模型的实现。Stream类实现了两个不同的归约操作:
- 纯归约操作,在不同版本的reduce()方法中实现,用来处理元素流获取一个值
- 可变归约操作,在不同版本的collect()方法中实现,用来处理元素流生成可变的数据结构,例如Collection或StringBuilder。
在本节中,将学习如何使用不同版本的reduce()方法从一个值流中生成结果。你可能已经猜到,reduce()方法是Stream中的终点操作。
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
在本节中,将实现之前描述的如何通过输入源创建流的范例。通过如下步骤实现范例:
-
首先,创建本范例中用到的辅助类。查看小节“创建不同来源的流”,复用范例中的Person和PersonGenerator类。
-
然后,创建名为DoubleGenerator的类,实现名为generateDoubleList()的方法用来生成双精度数字列表。此方法接收两个参数,其中包含生成列表的程度,以及列表中最大值。 它将生成随机的双精度数字列表:
public class DoubleGenerator { public static List<Double> generateDoubleList(int size, int max) { Random random = new Random(); List<Double> numbers = new ArrayList<>(); for(int i = 0 ; i < size ; i ++){ double value = random.nextDouble() * max; numbers.add(value); } return numbers; }
-
实现名为generateStreamFromList()的方法,此方法接收双精度数字列表为参数,并生成包含列表元素的DoubleStream流。因此,使用DoubleStream.Builder类构造流:
public static DoubleStream generateStreamFromList(List<Double> list) { DoubleStream.Builder builder = DoubleStream.builder(); for(Double number : list) { builder.add(number); } return builder.build(); } }
-
创建名为point的类,包括两个双精度属性,x和y,以及对应的get()和set()方法。代码很简单,不在此列出。
-
创建名为PointGenerator的类,包括名为generatePointList()的方法,此方法接收想要的列表长度,生成并返回随机Point对象列表:
public class PointGenerator { public static List<Point> generatePointList(int size) { List<Point> ret = new ArrayList<>(); Random randomGenerator = new Random(); for (int i = 0 ; i < size ; i ++) { Point point =new Point(); point.setX(randomGenerator.nextDouble()); point.setY(randomGenerator.nextDouble()); ret.add(point); } return ret; } }
-
现在创建包含main()方法的Main类。首先,使用DoubleGenerator类生成10000个双精度数字的List:
public class Main { public static void main(String[] args) { List<Double> numbers = DoubleGenerator.generateDoubleList(10000, 1000);
-
Stream类和特定的DoubleStream、IntStream、LongStream类实现一些特殊的归约操作方法。因此,我们使用DoubleGenerator类生成DoubleStream,以及使用count()、sum()、average()、max()和min()方法分别得到元素数量、所有元素的和、所有元素的平均值、流的最大值和最小值。因为只能处理流元素一次,所以每次操作都需要创建新的流。需要注意这些方法只针对DoubleStream、IntStream、和LongStream类。Stream类只有count()方法,其中一些方法返回可选对象。切记这种对象没有任何值,所以在得到值之前应当检查可选对象:
DoubleStream doubleStream = DoubleGenerator.generateStreamFromList(numbers); long numberOfElements = doubleStream.parallel().count(); System.out.printf("The list of numbers has %d elements.\n", numberOfElements); doubleStream = DoubleGenerator.generateStreamFromList(numbers); double sum = doubleStream.parallel().sum(); System.out.printf("Its numbers sum %f.\n", sum); doubleStream = DoubleGenerator.generateStreamFromList(numbers); double average = doubleStream.parallel().average().getAsDouble(); System.out.printf("Its numbers have an average value of %f.\n", average); doubleStream = DoubleGenerator.generateStreamFromList(numbers); double max = doubleStream.parallel().max().getAsDouble(); System.out.printf("The maximum value in the list is %f.\n", max); doubleStream = DoubleGenerator.generateStreamFromList(numbers); double min = doubleStream.parallel().min().getAsDouble(); System.out.printf("The minimum value in the list is %f.\n", min);
-
然后,使用reduce()方法的第一个版本。此方法接收关联的BinaryOperator作为参数,此参数接收两个相同类型的对象且返回此类型的对象。当此操作已经处理Stream所有元素时,返回相同类型参数化的Optional对象。例如,我们使用此方法计算两个Random对象随机列表的和:
List<Point> points=PointGenerator.generatePointList(10000); Optional<Point> point=points.parallelStream().reduce((p1,p2) -> { Point p=new Point(); p.setX(p1.getX()+p2.getX()); p.setY(p1.getY()+p2.getY()); return p; }); System.out.println(point.get().getX()+":"+point.get().getY());
-
然后,使用reduce()方法的第二个版本,与之前的方法类似,但在此情形中,除了组合的BinaryOperator对象,它接收该操作符的标识值(例如0表示和或者1表示产品)并返回正在处理的类型的元素。如果用到的流没有值,则将返回标识值。所以使用这个reduce()方法计算支付的薪水总额。我们使用map()方法将每个Person对象转换成int值(薪水数),所以当Stream对象执行reduce()方法时,具有了int值。在“变换流元素”小节中会获得更多的map()方法信息:
System.out.printf("Reduce, second version\n"); List<Person> persons = PersonGenerator.generatePersonList(10000); long totalSalary=persons.parallelStream().map(p -> p.getSalary()).reduce(0, (s1,s2) -> s1+s2); System.out.printf("Total salary: %d\n",totalSalary);
-
最后,使用reduce()方法的第三个版本。当归约操作的结果类型与流元素类型不同时,使用此版本。 我们需要提供返回类型的标识符和实现BiFunction接口的累加器;然后接收返回类型的一个对象和流元素来生成返回类型的值,以及一个组合函数。此函数实现BinaryOperator接口,并且接收返回类型的两个对象用于生成此类型的对象。因此,使用此版本方法来计算任意员工的列表中薪水超过50000的人数:
Integer value=0;
value=persons.parallelStream().reduce(value, (n,p) -> {
if (p.getSalary() > 50000) {
return n+1;
} else {
return n;
}
}, (n1,n2) -> n1+n2);
System.out.printf("The number of people with a salary bigger that 50,000 is %d\n",value);
}
}
工作原理
本范例中,学习如何使用Java流提供的不同的归约操作。首先,我们使用DoubleStream、IntStream、LongStream类提供的特定归约操作。这些操作用来计算流元素数量、流元素的总和、流元素平均值以及流的最大和最小值。如果使用一般的Stream操作,只包括count()方法来计算流元素。
然后我们用到Stream类提供的三个reduce()方法版本。第一个版本只接收BinaryOperator一个参数,通常会将运算符指定为lambda表达式,但也可以使用实现BinaryOperator接口的类的对象。此操作符将接收流的两个元素,然后生成相同类型的新元素。 例如,我们接收两个Point对象且生成一个新的Point对象。通过BinaryOperator实现的操作必须是组合的,也就是说,如下表达式必须为true:
(a op b) op c = a op (b op c)
op是范例中的BinaryOperator。
此版本的reduce()方法返回Optional对象,因为如果流没有元素的话,就没有结果值返回,这样Optional对象将为空。
第二个版本的reduce()方法接收标识值和BinaryOperator。与另一个版本的reduce()方法一样,BinaryOperator必须是组合的,标识值必须是true表达式:
identity op a = a op identity = a
这种情况下,reduce()方法返回一个流元素类型相同的元素,如果流没有元素,将返回标识值。
当想要返回与流元素类型不同的值时, 就用到最后一个版本的reduce()方法。此方法有三个参数,一个标识值、一个累加器操作符,和一个组合器操作符。累加器操作符接收返回类型的值和流元素,并生成返回类型的新对象。
组合器操作符接收两个返回类型的对象来计算出一个新的返回类型对象。标识值是返回类型的标识值,且需要验证如下表达式:
combiner (u, accumulator(identity, t)) == accumulator(u, t)
这里u是返回类型的对象,t是流对象。
下图显示本范例在控制台输出的执行信息:
扩展学习
我们已经用lambda表达式实现reduce()方法的所有参数。前两个版本的reduce()方法返回BinaryOperator,第三个版本接收BiFunction和BinaryOperator。如果想重用一个复杂的操作符,可以实现一个类,其实现必要的接口,并将该类的对象作为参数作用到这些接口以及Stream类的其它方法上。
更多关注
- 本章“创建不同来源的流”小节