Как вычислить среднее число нескольких чисел в последовательности, используя Java 8 лямбда

Если у меня есть коллекции Point, как я могу вычислить среднее значение x, y, используя поток Java 8 на одной итерации.

В следующем примере создается два потока и итерации дважды в коллекции ввода для вычисления среднего значения x и y. Является ли их каким-либо образом средним компьютером x, y на одной итерации с использованием java 8 lambda

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();

Ответ 1

Если вы не возражаете использовать дополнительную библиотеку, мы добавили поддержку сборщиков кортежей в 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 или что-то подобное.

Я также подробно описал эту технику в ответе на другой вопрос о переполнении стека.

Ответ 2

Напишите тривиальный сборщик. Посмотрите на реализацию коллектора 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());

Ответ 3

Один из способов - определить класс, который агрегирует значения точек 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();

В то же время повторение двух раз в списке является простым решением. Я бы сделал это, если у меня действительно не проблема с производительностью.

Ответ 4

С текущим моментальным снимком 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

Ответ 5

Вот простейшее решение. Вы добавляете все значения x и y, используя метод "добавить" Point2D, а затем используйте метод "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;    
    }

Ответ 6

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, должны быть фактически окончательными, поэтому мы не можем использовать простой 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;)