Java 8 Streams: How to call once the Collection.stream() method and retrieve an array of several aggregate values with different fields

谁说我不能喝 提交于 2019-12-02 23:48:38

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));
    }
}
Lukas Eder

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

How does 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λ.

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));
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!