Java Stream Sort With Null Values: nullsFirst and nullsLast

To sort a Java Stream or List that contains null values, wrap the comparator in Comparator.nullsFirst() or nullsLast(). Examples for null elements, null fields, descending order and the reversed() trap.

To sort a Java Stream or List that contains null values, we wrap the comparator in Comparator.nullsFirst() or Comparator.nullsLast(), which put the null values at the start or at the end and compare the other values with the wrapped comparator. Without the wrapper, the sort throws a NullPointerException as soon as it compares a null.

We need a null-safe sort whenever the data comes with gaps, such as a list read from a database where some rows have no value, or a to-do app where some tasks have no due date.

The following example sorts a list of fruit names with two null elements, and a list of tasks where some due dates are null. The comment on each line shows the sorted result.

List<String> fruits = Arrays.asList("banana", null, "apple", "cherry", null);

List<String> unsafe = fruits.stream().sorted().toList();                                                       // NullPointerException
List<String> nullsFirst = fruits.stream().sorted(Comparator.nullsFirst(Comparator.naturalOrder())).toList();   // [null, null, apple, banana, cherry]
List<String> nullsLast = fruits.stream().sorted(Comparator.nullsLast(Comparator.naturalOrder())).toList();     // [apple, banana, cherry, null, null]
List<String> descending = fruits.stream().sorted(Comparator.nullsLast(Comparator.reverseOrder())).toList();    // [cherry, banana, apple, null, null]

List<Task> sortedTasks = tasks.stream()
    .sorted(Comparator.comparing(Task::dueDate, Comparator.nullsLast(Comparator.naturalOrder())))
    .toList();                    // [taxes(2026-10-15), rent(2026-11-01), passport(2027-01-20), milk(null), gym(null)]

Notice that the null check goes to the place where the null can appear. For null elements, we wrap the whole comparator, and for null fields, we pass nullsLast() as the second argument of Comparator.comparing().

Next, we look at how the two methods compare values and why a plain sorted() fails. After that, we sort custom objects with null elements, null fields or both. The last sections cover descending order with the reversed() trap, and sorting a List or an array in place.

1. How nullsFirst() and nullsLast() Work

Both methods are static methods of the Comparator interface, added in Java 8. Each one takes a comparator for the non-null values and returns a new null-friendly comparator.

In both cases, two null values are equal, and two non-null values go to the wrapped comparator. When the wrapped comparator itself is null, all non-null values are equal, which we use in section 5.2.

abnullsFirst(cmp).compare(a, b)nullsLast(cmp).compare(a, b)
nullnull00
null“apple”-11
“apple”null1-1
“apple”“banana”cmp.compare(“apple”, “banana”)cmp.compare(“apple”, “banana”)

Both methods return an instance of the JDK class Comparators.NullComparator. The field nullFirst is true for nullsFirst() and false for nullsLast(), and the field real holds the comparator that we passed in. The compare() method has only three branches.

public int compare(T a, T b) {
  if (a == null) {
    return (b == null) ? 0 : (nullFirst ? -1 : 1);
  } else if (b == null) {
    return nullFirst ? 1 : -1;
  } else {
    return (real == null) ? 0 : real.compare(a, b);
  }
}

public Comparator<T> reversed() {
  return new NullComparator<>(!nullFirst, real == null ? null : real.reversed());
}

The reversed() method flips nullFirst as well, so a reversed nullsLast() comparator puts the null values first. We come back to this in section 3.3.

A plain sorted() without a comparator uses the natural order, which calls compareTo() on the elements. When one of the elements is null, the call fails, and the whole stream pipeline stops with an exception.

java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "c1" is null

2. Sorting a Stream of Custom Objects

For custom objects, a null can appear in two places. Either an element of the list is null, or an element is present but one of its fields is null. Each case needs the null-friendly comparator in a different place.

The following example is a to-do app. Each task is a record with a title and an optional due date, and its toString() method prints the title with the due date in parentheses. The app shows the tasks with the nearest due date first.

public record Task(String title, LocalDate dueDate) {

  @Override
  public String toString() {
    return title + "(" + dueDate + ")";
  }
}
Task rent = new Task("rent", LocalDate.of(2026, 11, 1));
Task taxes = new Task("taxes", LocalDate.of(2026, 10, 15));
Task passport = new Task("passport", LocalDate.of(2027, 1, 20));
Task milk = new Task("milk", null);   // no due date
Task gym = new Task("gym", null);     // no due date

2.1. Some Objects in the List Are Null

A null element shows up when a list is filled from a lookup that found nothing, for example a method findTask(id) that returns null for a deleted task. The comparator Comparator.comparing(Task::dueDate) calls dueDate() on each element, so it throws a NullPointerException for a null element.

We wrap the comparator in nullsFirst() or nullsLast(). The wrapper checks the elements for null before comparing() calls dueDate().

