Java Stream max(): Find the Largest Element With a Comparator

Java Stream max() examples with comparators, max by field, ties, empty streams, groups and primitive streams, plus comparators that return wrong results.

Decision chart for finding a maximum in Java, Stream.max with a comparator for the whole object, mapToInt and max for the number only, Collectors.maxBy inside groupingBy, and Collections.max for a plain collection

The Java Stream max(comparator) method is a terminal operation that returns the largest element of a stream according to the given Comparator, wrapped in an Optional, which stays empty for an empty stream. The Comparator decides what largest means, so the same method finds the highest number, the longest string or the object with the biggest field value.

We use max() for questions such as the longest ride in a fitness app, the most expensive order of the day or the newest version in a list of releases.

The following example finds the largest number, the rider with the longest ride and the result for an empty stream.

List<Integer> distances = List.of(42, 65, 30);
Optional<Integer> longest = distances.stream().max(Comparator.naturalOrder());     // Optional[65]
int longestKm = distances.stream().max(Integer::compare).orElseThrow();             // 65
List<Ride> rides = List.of(new Ride("Asha", "red", 42, LocalDate.of(2026, 6, 1)), new Ride("Ben", "blue", 65, LocalDate.of(2026, 6, 2)), new Ride("Chen", "red", 65, LocalDate.of(2026, 6, 3)), new Ride("Dina", "blue", 30, LocalDate.of(2026, 6, 3)), new Ride("Eli", "green", 55, LocalDate.of(2026, 6, 4)));
String topRider = rides.stream().max(Comparator.comparingInt(Ride::km)).map(Ride::rider).orElse("nobody");   // "Ben"
int maxPrimitive = IntStream.of(42, 65, 30).max().orElse(0);                        // 65
Optional<Integer> none = Stream.<Integer>empty().max(Comparator.naturalOrder());   // Optional.empty

Notice that Ben and Chen both rode 65 km and max() returned Ben, the first of them. We cover the signature and the comparators that work (and some that compile but give wrong answers), the max by a field, ties, empty streams and groups, and close with primitive streams and the alternatives to max().

1. How Stream max() Picks the Largest Element

The signature is Optional<T> max(Comparator<? super T> comparator). The method reads every element once, keeps the larger of the current winner and the next element, and returns the winner at the end. Internally, max() is a reduction, and the JDK implements it as reduce(BinaryOperator.maxBy(comparator)).

The result is an Optional because an empty stream has no largest element. As a terminal operation, max() consumes the stream, so a second call on the same stream object throws IllegalStateException.

The examples use rides logged in a cycling app, where each ride has a rider, a team, a distance in km and a date.

record Ride(String rider, String team, int km, LocalDate date) {}
Decision chart for finding a maximum in Java, Stream.max with a comparator for the whole object, mapToInt and max for the number only, Collectors.maxBy inside groupingBy, and Collections.max for a plain collection
We call max() on the object stream when we need the whole element, and on a primitive stream when we need only the number

2. Comparators for max() That Work and Some That Do Not

Any Comparator works with max(), as long as it returns a negative number, zero or a positive number in a consistent way. The common choices map to a few factory methods.

ElementsComparatorLargest element
Integer, String, LocalDate and other Comparable typesComparator.naturalOrder()The highest value, the last string alphabetically, the latest date
IntegerInteger::compareThe highest value
String by lengthComparator.comparingInt(String::length)The longest string
Objects by an int fieldComparator.comparingInt(Ride::km)The object with the biggest field value
Objects by a Comparable fieldComparator.comparing(Ride::date)The object with the latest date
Integer highest = Stream.of(42, 65, 30).max(Comparator.naturalOrder()).orElseThrow();            // 65
String lastName = Stream.of("Dina", "Asha", "Ben").max(Comparator.naturalOrder()).orElseThrow();    // "Dina"
String longestName = Stream.of("Eli", "Chen", "Ben").max(Comparator.comparingInt(String::length)).orElseThrow();   // "Chen"

Three comparators show up in tutorials and old code although they give wrong results. All three compile, which makes them dangerous.

  • The lambda (a, b) -> a – b overflows when the two numbers are far apart, and the overflowed difference has the wrong sign.
  • The method reference Integer::max fits the Comparator type by accident. It returns the larger number instead of a comparison result, so max() keeps the first element in most cases.
  • The comparator Comparator.reverseOrder() turns max() into min(), which is correct Java but rarely what the reader of the code expects.
