Comparator.nullsFirst(cmp) and Comparator.nullsLast(cmp) are decorator comparators that handle null inputs explicitly — placing nulls before or after non-null values — and delegate to the inner comparator for non-null pairs. Without them, comparing null throws NullPointerException.
The problem
List<String> names = Arrays.asList("Bob", null, "Alice", null);
names.sort(Comparator.naturalOrder());
// throws NullPointerException — String.compareTo(null) NPEs
The fix
names.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
// [null, null, "Alice", "Bob"]
names.sort(Comparator.nullsLast(Comparator.naturalOrder()));
// ["Alice", "Bob", null, null]
How they work
nullsFirst(cmp) returns a comparator that:
- If both args are non-null, delegates to
cmp.compare(a, b). - If both are null, returns 0.
- If only
ais null, returns negative (a comes first). - If only
bis null, returns positive (a comes after).
nullsLast does the inverse: nulls sort to the end.
Nullable fields, not just nullable arguments
The common case is sorting by an optional field:
public record Customer(String name, LocalDate lastOrder) {}
// lastOrder can be null for never-ordered customers
customers.sort(
Comparator.comparing(
Customer::lastOrder,
Comparator.nullsLast(Comparator.naturalOrder())
)
);
// Customers with orders sorted by date; never-ordered customers at the end.
comparing takes an optional second argument: a comparator for the extracted key. Passing nullsLast(naturalOrder()) here lets the key (lastOrder) be null without crashing.
Chaining with null-safe keys
Comparator<Customer> cmp = Comparator
.comparing(Customer::name, Comparator.nullsFirst(Comparator.naturalOrder()))
.thenComparing(Customer::lastOrder, Comparator.nullsLast(Comparator.naturalOrder()));
Each key gets its own null policy. Name nulls go first; date nulls go last.
Why this matters in practice
- Real datasets have null/missing values. Sorting them without handling nulls crashes on the first null.
Collectors.toMaprejects null values by default — butgroupingByallows them, and the resultingMap<K, List<V>>may have nullK.- Database results often have nullable columns. Mapping them to sort-friendly types means handling null in the comparator.
Alternative: filter nulls first
customers.stream()
.filter(c -> c.lastOrder() != null)
.sorted(Comparator.comparing(Customer::lastOrder))
.toList();
Sometimes the cleaner approach — but you lose the null entries from the result. nullsLast keeps them visible at the end.