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

后端 未结 3 418
没有蜡笔的小新
没有蜡笔的小新 2021-02-03 11:18

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         


        
相关标签:
3条回答
  • 2021-02-03 11:45

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

    0 讨论(0)
  • 2021-02-03 11:54

    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));
        }
    }
    
    0 讨论(0)
  • 2021-02-03 11:55

    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));
    
    0 讨论(0)
提交回复
热议问题