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