List<Task> withNullTasks = Arrays.asList(rent, null, taxes, null, passport);

List<Task> sorted = withNullTasks.stream()
    .sorted(Comparator.nullsFirst(Comparator.comparing(Task::dueDate)))
    .toList();                    // [null, null, taxes(2026-10-15), rent(2026-11-01), passport(2027-01-20)]

2.2. Some Field Values Are Null

In the to-do app, the tasks milk and gym have no due date. The task objects are present, but dueDate() returns null, so comparing(Task::dueDate) calls compareTo() on a null date and throws the exception.

List<Task> tasks = List.of(rent, milk, taxes, gym, passport);

List<Task> unsafe = tasks.stream()
    .sorted(Comparator.comparing(Task::dueDate))
    .toList();                    // NullPointerException: ... because the return value of "java.util.function.Function.apply(Object)" is null

We have two ways to handle the null due dates.

2.2.1. Null Checks in a Custom Comparator

We can write the null checks ourselves in a class that implements Comparator. The class DueDateComparator puts the tasks without a due date at the end.

public class DueDateComparator implements Comparator<Task> {

  @Override
  public int compare(Task t1, Task t2) {
    if (t1.dueDate() == null && t2.dueDate() == null) {
      return 0;
    } else if (t1.dueDate() == null) {
      return 1;
    } else if (t2.dueDate() == null) {
      return -1;
    }
    return t1.dueDate().compareTo(t2.dueDate());
  }
}
List<Task> sorted = tasks.stream()
    .sorted(new DueDateComparator())
    .toList();                    // [taxes(2026-10-15), rent(2026-11-01), passport(2027-01-20), milk(null), gym(null)]

The tasks milk and gym keep their original order, because Stream.sorted() is stable for ordered streams such as a stream from a List. A stable sort keeps the order of elements that compare as equal.

2.2.2. Using Comparator.comparing() With nullsLast()

The method Comparator.comparing() has a second overload that takes a key comparator. The key comparator compares the extracted due dates, so we pass a null-friendly comparator there. The result is the same as with DueDateComparator, in one line.

Comparator<Task> byDueDate = Comparator.comparing(Task::dueDate, Comparator.nullsLast(Comparator.naturalOrder()));

List<Task> sorted = tasks.stream()
    .sorted(byDueDate)
    .toList();                    // [taxes(2026-10-15), rent(2026-11-01), passport(2027-01-20), milk(null), gym(null)]

We prefer comparing(key, nullsLast(…)) over a custom comparator class, because the JDK method has no branch that we can get wrong. A custom class is useful when the comparison has rules that the Comparator methods cannot express.

2.3. Null Objects and Null Fields Together

When the list can contain null tasks and tasks with a null due date, we need both checks. The inner nullsLast() handles the null due dates, and the outer nullsLast() handles the null tasks.

List<Task> both = Arrays.asList(rent, null, milk, taxes, null, gym);

List<Task> sorted = both.stream()
    .sorted(Comparator.nullsLast(byDueDate))
    .toList();                    // [taxes(2026-10-15), rent(2026-11-01), milk(null), gym(null), null, null]

The same outer wrapper works with the custom class, as in Comparator.nullsLast(new DueDateComparator()), which gives the same order. Without the outer wrapper, byDueDate calls dueDate() on a null task and throws a NullPointerException.

2.4. Sorting by a Second Field

The tasks without a due date compare as equal, so they stay in the order of the source list. To show them in a fixed order, we add a second sort key with thenComparing(). For more on chained comparators, read sorting a stream by multiple fields.

List<Task> sorted = tasks.stream()
    .sorted(byDueDate.thenComparing(Task::title))
    .toList();                    // [taxes(2026-10-15), rent(2026-11-01), passport(2027-01-20), gym(null), milk(null)]

The second key compares only the tasks with an equal due date, so gym now comes before milk. If the title can also be null, we write thenComparing(Task::title, Comparator.nullsLast(Comparator.naturalOrder())).

3. Sorting With Nulls in Natural Order

For built-in Java types such as wrapper classes and Strings, we sort by their natural ordering, which comes from their compareTo() method. The only thing we take care of is that a null value does not break the sort with a NullPointerException.

Note that primitive values such as int can never be null, so the problem appears only with wrapper types such as Integer and with object types.

3.1. Ordering the Nulls Last

We use Comparator.nullsLast(Comparator.naturalOrder()) to put the non-null values first in ascending order and the null values at the end.

List<String> fruits = Arrays.asList("banana", null, "apple", "cherry", null);

List<String> sorted = fruits.stream()
    .sorted(Comparator.nullsLast(Comparator.naturalOrder()))
    .toList();                    // [apple, banana, cherry, null, null]

We create the list with Arrays.asList(), because List.of() does not accept null elements, as section 5.3 explains.

3.2. Ordering the Nulls First

We use Comparator.nullsFirst(Comparator.naturalOrder()) to put the null values first and the non-null values after them in ascending order.

