Summary
New default methods min() and max() are added to the Comparator interface, which allow finding greater or smaller of two objects, according to this Comparator.
Problem
While we have Stream.min/max and Collections.min/max, often it's required to select a greater or smaller object of two. Doing this with existing APIs is unnecessarily complicated. E.g., given two objects a and b and a comparator comp, we have the following possibilities to find the maximal object:
comp.compare(a, b) > 0 ? a : b
This explicit version mentions both
a
andb
twice and not very readable, as it requires matching the sign (>
or<
) with right-hand operands of ?: to understand whether we are looking for maximum or minimumStream.of(a, b).max(comp).get()
Creates an unnecessary
Optional
which is always present and should be explicitly unwrapped. Also, depending on JIT compiler used and compilation tier, it may create unnecessary performance overhead and heap objects.BinaryOperator.maxBy(comp).apply(a, b)
While being the longest in terms of characters, it's probably the best of existing alternatives. However, it's not very discoverable and creates an unnecessary function when we just want to get the value. Also, this version would require an explicit type argument if the comparator is defined for a more abstract type:
Comparator<CharSequence> comp = Comparator.comparingInt(CharSequence::length);
// Here <String> is necessary
String max = BinaryOperator.<String>maxBy(comp).apply("long", "short");
Solution
It's suggested to extend the Comparator
interface adding two default methods:
public interface Comparator<T> {
...
default <U extends T> U max(U o1, U o2) { ... }
default <U extends T> U min(U o1, U o2) { ... }
}
The solution was preliminarily discussed in core-libs-dev in this thread: https://mail.openjdk.org/pipermail/core-libs-dev/2025-May/145638.html
Specification
/**
* Returns the greater of two values according to this comparator.
* If the arguments are equal with respect to this comparator,
* the {@code o1} argument is returned.
*
* @implSpec This default implementation behaves as if
* {@code compare(o1, o2) >= 0 ? o1 : o2}.
*
* @param o1 an argument.
* @param o2 another argument.
* @param <U> the type of the arguments and the result.
* @return the larger of {@code o1} and {@code o2} according to this comparator.
* @throws NullPointerException if an argument is null and this
* comparator does not permit null arguments
* @throws ClassCastException if the arguments' types prevent them from
* being compared by this comparator.
*
* @since 25
*/
default <U extends T> U max(U o1, U o2) { ... }
/**
* Returns the smaller of two values according to this comparator.
* If the arguments are equal with respect to this comparator,
* the {@code o1} argument is returned.
*
* @implSpec This default implementation behaves as if
* {@code compare(o1, o2) <= 0 ? o1 : o2}.
*
* @param o1 an argument.
* @param o2 another argument.
* @param <U> the type of the arguments and the result.
* @return the smaller of {@code o1} and {@code o2} according to this comparator.
* @throws NullPointerException if an argument is null and this
* comparator does not permit null arguments
* @throws ClassCastException if the arguments' types prevent them from
* being compared by this comparator.
*
* @since 25
*/
default <U extends T> U min(U o1, U o2) { ... }
Impact estimation
To estimate the possible source compatibility impact, I used an internal tool that performs a regular expression search over the open source Java repositories. The tool indexes 35,961,108 Java files in 309,661 repositories that contain Java code (forks and archived repositories are excluded).
The goal was to find Java classes or interfaces that extend/implement the Comparator
interface and declare a
max
method with a potentially conflicting signature. I've used the following regular expression:
(extends|implements)\s+Comparator.+\smax\s*\(
It may produce false-positives, but false-negatives are unlikely. Possible false-negatives may include
having a comment in a weird place (extends /*comment*/ Comparator
), using a fully-qualified name (extends java.util.Comparator
),
and so on. Also, it's possible that an anonymous class like new Comparator<>() {...}
is declared, but it's highly unlikely that
it will contain a max
method, as it's hard to call methods of anonymous classes from outside.
I haven't checked the min
method, but I expect the results to be similar. In fact, most of the found occurrences contained
both max
and min
methods.
The tool found 242 files (including 179 unique ones) from 150 repositories (0.05% of all repositories). Due to tooling limitations, I've got the content and location for only 164 files, which is 67.7% of all found files. I checked and categorized all of them manually. All the occurrences could be grouped into the following 8 categories.
The first four categories are basically no match (false-positive):
- NC = No Comparator: 'extends Comparator' is a part of comment, string literal, type parameter bound, etc.
- NM = No method: 'max(' substring is part of the call, part of comment, part of string, etc.
- MO = Method outside: 'max' method is declared outside the Comparator (in another class in the same file)
- SD = Signature differs: 'max' method is declared inside the Comparator but does not override the new 'max' method (e.g., different number of parameters)
The last four categories contain actual matches of the max
method with a compatible signature inside the Comparator
:
- OK = 'max' method overrides the new 'max' method in a compatible way. Typical code:
interface C<T> extends Comparator<T> {
<E extends T> E max(E a, E b);
}
- WA = Compilation warning due to unchecked conversion in return type. Typical code:
class C extends Comparator<String> {
public String max(String o1, String o2) { ... }
}
- EC = Compilation error due to the signature clash. Typical code
interface C<T> extends Comparator<T> {
T max(T a, T b);
}
- ES = Compilation error due to static 'max' method with compatible signature. Typical code
class C extends Comparator<String> {
public static String max(String o1, String o2) { ... }
}
Additionally, the following tags were added, for informational purposes:
- NO (15 occurrences) = Repository unavailable online (DCMA takedown, removed by owner, made private, etc.); I still checked the indexed copy of the file, as we have them in our tooling
- GO (44 occurrences) = Copy of Guava Ordering interface
- JC (28 occurrences) = Copy of java.util.Collections class (either from OpenJDK, or from Apache Harmony)
- GD (5 occurrences) = Copy of Groovy DefaultGroovyMethods class
Results
I discovered 7 files that are source-incompatible with the proposed change.
Of these, 5 files declare a derived generic Comparator interface but do not provide type parameter to min/max methods,
like Guava does. This could be easily fixed changing the min/max
method signature without touching the callsites
(about two lines of code needs to be updated). Alternatively, the min/max
methods could be removed in favor of
newly available standard methods.
Two Java files declare static methods with signature clash. Fixing the compatibility in this case might be harder, as callsites should be updated as well (e.g., the method could be renamed).
Additionally, 5 files were found where a new warning will be issued. These files declare a concrete comparator implementation
with a convenient min/max
methods without a type parameter. In most of the cases, it's ok just to remove the methods and
rely on the newly available default implementation.
As I categorized 67.7% of all occurrences, I estimate the number of processed Java repositories to be 309,661 * 67.7% = 209,640. So we can expect that:
- 0.0033% of Java repositories (7 of 209640) may have a compilation error after applying the proposed change.
- 0.0024% of Java repositories (5 of 209640) may have a compilation warning after applying the proposed change.
Raw analysis data
- csr of
-
JDK-8356995 Provide default methods min(T, T) and max(T, T) in Comparator interface
-
- Open
-