Java Stream min and max

Introduction

In this post, we will learn about using Java Stream min and max methods to find the minimum and the maximum element in a Java Stream.

Java Stream min and max

The Java Stream class has the min and the max operations. We have to pass a Comparator to these methods (method signature is below). With these we can find the minimum and the maximum element in a Java Stream as per the ordering enforced by the passed comparator.

Optional<T> min(Comparator<? super T> comparator);

Optional<T> max(Comparator<? super T> comparator);

It returns the resulting minimum or maximum element as an Optional.

Finding the minimum element in a Java Stream

Let us start with a List<String> which has a list of fruit names. We will create a stream out of this and use the Stream#min to find the minimum element among the stream elements.

List<String> fruits = List.of("orange", "pear", "apple", "fig");
Optional<String> min = fruits.stream()
        .min(Comparator.naturalOrder());
System.out.println(min); //Optional[apple]

We pass Comparator.naturalOrder() as the comparator to the min method. Hence, this will return the first fruit name when all elements are ordered as per the natural ordering (for strings it is lexicographical ordering).

Another example is shown below. Here, we use Comparator.comparingInt and pass a method reference String::length to map each string value (fruit name) to its length. When we pass this comparator to the Stream#min method, it will return the minimum element (i.e., the first element) when all fruit names are ordered as per their length. Hence, we get ‘fig’ as the result.

Optional<String> min = fruits.stream()
        .min(Comparator.comparingInt(String::length));
System.out.println(min); //Optional[fig]

You can learn about various methods in the Comparator class in the Comparator comparing post.

If we call min on an empty stream, we will get an empty Optional.

Optional<String> min = Stream.<String>of()
        .min(Comparator.naturalOrder());
System.out.println(min); //Optional.empty

Stream#min on a stream of custom object

Here we use a record to model a simple Student type with id and name as the record components. (Records were added in Java 14 as a preview feature and finalized as a language feature in Java 16. You can learn about records in detail in the Record class in Java post. If you are using a Java version lower than 14, you can create a simple POJO to model the Student type).

public record Student(int id, String name) {
}

We have a list of student objects as shown below.

List<Student> students = List.of(
        new Student(1, "Joe"),
        new Student(2, "Scott"),
        new Student(3, "Adam"),
        new Student(4, "Chris")
);

We then create a stream from the students list and pass a comparator to the Stream#min. The comparator uses comparingInt method by extracting the id field of the student object. Thus, it returns the Student with the smallest id value.

Optional<Student> minStudent = students.stream()
        .min(Comparator.comparingInt(Student::id));
System.out.println(minStudent); //Optional[Student[id=1, name=Joe]]

The below code uses Comparator.comparing with a function (as a method reference) to extract the student name from each student instance in the stream. As a result, we will get back the student instance, whose name will appear first when all the student names are considered in lexicographical order (i.e., the min as per the enforced ordering).

Optional<Student> minStudent = students.stream()
        .min(Comparator.comparing(Student::name));
System.out.println(minStudent); //Optional[Student[id=3, name=Adam]]

Finding the maximum element in a Java Stream

We will not look into using Stream#max to get the maximum element from a Java Stream as per the ordering dictated by the passed Comparator. We will use the same examples as in the previous section.

Using the same list of fruit names, we pass the Comparator.naturalOrder comparator to the Stream#max method. This will return the last fruit (the max value) when it sorts/orders the fruit names in their natural order (lexicographical order). Thus we get ‘pear’ as the result.

List<String> fruits = List.of("orange", "pear", "apple", "fig");
Optional<String> max = fruits.stream()
        .max(Comparator.naturalOrder());
System.out.println(max); //Optional[pear]

If we use a comparator to order the stream elements (strings) as per their length, then we will get ‘orange’ as the result (as it is the longest in terms of the length).

Optional<String> max = fruits.stream()
        .max(Comparator.comparingInt(String::length));
System.out.println(max); //Optional[orange]

Finally (as seen before for Stream#min), if we use Stream#max on an empty stream, we get an empty Optional as the result.

Optional<String> max = Stream.<String>of()
        .max(Comparator.naturalOrder());
System.out.println(max); //Optional.empty

Stream#max on a stream of custom object

Let us use the same list of students.

List<Student> students = List.of(
        new Student(1, "Joe"),
        new Student(2, "Scott"),
        new Student(3, "Adam"),
        new Student(4, "Chris")
);

As seen before, using Comparator.comparingInt(Student::id) as the comparator will impose an ordering based on the Student id. Passing this to the Stream#max will return the student with the largest id value.

Optional<Student> maxStudent = students.stream()
        .max(Comparator.comparingInt(Student::id));
System.out.println(maxStudent); //Optional[Student[id=4, name=Chris]]

Finally, passing Comparator.comparing(Student::name) comparator to the max method will return the student whose name will appear last when the student names are considered in lexicographical order.

maxStudent = students.stream()
        .max(Comparator.comparing(Student::name));
System.out.println(maxStudent); //Optional[Student[id=2, name=Scott]]

Stream’s min/max cannot be null

The result of Stream#min or Stream#max cannot be null. If so, it will throw a NullPointerException.

Let us use the same fruits example and add a null at the end of the list as shown below.

List<String> fruits = List.of("orange", "pear", "apple", "fig");
List<String> fruitsWithNull = new ArrayList<>(fruits);
fruitsWithNull.add(null);

In the below code, we have the naturalOrder comparator wrapped in Comparator.nullsLast and hence null will be the last element. Passing this comparator to Stream#min doesn’t cause any issue. But if we use Comparator.nullsFirst comparator, then the first element (the min element) will be null. Hence, the call will throw a NullPointerException.

System.out.println(fruitsWithNull.stream()
                .min(Comparator.nullsLast(Comparator.naturalOrder()))); //Optional[apple]

//Exception in thread "main" java.lang.NullPointerException
System.out.println(fruitsWithNull.stream()
        .min(Comparator.nullsFirst(Comparator.naturalOrder())));

Similarly, using Comparator.nullsFirst comparator with Stream#max will not cause any issue and will correctly return the max element (’pear’ in this example). But if we use Comparator.nullsLast comparator, since null is the last element (the max element), it will throw a NullPointerException as shown below.

System.out.println(fruitsWithNull.stream()
                .max(Comparator.nullsFirst(Comparator.naturalOrder()))); //Optional[pear]

//Exception in thread "main" java.lang.NullPointerException
System.out.println(fruitsWithNull.stream()
        .max(Comparator.nullsLast(Comparator.naturalOrder())));

Conclusion

In this post, we learnt about using Java Stream min and max methods. Check out the other Java8 stream operations.

Leave a Reply