如何使用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 super T> 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; });
另一方面,如果准确性是一个大问题,你不会使用浮动;)