Reputation: 161
If I have collections Point , how do I compute average of x,y using Java 8 stream on a single iteration.
Following example creates two stream & iterates twice on the input collection to compute the average of x & y. Is their any way to computer average x,y on single iteration using 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();
Upvotes: 12
Views: 9690
Reputation: 2373
Just an update since Java 12 there is quite a nice solution to this using teeing collector. The code would look like this
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);
}
}
and the output will be Point2D.Double[5.5, 6.95]
Upvotes: 2
Reputation: 95346
Write a trivial collector. Look at the implementation of the averagingInt
collector (from 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);
}
This can be easily adapted to sum along two axes instead of one (in a single pass), and return the result in some simple holder:
AverageHolder h = streamOfPoints.collect(averagingPoints());
Upvotes: 8
Reputation: 559
Here is the simplest solution. You add up all values of x and y using "add" method of Point2D and then use "multiply" method to get average. Code should be like this
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;
}
Upvotes: 1
Reputation: 220832
If you don't mind using an additional library, we've added support for tuple collectors to jOOλ, recently.
Tuple2<Double, Double> avg = points.stream().collect(
Tuple.collectors(
Collectors.averagingDouble(p -> p.x),
Collectors.averagingDouble(p -> p.y)
)
);
In the above code, Tuple.collectors()
combines several java.util.stream.Collector
instances into a single Collector
that collects individual values into a Tuple
.
This is much more concise and reusable than any other solution. The price you'll pay is that this currently operates on wrapper types, instead of primitive double
. I guess we'll have to wait until Java 10 and project valhalla for primitive type specialisation in generics.
In case you want to roll your own, instead of creating a dependency, the relevant method looks like this:
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)
)
);
}
Where Tuple2
is just a simple wrapper for two values. You might as well use AbstractMap.SimpleImmutableEntry
or something similar.
I've also detailed this technique in an answer to another Stack Overflow question.
Upvotes: 8
Reputation: 2272
With the current 1.2.0 snapshot of Javaslang you may write
import javaslang.collection.List;
List.of(points)
.unzip(p -> Tuple.of(p.x, p.y))
.map((l1, l2) -> Tuple.of(l1.average(), l2.average())));
Unfortunately Java 1.8.0_31 has a compiler bug which does not compile it :'(
You get a Tuple2 avgs which contains the computed values:
double xAvg = avgs._1;
double yAvg = avgs._2;
Here is the general behavior of 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
Upvotes: 2
Reputation: 26828
avarage()
is a reduction operation, so on generic streams you would use reduce()
. The problem is that it doesn't offer a finishing operation. If you want to calculate the average by first summing up all values and then dividing them by their count it gets a bit trickier.
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();
Some notes:
counter[]
has to be an array because variables used by lambdas have to be effectively final, so we can't use a simple int
.
This version of reduce()
returns an Optional
, so we have to use get()
to get the value. If the stream can be empty then get()
would throw an exception obviously, but we can use the Optional
to our advantage then.
I'm not entirely sure if this works with parallel streams.
You could also do the following. It's probably less accurate but may be better suited if you have a lot of really, really large numbers:
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;
});
On the other hand, if accuracy was a big concern you wouldn't use float anyway ;)
Upvotes: 0
Reputation: 93842
One way would be to define a class that aggregates the points' x and y values.
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();
}
}
Then you just collect the Stream
into a new instance:
AggregatePoints agg = points.stream().collect(AggregatePoints::new,
AggregatePoints::add,
AggregatePoints::merge);
double xAvg = agg.averageX();
double yAvg = agg.averageY();
Alhough iterating two times on the list is a simple solution. I would do it unless I really have a performance problem.
Upvotes: 4