List<String> sorted = fruits.stream()
    .sorted(Comparator.nullsFirst(Comparator.naturalOrder()))
    .toList();                    // [null, null, apple, banana, cherry]

3.3. Descending Order With Nulls Last

For a descending sort, we wrap Comparator.reverseOrder() in nullsLast(). The values come in reverse order, and the null values stay at the end.

List<String> descending = fruits.stream()
    .sorted(Comparator.nullsLast(Comparator.reverseOrder()))
    .toList();                    // [cherry, banana, apple, null, null]

List<Task> latestFirst = tasks.stream()
    .sorted(Comparator.comparing(Task::dueDate, Comparator.nullsLast(Comparator.reverseOrder())))
    .toList();                    // [passport(2027-01-20), rent(2026-11-01), taxes(2026-10-15), milk(null), gym(null)]

Calling reversed() on the finished comparator gives a different result. As we saw in section 1, reversed() flips the position of the null values too, so they move to the front.

List<String> reversed = fruits.stream()
    .sorted(Comparator.nullsLast(Comparator.<String>naturalOrder()).reversed())
    .toList();                    // [null, null, cherry, banana, apple]

List<Task> reversedTasks = tasks.stream()
    .sorted(byDueDate.reversed())
    .toList();                    // [milk(null), gym(null), passport(2027-01-20), rent(2026-11-01), taxes(2026-10-15)]

For example, an orders page sorts by delivery date with the newest date first. A developer adds .reversed() to the existing ascending comparator, and the orders without a delivery date jump from the bottom of the page to the top. So we reverse the wrapped comparator, not the null-friendly one.

3.4. Removing the Nulls Before Sorting

When the null values carry no information, we remove them with filter(Objects::nonNull) and sort the rest with the natural order.

List<String> withoutNulls = fruits.stream()
    .filter(Objects::nonNull)
    .sorted()
    .toList();                    // [apple, banana, cherry]

4. Sorting a List or an Array in Place

The same comparators work outside of streams. The method List.sort() sorts a modifiable list in place, and Arrays.sort() does the same for an object array. Without a comparator, Collections.sort(list) uses the natural order and throws a NullPointerException for a null element, like sorted().

List<String> list = new ArrayList<>(fruits);
list.sort(Comparator.nullsLast(Comparator.naturalOrder()));             // [apple, banana, cherry, null, null]

String[] array = {"banana", null, "apple"};
Arrays.sort(array, Comparator.nullsFirst(Comparator.naturalOrder()));   // [null, apple, banana]

The method Stream.toList() (Java 16) returns an unmodifiable list, so calling sort() on it throws an UnsupportedOperationException. We copy such a list into an ArrayList before we sort it in place. For more sorted() variants, read Stream sorted() examples.

5. Sorting With Nulls FAQs

5.1. Why Does reversed() Put the Nulls First?

Because reversed() inverts every comparison, including the comparisons with null. The null-friendly comparator returns a new comparator with the opposite null position, as the JDK source in section 1 shows. To sort in descending order with the null values last, we write Comparator.nullsLast(Comparator.reverseOrder()).

5.2. How Do I Move Nulls to the End Without Sorting the Other Values?

We pass null as the wrapped comparator. The comparator Comparator.nullsLast(null) treats all non-null values as equal, and the stable sort keeps them in their original order.

List<String> nullsMoved = fruits.stream()
    .sorted(Comparator.nullsLast(null))
    .toList();                    // [banana, apple, cherry, null, null]

5.3. Can List.of() Hold Null Values?

No. The methods List.of(), Set.of() and Map.of() throw a NullPointerException when an element is null, whereas Stream.toList() accepts null elements. For test data with null values, we use Arrays.asList() or an ArrayList.

List<String> rejected = List.of("banana", null);         // NullPointerException
List<String> accepted = Arrays.asList("banana", null);   // [banana, null]

5.4. How Do I Sort Strings Ignoring Case With Nulls?

We wrap String.CASE_INSENSITIVE_ORDER in nullsLast() or nullsFirst(). The case-insensitive comparator throws a NullPointerException for null values on its own, like the natural order.

List<String> mixedCase = Arrays.asList("banana", null, "Apple", "cherry");

List<String> sorted = mixedCase.stream()
    .sorted(Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER))
    .toList();                    // [Apple, banana, cherry, null]

6. Conclusion

A sort calls compare() or compareTo() on the elements, so a null element or a null sort key ends in a NullPointerException. The methods Comparator.nullsFirst() and Comparator.nullsLast() wrap a comparator and decide where the null values go.

For null elements, we wrap the whole comparator. For null fields, we pass the null-friendly comparator as the key comparator of Comparator.comparing(), and when both can be null, we use both wrappers.

For a descending order, we wrap Comparator.reverseOrder() and don’t call reversed() on the null-friendly comparator, because reversed() moves the null values to the other end.

7. 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.