2

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

2
  • What is the data type of start and end? Commented May 6, 2022 at 16:02
  • Let's say they are long values. I've updated the question accordingly. Commented May 6, 2022 at 16:03

3 Answers 3

2

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(Map.entry(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.

Sign up to request clarification or add additional context in comments.

7 Comments

Your example shows me how to get the min start. It doesn't show getting the max end. And either maxEnd = stat.getMin() is a typo, or you missed one of the requirements.
@GarretWilson Sorry, it's a typo.
Still your code doesn't even call span.getEnd(), so I don't know how I could be getting the max of it.
Thanks for fixing the typos. Oh, I see; you're just putting all the mins and maxes into a single stream, and then finding the min and max values (via summaryStatistics()) from the flattened values. You no longer care which is Span.getStart() or Span.getEnd(), or if they overlap. You just want the min and max value. Interesting approach.
@GarretWilson Sure, I expect that start and end will overlap, but as I've said if start will be always less than end it will have no impact on finding the max end and min start.
|
0

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

Comments

0

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]

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.