I\'m starting with the Stream API in Java 8.
Here is my Person object I use:
public class Person {
private String firstName;
private String last
This is a bit tricky to solve with standard JDK 8 API, which doesn't offer many ways to compose Collector
types. If you're willing to use a third party library like jOOλ, you could write:
Tuple4<Long, Optional<Integer>, Optional<Double>, Optional<Double>> result =
Seq.seq(personsList)
.collect(
filter(p -> p.getFirstName().equals("John"), count()),
max(Person::getAge),
min(Person::getHeight),
avg(Person::getWeight)
);
System.out.println(result);
The above yields:
(2, Optional[35], Optional[1.8], Optional[75.0])
Note, it's using the new Agg.filter() method, which is similar to the JDK 9 Collectors.filtering()
method and works like this:
public static <T, A, R> Collector<T, A, R> filter(
Predicate<? super T> predicate, Collector<T, A, R> downstream) {
return Collector.of(
downstream.supplier(),
(c, t) -> {
if (predicate.test(t))
downstream.accumulator().accept(c, t);
},
downstream.combiner(),
downstream.finisher()
);
}
collect(collector1, collector2, ...)
work?If you don't want to use the above third-party library, you can write your own Collector
combining utility. An example that combines two collectors into a Tuple2
collector:
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.<T, Tuple2<A1, A2>, Tuple2<D1, D2>>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)
)
);
}
Disclaimer: I work for the company behind jOOλ.
Here is the collector
public class PersonStatistics {
private long firstNameCounter;
private int maxAge = Integer.MIN_VALUE;
private double minHeight = Double.MAX_VALUE;
private double totalWeight;
private long total;
private final Predicate<Person> firstNameFilter;
public PersonStatistics(Predicate<Person> firstNameFilter) {
Objects.requireNonNull(firstNameFilter);
this.firstNameFilter = firstNameFilter;
}
public void accept(Person p) {
if (this.firstNameFilter.test(p)) {
firstNameCounter++;
}
this.maxAge = Math.max(p.getAge(), maxAge);
this.minHeight = Math.min(p.getHeight(), minHeight);
this.totalWeight += p.getWeight();
this.total++;
}
public PersonStatistics combine(PersonStatistics personStatistics) {
this.firstNameCounter += personStatistics.firstNameCounter;
this.maxAge = Math.max(personStatistics.maxAge, maxAge);
this.minHeight = Math.min(personStatistics.minHeight, minHeight);
this.totalWeight += personStatistics.totalWeight;
this.total += personStatistics.total;
return this;
}
public Object[] toStatArray() {
return new Object[]{firstNameCounter, maxAge, minHeight, total == 0 ? 0 : totalWeight / total};
}
}
You can use this collector as follows
public class PersonMain {
public static void main(String[] args) {
List<Person> personsList = new ArrayList<>();
personsList.add(new Person("John", "Doe", 25, 180, 80));
personsList.add(new Person("Jane", "Doe", 30, 169, 60));
personsList.add(new Person("John", "Smith", 35, 174, 70));
personsList.add(new Person("John", "T", 45, 179, 99));
Object[] objects = personsList.stream().collect(Collector.of(
() -> new PersonStatistics(p -> p.getFirstName().equals("John")),
PersonStatistics::accept,
PersonStatistics::combine,
PersonStatistics::toStatArray));
System.out.println(Arrays.toString(objects));
}
}
Without third-party libraries you may create a universal collector which combines the results of any number of specified collectors into single Object[]
array:
/**
* Returns a collector which combines the results of supplied collectors
* into the Object[] array.
*/
@SafeVarargs
public static <T> Collector<T, ?, Object[]> multiCollector(
Collector<T, ?, ?>... collectors) {
@SuppressWarnings("unchecked")
Collector<T, Object, Object>[] cs = (Collector<T, Object, Object>[]) collectors;
return Collector.<T, Object[], Object[]> of(
() -> Stream.of(cs).map(c -> c.supplier().get()).toArray(),
(acc, t) -> IntStream.range(0, acc.length).forEach(
idx -> cs[idx].accumulator().accept(acc[idx], t)),
(acc1, acc2) -> IntStream.range(0, acc1.length)
.mapToObj(idx -> cs[idx].combiner().apply(acc1[idx], acc2[idx])).toArray(),
acc -> IntStream.range(0, acc.length)
.mapToObj(idx -> cs[idx].finisher().apply(acc[idx])).toArray());
}
For your concrete problem you'll also need a filtering()
collector (which will be added in JDK-9, see JDK-8144675):
public static <T, A, R> Collector<T, A, R> filtering(
Predicate<? super T> filter, Collector<T, A, R> downstream) {
BiConsumer<A, T> accumulator = downstream.accumulator();
Set<Characteristics> characteristics = downstream.characteristics();
return Collector.of(downstream.supplier(), (acc, t) -> {
if(filter.test(t)) accumulator.accept(acc, t);
}, downstream.combiner(), downstream.finisher(),
characteristics.toArray(new Collector.Characteristics[characteristics.size()]));
}
Now you can build a collector which will generate the final result:
Collector<Person, ?, Object[]> collector =
multiCollector(
filtering(p -> p.getFirstName().equals("John"), counting()),
collectingAndThen(mapping(Person::getAge,
maxBy(Comparator.naturalOrder())), Optional::get),
collectingAndThen(mapping(Person::getHeight,
minBy(Comparator.naturalOrder())), Optional::get),
averagingDouble(Person::getWeight));
Object[] result = personsList.stream().collect(collector);
System.out.println(Arrays.toString(result));