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

12

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

以下示例创建了两个流,并在输入集合上迭代两次以计算x和y的平均值。是否有任何方法可以使用Java 8 lambda在单次迭代中计算x和y的平均值:

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

以下示例创建了两个流,并在输入集合上迭代两次以计算x和y的平均值。是否有任何方法可以使用Java 8 lambda在单次迭代中计算x和y的平均值:

List<Point2D.Float> 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 -> p.x).average().getAsDouble();
double yAvg = points.stream().mapToDouble( p -> p.y).average().getAsDouble();
7个回答

8
如果您不介意使用额外的库,我们最近为 jOOλ 添加了元组收集器支持。
Tuple2<Double, Double> avg = points.stream().collect(
    Tuple.collectors(
        Collectors.averagingDouble(p -> p.x),
        Collectors.averagingDouble(p -> p.y)
    )
);

在上面的代码中,Tuple.collectors()将几个java.util.stream.Collector实例组合成一个单一的Collector,将各个值收集到一个Tuple中。这比其他任何解决方案都更简洁和可重用。你需要付出的代价是目前它只能操作包装类型,而不是原始的double。我想我们得等到Java 10和Valhalla项目为泛型特化提供基本类型支持

如果你想自己创建而不是创建依赖项,则相关方法如下:

static <T, A1, A2, D1, D2> Collector<T, Tuple2<A1, A2>, Tuple2<D1, D2>> collectors(
    Collector<T, A1, D1> collector1
  , Collector<T, A2, D2> 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问题的答案中详细说明了这种技术。


7

编写一个简单的收集器。查看averagingInt收集器的实现(来自Collectors.java):

public static <T> Collector<T, ?, Double>
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());

2
Java 7风格(命令式)对我来说看起来更简单,如果您还想使用并行流并行运行平均值,则您的解决方案可能很好。 // java 7 float xSum = 0; float ySum = 0; for (Point2D.Float float1 : points) { xSum += float1.x; xSum += float1.y; } float xAvg = xSum / points.size(); float yAvg = ySum / points.size(); - madhub

4

一个方法是定义一个类,聚合点的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();

虽然在列表上迭代两次是一个简单的解决方案,但除非我真的遇到了性能问题,否则我不会这样做。


2

在当前的1.2.0快照版本Javaslang中,你可以编写以下代码:

import javaslang.collection.List;

List.of(points)
        .unzip(p -> Tuple.of(p.x, p.y))
        .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<Integer> set = new java.util.HashSet<>();
set.add(1);
set.add(2);
set.add(3);
set.add(4);
List.of(set).average(); // = 2

1
"很不幸,Java 1.8.0_31存在编译器错误,导致无法编译它。" - 其中一些错误已在1.8.0_40-ea-b21版本中得到修复。 - Lukas Eder
谢谢Lukas,听起来很棒! - Daniel Dietrich

1
这是最简单的解决方案。您可以使用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;    
    }

1

Java 12自带一个相当不错的解决方案,使用teeeing collector即可。代码如下:

import java.awt.geom.Point2D;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

class Scratch {
    public static void main(String[] args) {
        List<Point2D.Double> points = Arrays.asList(
                new Point2D.Double(10.0,11.0),
                new Point2D.Double(1.0,2.9)
        );

        Point2D.Double averagePoint = points.stream()
                .collect(Collectors.teeing(
                        Collectors.averagingDouble(point -> point.getX()),
                        Collectors.averagingDouble(point -> point.getY()),
                        (avgX, avgY) -> new Point2D.Double(avgX, avgY)
                        ));

        System.out.println(averagePoint);
    }
}

输出将是 Point2D.Double[5.5, 6.95]


0

avarage() 是一个归约操作,因此在通用流上,您应该使用 reduce()。问题是它不提供完成操作。如果您想通过首先将所有值相加然后除以它们的计数来计算平均值,则会变得有些棘手。

List<Point2D.Float> 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[]必须是一个数组,因为lambda使用的变量必须是有效的最终变量,所以我们不能使用简单的int
这个版本的reduce()返回一个Optional,所以我们必须使用get()来获取值。如果流可以为空,那么get()显然会抛出异常,但是我们可以利用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;
                         });

另一方面,如果准确性是一个重要问题,你也不会使用浮点数 ;)


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