Integer overflow = Stream.of(-2_000_000_000, 2_000_000_000).max((a, b) -> a - b).orElseThrow();    // -2000000000
Integer notMax = Stream.of(3, 7, 5).max(Integer::max).orElseThrow();                                 // 3
Integer smallest = Stream.of(3, 7, 5).max(Comparator.reverseOrder()).orElseThrow();                  // 3
Integer correct = Stream.of(-2_000_000_000, 2_000_000_000).max(Integer::compare).orElseThrow();      // 2000000000

We compare numbers with Integer::compare or Comparator.naturalOrder(), never with subtraction or Integer::max. The method Integer.max() belongs in reduce(Integer::max), where it combines two values instead of comparing them.

3. Finding the Max by a Field of an Object

For objects, we pass a comparator that reads the field. The result is the whole object, so we can show any of its fields. When we need only the number, mapping to a primitive stream first is shorter and avoids the Optional of an object.

List<Ride> rides = List.of(new Ride("Asha", "red", 42, LocalDate.of(2026, 6, 1)), new Ride("Ben", "blue", 65, LocalDate.of(2026, 6, 2)), new Ride("Chen", "red", 65, LocalDate.of(2026, 6, 3)), new Ride("Dina", "blue", 30, LocalDate.of(2026, 6, 3)), new Ride("Eli", "green", 55, LocalDate.of(2026, 6, 4)));
Ride longestRide = rides.stream().max(Comparator.comparingInt(Ride::km)).orElseThrow();
String longestBy = longestRide.rider();                                                     // "Ben"
int longestDistance = rides.stream().mapToInt(Ride::km).max().orElse(0);                    // 65
String latestRider = rides.stream().max(Comparator.comparing(Ride::date)).map(Ride::rider).orElse("none");   // "Eli"

The last line calls map() on the Optional to read one field and supplies a default for an empty list in the same expression. Eli logged the last ride, on June 4, so the latest ride is his. For an int field, comparingInt() compares without boxing each value into an Integer, whereas comparing() boxes.

4. Which Element Wins a Tie

When the comparator returns 0 for two elements, the JDK keeps the element it already holds, so the first of the equal elements wins. The Javadoc does not promise that, though. When a tie must have a fixed winner, we add a second key with thenComparing().

List<Ride> rides = List.of(new Ride("Asha", "red", 42, LocalDate.of(2026, 6, 1)), new Ride("Ben", "blue", 65, LocalDate.of(2026, 6, 2)), new Ride("Chen", "red", 65, LocalDate.of(2026, 6, 3)), new Ride("Dina", "blue", 30, LocalDate.of(2026, 6, 3)), new Ride("Eli", "green", 55, LocalDate.of(2026, 6, 4)));
String firstOfTie = rides.stream().max(Comparator.comparingInt(Ride::km)).map(Ride::rider).orElseThrow();                                       // "Ben"
String byNameToo = rides.stream().max(Comparator.comparingInt(Ride::km).thenComparing(Ride::rider)).map(Ride::rider).orElseThrow();             // "Chen"
String earliest = rides.stream().max(Comparator.comparingInt(Ride::km).thenComparing(Ride::date, Comparator.reverseOrder())).map(Ride::rider).orElseThrow();   // "Ben"

A leaderboard often needs every tied rider, not one of them. Grouping the riders by distance into a TreeMap and taking its last entry finds all of them in a single pass, because a TreeMap keeps its keys sorted.

List<Ride> rides = List.of(new Ride("Asha", "red", 42, LocalDate.of(2026, 6, 1)), new Ride("Ben", "blue", 65, LocalDate.of(2026, 6, 2)), new Ride("Chen", "red", 65, LocalDate.of(2026, 6, 3)), new Ride("Dina", "blue", 30, LocalDate.of(2026, 6, 3)), new Ride("Eli", "green", 55, LocalDate.of(2026, 6, 4)));
List<String> allLongest = rides.stream().collect(Collectors.groupingBy(Ride::km, TreeMap::new, Collectors.mapping(Ride::rider, Collectors.toList()))).lastEntry().getValue();   // [Ben, Chen]

5. Handling Empty Streams and null Elements

An empty stream returns Optional.empty, and the Optional methods decide what happens next. Calling get() or orElseThrow() on an empty result throws NoSuchElementException, so we pick the method that matches what an empty input means for our code.

