Search code examples
javajava-8java-streamcollectors

Return in one string the min, max, average, sum, count of salaries with Stream and java8


I have a List of employees which are characterized by a salary. Why this code does not work?

String joined = employees.stream().collect(
    Collectors.summingInt(Employee::getSalary),
    Collectors.maxBy(Comparator.comparing(Employee::getSalary)),
    Collectors.minBy(Comparator.comparing(Employee::getSalary)),
    Collectors.averagingLong((Employee e) ->e.getSalary() * 2),
    Collectors.counting(),
    Collectors.joining(", "));

I'm using a suite of collectors.


Solution

  • Note that currently you're trying to get not the max/min salary, but the Employee having such salary. If you actually want to have the max/min salary itself (number), then these characteristics could be calculated at once using Collectors.summarizingInt():

    IntSummaryStatistics stats = employees.stream()
        .collect(Collectors.summarizingInt(Employee::getSalary));
    

    If you want to join them to string, you may use:

    String statsString = Stream.of(stats.getSum(), stats.getMax(), stats.getMin(), 
                                   stats.getAverage()*2, stats.getCount())
                               .map(Object::toString)
                               .collect(Collectors.joining(", "));
    

    If you actually want to get an Employee with max/min salary, here IntSummaryStatistics will not help you. However you may create the stream of collectors instead:

    String result = Stream.<Collector<Employee,?,?>>of(
                Collectors.summingInt(Employee::getSalary),
                Collectors.maxBy(Comparator.comparing(Employee::getSalary)),
                Collectors.minBy(Comparator.comparing(Employee::getSalary)),
                Collectors.averagingLong((Employee e) ->e.getSalary() * 2),
                Collectors.counting())
            .map(collector -> employees.stream().collect(collector))
            .map(Object::toString)
            .collect(Collectors.joining(", "));
    

    Note that in this way you will have an output like (depending on the Employee.toString() implementation:

    1121, Optional[Employee [salary=1000]], Optional[Employee [salary=1]], 560.5, 4
    

    Don't forget that maxBy/minBy return Optional.


    If you are unsatisfied with the first solution and for some reason don't want to iterate the input several times, you can create a combined collector using a method like this:

    /**
     * Returns a collector which joins the results of supplied collectors
     * into the single string using the supplied delimiter.
     */
    @SafeVarargs
    public static <T> Collector<T, ?, String> joining(CharSequence delimiter, 
            Collector<T, ?, ?>... collectors) {
        @SuppressWarnings("unchecked")
        Collector<T, Object, Object>[] cs = (Collector<T, Object, Object>[]) collectors;
        return Collector.<T, Object[], String>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]).toString())
                .collect(Collectors.joining(delimiter)));
    }
    

    Having such method you can write

    String stats = employees.stream().collect(joining(", ",
            Collectors.summingInt(Employee::getSalary),
            Collectors.maxBy(Comparator.comparing(Employee::getSalary)),
            Collectors.minBy(Comparator.comparing(Employee::getSalary)),
            Collectors.averagingLong((Employee e) ->e.getSalary() * 2),
            Collectors.counting()));