12

如果我有集合 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();
4

7 回答 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)
        )
    );
}

WhereTuple2只是两个值的简单包装器。您不妨使用AbstractMap.SimpleImmutableEntry或类似的东西。

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

于 2015-01-30T20:42:36.797 回答
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());
于 2015-01-13T21:45:31.373 回答
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();

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

于 2015-01-13T15:53:55.853 回答
2

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

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
于 2015-01-31T13:42:19.783 回答
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;    
    }
于 2015-02-05T04:06:54.367 回答
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[]必须是一个数组,因为 lambdas 使用的变量必须是有效的 final,所以我们不能使用简单的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;
                         });

另一方面,如果准确性是一个大问题,那么您无论如何都不会使用 float ;)

于 2015-01-13T19:12:05.250 回答
0

只是自 Java 12 以来的一次更新,使用teeing 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]

于 2021-05-28T08:09:48.860 回答