Back to Stream: Sorting & Comparator
Q23Question 23 of 23

Sort employees with nulls-last for a nullable field using Comparator.nullsLast

Key Concepts
  • Comparator.nullsLast(naturalOrder()) treats null as greater than any non-null value, pushing nulls to the end
  • Without nullsLast, a plain comparator throws NullPointerException on null field values
  • nullsFirst is the mirror image: nulls sort to the top instead of the bottom
  • Use comparing(keyExtractor, nullsLast(naturalOrder())) to apply null-safety to a specific field
  • nullsLast protects against null field values but not null Employee objects; filter those separately
  • Add a thenComparing tie-breaker so same-city employees (including the null group) are ordered predictably

Definition

`nullsLast(naturalOrder())` sorts real values normally and pushes nulls to the end instead of crashing.
  • Comparator.nullsLast(wrapped) returns a null-friendly comparator treating null as greater than any non-null value.
  • nullsFirst(wrapped) is the mirror: nulls sort to the top.
  • Without null-safety: Comparator.comparing(getCity) throws NullPointerException on a null city.
  • Usage: comparing(Employee::getCity, nullsLast(naturalOrder())) safely sorts on a nullable key.
  • Scope: protects against null field values, not null Employee objects.

Some employees at the National Bank of India have not filled in their city yet, so that field is blank. Priya wants the list sorted by city A to Z, but everyone with a missing city should sit at the bottom rather than crash the sort.

Understanding the Concepts

Why does a null field crash the sort?

A plain Comparator.comparing(Employee::getCity) calls compareTo on the extracted key. If the key is null, compareTo throws a NullPointerException. nullsLast intercepts null values before they reach the wrapped comparator.

How do you sort with nulls at the end?

JAVA
package com.bank.hr;

import java.util.*;
import java.util.stream.Collectors;

public class NullsLastSort {
    public static void main(String[] args) {
        List<Employee> staff = List.of(
            new Employee("Rajesh",  "Mumbai"),
            new Employee("Priya",   null),
            new Employee("Amit",    "Delhi"),
            new Employee("Kavita",  null),
            new Employee("Suresh",  "Chennai"));

        List<Employee> sorted = staff.stream()
            .sorted(Comparator.comparing(Employee::getCity,
                Comparator.nullsLast(Comparator.naturalOrder()))
                .thenComparing(Employee::getName))
            .collect(Collectors.toList());

        sorted.forEach(e -> System.out.printf("%-8s %s%n",
            e.getName(), e.getCity() != null ? e.getCity() : "(none)"));
    }
}
TEXT
Suresh   Chennai
Amit     Delhi
Rajesh   Mumbai
Kavita   (none)
Priya    (none)

How is nullsLast different from nullsFirst?

| Comparator | Null placement | Non-null ordering |
|---|---|---|
| nullsLast(naturalOrder()) | Nulls at the end | Alphabetical |
| nullsFirst(naturalOrder()) | Nulls at the top | Alphabetical |
| nullsLast(reverseOrder()) | Nulls at the end | Reverse alphabetical |

JAVA
// Nulls first instead of last
List<Employee> nullsTop = staff.stream()
    .sorted(Comparator.comparing(Employee::getCity,
        Comparator.nullsFirst(Comparator.naturalOrder())))
    .collect(Collectors.toList());

What are common mistakes?

Mistake: forgetting to handle nulls and crashing the sort.

JAVA
// WRONG: NPE when city is null
staff.stream()
    .sorted(Comparator.comparing(Employee::getCity))  // NPE!
    .collect(Collectors.toList());

// CORRECT: null-safe comparator
staff.stream()
    .sorted(Comparator.comparing(Employee::getCity,
        Comparator.nullsLast(Comparator.naturalOrder())))
    .collect(Collectors.toList());

What about null Employee objects?

nullsLast protects against null field values, but not null Employee objects in the stream. Filter those separately.

JAVA
// Guard against null Employee objects AND null city
staff.stream()
    .filter(Objects::nonNull)
    .sorted(Comparator.comparing(Employee::getCity,
        Comparator.nullsLast(Comparator.naturalOrder())))
    .collect(Collectors.toList());

What about adding a name tie-breaker within the null group?

Without a tie-breaker, employees in the null-city group appear in an arbitrary order. Adding thenComparing(Employee::getName) makes the null group alphabetical.

What should a senior engineer know?

  • nullsLast is a decorator: it checks for null before delegating to the wrapped comparator.
  • It applies to the extracted key value, not the stream element itself.
  • For stable, predictable reports, always add a deterministic tie-breaker.
  • In a data migration scenario, null fields are common and must be handled explicitly.

What are common interview follow-up questions?

  • "What does nullsFirst do?" Sends nulls to the top instead of the bottom.
  • "Does nullsLast protect against null Employee objects?" No, only null field values.
  • "Can you combine nullsLast with reversed?" Yes: nullsLast(reverseOrder()) sorts non-nulls in reverse.

Visual Overview

DIAGRAM
  Without nullsLast:
  comparing(getCity) on null → NullPointerException!

  With nullsLast(naturalOrder()):
  ┌────────────────────────────────────────────┐
  │  Chennai < Delhi < Mumbai < null < null    │
  │  ↑ alphabetical non-nulls ↑  ↑ nulls end ↑│
  └────────────────────────────────────────────┘

  With nullsFirst(naturalOrder()):
  ┌────────────────────────────────────────────┐
  │  null < null < Chennai < Delhi < Mumbai    │
  │  ↑ nulls top ↑  ↑ alphabetical non-nulls ↑│
  └────────────────────────────────────────────┘

Key Takeaway

When sorting on a field that can be null I wrap the key comparator with Comparator.nullsLast, which treats null as greater than any real value so employees with a missing city sort to the end instead of crashing with a NullPointerException.

In my banking project at the National Bank of India this came from a half-finished data migration where some employee records had no city. The directory report needed those incomplete records visible but parked at the bottom, and nullsLast did exactly that.

nullsFirst is the mirror image for nulls at the top. nullsLast guards field values, not the Employee object itself, so I filter null objects with Objects::nonNull first. I always add a name tie-breaker so the null group is also ordered predictably.

Remember one sentence: comparing(getCity, nullsLast(naturalOrder())) sorts non-nulls alphabetically and pushes nulls to the end without crashing.

---

Share
Use ← → arrow keys to navigate