如何使用Java 8 lambda按顺序计算多个数字的平均值

如果我有集合Point,如何在单次迭代中使用Java 8流计算x,y的平均值。

下面的示例在输入集合上创建两个流和迭代两次,以计算x和y的平均值。 他们用计算机平均x,y在使用java 8 lambda的单次迭代中的任何方式:

List points = Arrays.asList(new Point2D.Float(10.0f,11.0f), new Point2D.Float(1.0f,2.9f)); // java 8, iterates twice double xAvg = points.stream().mapToDouble( p -> px).average().getAsDouble(); double yAvg = points.stream().mapToDouble( p -> py).average().getAsDouble(); 

如果你不介意使用额外的库,我们最近已经为jOOλ添加了对元组收集器的支持。

 Tuple2 avg = points.stream().collect( Tuple.collectors( Collectors.averagingDouble(p -> px), Collectors.averagingDouble(p -> py) ) ); 

在上面的代码中, Tuple.collectors()将几个java.util.stream.Collector实例组合到一个Collector ,该收集Collector将各个值收集到一个Tuple

这比任何其他解决方案都更简洁和可重用。 你需要支付的价格是,它目前在包装类型上运行,而不是原始的double 。 我想我们将不得不等到Java 10并将valhalla项目用于generics中的原始类型专业化 。

如果您想要自己滚动,而不是创建依赖项,相关方法如下所示:

 static  Collector, Tuple2> collectors( Collector collector1 , Collector collector2 ) { return Collector.of( () -> tuple( collector1.supplier().get() , collector2.supplier().get() ), (a, t) -> { collector1.accumulator().accept(a.v1, t); collector2.accumulator().accept(a.v2, t); }, (a1, a2) -> tuple( collector1.combiner().apply(a1.v1, a2.v1) , collector2.combiner().apply(a1.v2, a2.v2) ), a -> tuple( collector1.finisher().apply(a.v1) , collector2.finisher().apply(a.v2) ) ); } 

其中Tuple2只是两个值的简单包装器。 您也可以使用AbstractMap.SimpleImmutableEntry或类似的东西。

我还在回答另一个Stack Overflow问题时详细介绍了这种技术。

写一个琐碎的collections家。 查看averagingInt收集器的实现(来自Collectors.java):

 public static  Collector averagingInt(ToIntFunction mapper) { return new CollectorImpl<>( () -> new long[2], (a, t) -> { a[0] += mapper.applyAsInt(t); a[1]++; }, (a, b) -> { a[0] += b[0]; a[1] += b[1]; return a; }, a -> (a[1] == 0) ? 0.0d : (double) a[0] / a[1], CH_NOID); } 

这可以很容易地适应沿两个轴而不是一个轴(在一次通过中)相加,并将结果返回到一个简单的持有者:

 AverageHolder h = streamOfPoints.collect(averagingPoints()); 

一种方法是定义一个聚合点的x和y值的类。

 public class AggregatePoints { private long count = 0L; private double sumX = 0; private double sumY = 0; public double averageX() { return sumX / count; } public double averageY() { return sumY / count; } public void merge(AggregatePoints other) { count += other.count; sumX += other.sumX; sumY += other.sumY; } public void add(Point2D.Float point) { count += 1; sumX += point.getX(); sumY += point.getY(); } } 

然后你只需将Stream收集到一个新实例中:

  AggregatePoints agg = points.stream().collect(AggregatePoints::new, AggregatePoints::add, AggregatePoints::merge); double xAvg = agg.averageX(); double yAvg = agg.averageY(); 

尽管在列表上迭代两次是一个简单的解决方案。 除非我确实遇到性能问题,否则我会这样做。

使用Javaslang的当前1.2.0快照,您可以写

 import javaslang.collection.List; List.of(points) .unzip(p -> Tuple.of(px, py)) .map((l1, l2) -> Tuple.of(l1.average(), l2.average()))); 

不幸的是,Java 1.8.0_31有一个编译器错误,无法编译它:’(

你得到一个包含计算值的Tuple2 avgs:

 double xAvg = avgs._1; double yAvg = avgs._2; 

以下是average()的一般行为:

 // = 2 List.of(1, 2, 3, 4).average(); // = 2.5 List.of(1.0, 2.0, 3.0, 4.0).average(); // = BigDecimal("0.5") List.of(BigDecimal.ZERO, BigDecimal.ONE).average(); // = UnsupportedOpertationException("average of nothing") List.nil().average(); // = UnsupportedOpertationException("not numeric") List.of("1", "2", "3").average(); // works well with java.util collections final java.util.Set set = new java.util.HashSet<>(); set.add(1); set.add(2); set.add(3); set.add(4); List.of(set).average(); // = 2 

这是最简单的解决方案。 使用Point2D的“add”方法将x和y的所有值相加,然后使用“multiply”方法得到平均值。 代码应该是这样的

  int size = points.size(); if (size != 0){ Point2D center = points.parallelStream() .map(Body::getLocation) .reduce( new Point2D(0, 0), (a, b) -> a.add(b) ) .multiply( (double) 1/size ); return center; } 

avarage()是一个简化操作,因此在generics流上你会使用reduce() 。 问题是它不提供精加工操作。 如果你想通过首先总结所有值然后除以它们的计数来计算平均值,那么它会变得有点棘手。

 List points = Arrays.asList(new Point2D.Float(10.0f,11.0f), new Point2D.Float(1.0f,2.9f)); int counter[] = {1}; Point2D.Float average = points.stream().reduce((avg, point) -> { avg.x += point.x; avg.y += point.y; ++counter[0]; if (counter[0] == points.size()) { avg.x /= points.size(); avg.y /= points.size(); } return avg; }).get(); 

一些注意事项: counter[]必须是一个数组,因为lambdas使用的变量必须是有效的,所以我们不能使用简单的int

这个版本的reduce()返回一个Optional ,所以我们必须使用get()来获取值。 如果流可以为空,那么get()显然会抛出exception,但我们可以使用Optional来获得优势。

我不完全确定这是否适用于并行流。

您也可以执行以下操作。 它可能不太准确但如果你有很多非常非常大的数字可能更适合:

 double factor = 1.0 / points.size(); Point2D.Float average = points.stream().reduce(new Point2D.Float(0.0f,0.0f), (avg, point) -> { avg.x += point.x * factor; avg.y += point.y * factor; return avg; }); 

另一方面,如果准确性是一个大问题,你不会使用浮动;)