Home > other >  Java stream min/max of separate variables
Java stream min/max of separate variables

Time:05-07

How can I determine both the min and max of different attributes of objects in a stream?

I've seen answers on how get min and max of the same variable. I've also seen answers on how to get min or max using a particular object attribute (e.g. maxByAttribute()). But how do I get both the min of all the "x" attributes and the max of all the "y" attributes of objects in a stream?

Let's say I have a Java Stream<Span> with each object having a Span.getStart() and Span.getEnd() returning type long. (The units are irrelevant; it could be time or planks on a floor.) I want to get the minimum start and the maximum end, e.g. to represent the minimum span covering all the spans. Of course, I could create a loop and manually update mins and maxes, but is there a concise and efficient functional approach using Java streams?

Note that I don't want to create intermediate spans! If you want to create some intermediate Pair<Long> instance that would work, but for my purposes the Span type is special and I can't create more of them. I just want to find the minimum start and maximum end.

Bonus for also showing whether this is possible using the new Java 12 teeing(), but for my purposes the solution must work in Java 8 .

CodePudding user response:

Assuming that all data is valid (end > start) you can create LongSummaryStatistics object containing such information as min/max values, average, etc., by using summaryStatistics() as a terminal operation.

List<Span> spans = // initiliazing the source
    
LongSummaryStatistics stat = spans.stream()
    .flatMapToLong(span -> LongStream.of(span.getStart(), span.getEnd()))
    .summaryStatistics();
        
long minStart = stat.getMin();
long maxEnd = stat.getMax();

Note that if the stream source would be empty (you can check it by invoking stat.getCount(), which will give the number of consumed elements), min and max attributes of the LongSummaryStatistics object would have their default values, which are maximum and minimum long values respectively.


That is how it could be done using collect() and picking max and min values manually:

long[] minMax = spans.stream()
    .collect(() -> new long[2],
        (long[] arr, Span span) -> { // consuming the next value
            arr[0] = Math.min(arr[0], span.getStart());
            arr[1] = Math.max(arr[1], span.getEnd());
        },
        (long[] left, long[] right) -> { // merging partial results produced in different threads
            left[0] = Math.min(left[0], right[0]);
            left[1] = Math.max(left[1], right[1]);
        });

In order to utilize Collectors.teeing() you need to define two collectors and a function. Every element from the stream will be consumed by both collectors at the same time and when they are done, merger function will grab their intermediate results and will produce the final result.

In the example below, the result is Optional of map entry. In case there would be no elements in the stream, the resulting optional object would be empty as well.

List<Span> spans = List.of(new Span(1, 3), new Span(3, 6), new Span(7, 9));
        
Optional<Map.Entry<Long, Long>> minMaxSpan = spans.stream()
    .collect(Collectors.teeing(
        Collectors.minBy(Comparator.comparingLong(Span::getStart)),
        Collectors.maxBy(Comparator.comparingLong(Span::getStart)),
        (Optional<Span> min, Optional<Span> max) ->
            min.isPresent() ? Optional.of(new AbstractMap.SimpleEntry<>(min.get().getStart(), max.get().getEnd())) : Optional.empty()));
        
minMaxSpan.ifPresent(System.out::println);

Output

1=9

As an alternative data-carrier, you can use a Java 16 record:

public record MinMax(long start, long end) {}

Getters in the form start() and end() will be generated by the compiler.

CodePudding user response:

I am afraid for pre Java 12 you need to operate on the given Stream twice.

Given a class Span

@Getter
@AllArgsConstructor
@ToString
static class Span {
    int start;
    int end;
}

and a list of spans

List<Span> spanList = List.of(new Span(1,2),new Span(3,4),new Span(5,1));

you could do something like below for java 8:

Optional<Integer> minimumStart = spanList.stream().map(Span::getStart).min(Integer::compareTo);
Optional<Integer> maximumEnd = spanList.stream().map(Span::getEnd).max(Integer::compareTo);

For Java 12 as you already noticed you can use the built-in teeing collector like:

HashMap<String, Integer> result = spanList.stream().collect(
        Collectors.teeing(
                Collectors.minBy(Comparator.comparing(Span::getStart)),
                Collectors.maxBy(Comparator.comparing(Span::getEnd)),
                (min, max) -> {
                    HashMap<String, Integer> map = new HashMap();
                    map.put("minimum start", min.get().getStart());
                    map.put("maximum end", max.get().getEnd());
                    return map;
                }
        ));

System.out.println(result);

CodePudding user response:

Here is a Collectors.teeing solution using a record as the Span class.

record Span(long getStart, long getEnd) {
}

List<Span> spans = List.of(new Span(10,20), new Span(30,40));
  • the Collectors in teeing are built upon each other. In this case
  • mapping - to get the longs out of the Span class
  • maxBy, minBy - takes a comparator to get the max or min value as appropriate Both of these return optionals so get must be used.
  • merge operation - to merge the results of the teed collectors.
  • final results are placed in a long array
long[] result =
        spans.stream()
                .collect(Collectors.teeing(
                        Collectors.mapping(Span::getStart,
                                Collectors.minBy(
                                        Long::compareTo)),
                        Collectors.mapping(Span::getEnd,
                                Collectors.maxBy(
                                        Long::compareTo)),
                        (a, b) -> new long[] { a.get(),
                                b.get() }));

System.out.println(Arrays.toString(result));

prints

[10, 40]

You can also use collectingAndThen to put them in an array after get the values from Summary statistics.

long[] results = spans.stream().flatMap(
        span -> Stream.of(span.getStart(), span.getEnd()))
        .collect(Collectors.collectingAndThen(
                Collectors.summarizingLong(Long::longValue),
                stats -> new long[] {stats.getMin(),
                        stats.getMax()}));

System.out.println(Arrays.toString(results));

prints

[10, 40]
  • Related