List<Integer> noRides = List.of();
Optional<Integer> result = noRides.stream().max(Integer::compare);                                   // Optional.empty
Integer orZero = noRides.stream().max(Integer::compare).orElse(0);                                    // 0
Integer withMessage = noRides.stream().max(Integer::compare).orElseThrow(() -> new IllegalStateException("no rides logged"));   // IllegalStateException: no rides logged
String label = noRides.stream().max(Integer::compare).map(km -> km + " km").orElse("no rides yet");   // "no rides yet"
Integer unsafe = noRides.stream().max(Integer::compare).get();                                        // NoSuchElementException: No value present

A dashboard tile can show 0 or a text such as “no rides yet”, whereas a monthly payout job that pays the top rider must stop with a clear message. The method ifPresent() fits code that acts on the winner only when one exists.

Elements that are null cause a NullPointerException, either inside the comparator or because an Optional cannot hold null. Removing them before the comparison is the safe fix.

List<Integer> withGaps = Arrays.asList(42, null, 65);
Optional<Integer> crash = withGaps.stream().max(Comparator.naturalOrder());                           // NullPointerException
Integer safeMax = withGaps.stream().filter(Objects::nonNull).max(Comparator.naturalOrder()).orElse(0);   // 65
Optional<Integer> nullWins = withGaps.stream().max(Comparator.nullsLast(Comparator.naturalOrder()));  // NullPointerException

The last line fails although the comparator handles null, because nullsLast() ranks null as the largest value and max() cannot wrap it in an Optional.

6. Finding the Max per Group

To find the longest ride of each team, we combine Collectors.groupingBy() with Collectors.maxBy(), the collector form of max(). Each group gets an Optional, which collectingAndThen() unwraps, because every group has at least one element.

List<Ride> rides = List.of(new Ride("Asha", "red", 42, LocalDate.of(2026, 6, 1)), new Ride("Ben", "blue", 65, LocalDate.of(2026, 6, 2)), new Ride("Chen", "red", 65, LocalDate.of(2026, 6, 3)), new Ride("Dina", "blue", 30, LocalDate.of(2026, 6, 3)), new Ride("Eli", "green", 55, LocalDate.of(2026, 6, 4)));
Map<String, String> bestPerTeam = rides.stream().collect(Collectors.groupingBy(Ride::team, TreeMap::new, Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparingInt(Ride::km)), best -> best.orElseThrow().rider())));   // {blue=Ben, green=Eli, red=Chen}
Map<String, Integer> maxKmPerTeam = rides.stream().collect(Collectors.toMap(Ride::team, Ride::km, Math::max, TreeMap::new));   // {blue=65, green=55, red=65}

The second line needs only the number per team, so toMap() with Math::max as the merge function is enough. Here Math::max is correct, because a merge function combines two values, unlike the comparator misuse from section 2. The TreeMap::new argument keeps the teams sorted, since a HashMap has no fixed order.

7. max() on IntStream, LongStream and DoubleStream

Each of the primitive streams offers a max() without arguments, because primitive values have one numeric order. The result is an OptionalInt, OptionalLong or OptionalDouble, and orElse() or orElseThrow() reads it.

OptionalInt maxInt = IntStream.of(42, 65, 30).max();                                  // OptionalInt[65]
long maxLong = LongStream.of(3_000_000_000L, 12L).max().orElseThrow();                  // 3000000000
char maxChar = (char) "rides".chars().max().orElseThrow();                              // 's'
OptionalDouble withNaN = DoubleStream.of(21.5, Double.NaN, 18.0).max();                 // OptionalDouble[NaN]
double cleanMax = DoubleStream.of(21.5, Double.NaN, 18.0).filter(d -> !Double.isNaN(d)).max().orElse(0);   // 21.5

The method DoubleStream.max() returns NaN when any element is NaN. A broken sensor reading can therefore hide every real value, so we filter NaN out when the data comes from measurements.

8. max() Compared With Other Ways to Find the Largest Value

Java offers several ways to find a maximum, with different return types and different behavior for an empty input. For min() next to max() and more combined examples, see the article on Stream max() and min().

OptionReturnsEmpty inputBest for
stream.max(comparator)Optional<T>Optional.emptyThe whole element after filter() or map()
mapToInt(…).max()OptionalIntOptionalInt.emptyThe number only
reduce(Integer::max)Optional<Integer>Optional.emptySame as max(), more verbose
Collectors.maxBy(comparator)Optional<T>Optional.emptyInside groupingBy() or teeing()
Collections.max(collection, comparator)TNoSuchElementExceptionA collection that needs no stream steps
sorted(reversed).findFirst()Optional<T>Optional.emptyAvoid, it sorts everything to read one element
List<Integer> distances = List.of(42, 65, 30);
Integer viaCollections = Collections.max(distances);                                           // 65
Optional<Integer> viaReduce = distances.stream().reduce(Integer::max);                         // Optional[65]
Optional<Integer> viaSort = distances.stream().sorted(Comparator.reverseOrder()).findFirst();  // Optional[65]

