Explain Codes LogoExplain Codes Logo

Java 8 stream's .min() and .max(): why does this compile?

java
method-references
autoboxing
sam
Nikita BarsukovbyNikita Barsukov·Feb 25, 2025
TLDR

The functionality of Java 8's .min() and .max() methods in handling method references such as Integer::min or Integer::max largely stems from target typing. The concept of type inference lends a hand here, allowing method references to be determined based on their placement and role within the given context.

As an example, consider this Comparator usage:

Stream<Integer> numbers = Stream.of(3, 1, 4); Optional<Integer> smallest = numbers.min(Integer::compare); // Cracks the case - finds the smallest!

In this scenario, Integer::compare acts as a Comparator<Integer> because its signature aligns with the Comparator functional interface.

Diving into method referencing and SAM (Single Abstract Method)

Interfaces with Single Abstract Method (SAM), such as Comparator, can be implemented using lambda expressions or method references. The underlying concept here is that the Comparator has a functional signature that fits perfectly with the method reference Integer::compare, enabling seamless use of Stream.min() and Stream.max().

Autoboxing and surprisingly successful method references

It might seem puzzling initially, but method references to Integer.min() and Integer.max() compile successfully. This is made possible by the principle of autoboxing, enabling these method references to be used where a Comparator<Integer> is expected.

Comparator<Integer> minComparator = Integer::min; // Quite daring to take the leap! Comparator<Integer> maxComparator = Integer::max; // Leaps and lands successfully!

Even though it doesn't logically imply a comparator for identifying the minimum or maximum, the compilation happens due to autoboxing and SAM conversion. The semantics are the catch here – while Integer.min() and Integer.max() qualify as static methods needing two integers and returning one (matching the Comparator.compare() method signature), they don't compare two integers in the traditional sense. For that, you'd typically use Integer::compare.

Be cautious with your comparators

The choice of Comparator when employing .min() and .max() should be made with extra care to avoid potential pitfalls. Misuse of Integer.min() and Integer.max() might lead to subtle bugs in your code. Remember to stick to Integer::compare for a logically sound comparison setup.

Practical tricks and tips for Stream comparisons

The ally - BinaryOperator

A great alternative to consider when working with streams and zeroing in on the minimum or maximum value is BinaryOperator<Integer>.

BinaryOperator<Integer> minOperator = Integer::min; int smallestValue = numbers.reduce(Integer.MAX_VALUE, minOperator); // Back to basics - arithmetic min!

The reduce operation used here with Integer.MAX_VALUE and minOperator effectively zeroes in on the smallest integer within the stream.

Deploy Collectors when Comparator falls short

There might be moments where the nature of the stream content doesn't allow for directly employing Comparator with Stream.min() or Stream.max(). In such cases, Collectors.minBy and Collectors.maxBy come to the rescue.

Optional<Integer> smallestValue = numbers.collect(Collectors.minBy(Integer::compare)); Optional<Integer> largestValue = numbers.collect(Collectors.maxBy(Integer::compare));

Similar in function to Stream.min() and Stream.max(), this approach uses collectors that offer additional flexibility and options in managing the mutable reduction process.