The sorting version gives the same answer but sorts all elements, which is O(n log n) work and keeps every element in memory, whereas max() reads each element once.

9. Real-World Example of a Ride of the Week

A cycling app shows a ride of the week badge on its home screen. The badge goes to the longest ride within the week, and when two rides have the same distance the earlier ride wins, because that rider reached the distance first. A week without rides shows a placeholder text instead of an error.

static Optional<Ride> rideOfTheWeek(List<Ride> rides, LocalDate weekStart) {
    LocalDate weekEnd = weekStart.plusDays(7);
    return rides.stream()
        .filter(r -> !r.date().isBefore(weekStart) && r.date().isBefore(weekEnd))
        .max(Comparator.comparingInt(Ride::km)
            .thenComparing(Ride::date, Comparator.reverseOrder()));
}
List<Ride> rides = List.of(new Ride("Asha", "red", 42, LocalDate.of(2026, 6, 1)), new Ride("Ben", "blue", 65, LocalDate.of(2026, 6, 2)), new Ride("Chen", "red", 65, LocalDate.of(2026, 6, 3)), new Ride("Dina", "blue", 30, LocalDate.of(2026, 6, 3)), new Ride("Eli", "green", 55, LocalDate.of(2026, 6, 4)));
String badge = rideOfTheWeek(rides, LocalDate.of(2026, 6, 1)).map(r -> r.rider() + ", " + r.km() + " km").orElse("No rides yet");   // "Ben, 65 km"
String quietWeek = rideOfTheWeek(rides, LocalDate.of(2026, 7, 1)).map(r -> r.rider() + ", " + r.km() + " km").orElse("No rides yet");   // "No rides yet"

The method returns Optional<Ride> instead of a Ride or null, so every caller has to decide what an empty week looks like. The reversed date comparator makes the earlier ride the larger one, so Ben’s ride on June 2 beats Chen’s ride on June 3.

10. Stream max() FAQs

Integer lists, empty results, null values and dates account for most follow-up questions on max().

10.1. How Do We Get the Max Value From a List of Integers?

For a list without other processing, Collections.max(list) is the shortest call. Inside a pipeline, list.stream().max(Integer::compare) or mapToInt(Integer::intValue).max() gives the same value as an Optional.

List<Integer> scores = List.of(12, 48, 31);
Integer top = Collections.max(scores);                                    // 48
int topOfEven = scores.stream().filter(n -> n % 2 == 0).mapToInt(Integer::intValue).max().orElse(0);   // 48

10.2. Why Does max() Return an Optional Instead of the Value?

Because an empty stream has no largest element, and returning null would hide that case until a later NullPointerException. The Optional forces the caller to choose a default, an exception or a conditional action at the point of the call.

10.3. Can Stream max() Return null?

No. The method returns either a non-empty Optional or Optional.empty. If the largest element is null, max() throws NullPointerException instead, as shown in section 5.

10.4. How Do We Find the Object With the Latest Date?

We pass Comparator.comparing() with the date accessor, because LocalDate, LocalDateTime and Instant compare in time order. The article on getting the object with the max date covers more date types.

List<Ride> rides = List.of(new Ride("Asha", "red", 42, LocalDate.of(2026, 6, 1)), new Ride("Ben", "blue", 65, LocalDate.of(2026, 6, 2)), new Ride("Chen", "red", 65, LocalDate.of(2026, 6, 3)), new Ride("Dina", "blue", 30, LocalDate.of(2026, 6, 3)), new Ride("Eli", "green", 55, LocalDate.of(2026, 6, 4)));
LocalDate lastRide = rides.stream().map(Ride::date).max(Comparator.naturalOrder()).orElseThrow();   // 2026-06-04

11. Conclusion

The call stream.max(comparator) returns the largest element as an Optional, and the comparator alone defines what largest means. We build that comparator with naturalOrder(), Integer::compare or comparingInt(), and we avoid subtraction and Integer::max, which compile but return wrong elements.

Ties go to the first element in the JDK, so a second key with thenComparing() makes the winner predictable. Empty streams need an explicit orElse() or orElseThrow(), null elements need a filter, and NaN values decide every DoubleStream.max() they appear in. For groups we use maxBy() or toMap() with Math::max. Other stream operations are listed in the Java Streams guide.

12. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.