Java Stream Sorted: Ordering Streams with Comparators
java stream sorted: Learn how to use Java's stream sorted() method for natural and custom ordering, including comparators, null handling, and performance tradeoffs.
When you need to order the elements of a stream, java stream sorted is the operation that handles it. The sorted() intermediate operation returns a stream whose elements are ordered according to natural ordering or a provided comparator. Like all intermediate operations, it is lazy: no sorting happens until a terminal operation such as collect(), forEach(), or toList() triggers the pipeline.
List<String> names = List.of("carol", "alice", "bob"); List<String> sorted = names.stream() .sorted() .collect(Collectors.toList()); // [alice, bob, carol]
The no-argument variant requires that the element type implements Comparable. For String, Integer, Long, and other standard types, natural ordering is well-defined. If the element type does not implement Comparable, the pipeline throws a ClassCastException at terminal operation time, not when sorted() is called.
Sorting with a Custom Comparator
When natural ordering is not what you need, pass a Comparator to sorted(Comparator). This covers reverse order, case-insensitive string comparison, and ordering by a property of the element.
List<String> names = List.of("carol", "Alice", "bob"); List<String> byNatural = names.stream().sorted().collect(Collectors.toList()); // [Alice, bob, carol] — uppercase sorts before lowercase List<String> caseInsensitive = names.stream() .sorted(String.CASE_INSENSITIVE_ORDER) .collect(Collectors.toList()); // [Alice, bob, carol]
For reversing natural order, Comparator.reverseOrder() is clearer than writing a lambda that negates the comparison result. For reversing an existing comparator, Comparator.reversed() works on any comparator instance.
Chaining Comparators for Multi-Field Sorting
Real data rarely sorts by a single field. Comparator.comparing() and thenComparing() let you build a comparator that orders by one field and breaks ties with subsequent fields.
record Employee(String name, String department, int salary) {} List<Employee> employees = List.of( new Employee("alice", "eng", 120000), new Employee("bob", "eng", 95000), new Employee("carol", "sales", 110000) ); List<Employee> sorted = employees.stream() .sorted(Comparator.comparing(Employee::department) .thenComparing(Employee::salary)) .collect(Collectors.toList());
The resulting stream orders employees by department first, then by salary within each department. thenComparing can be chained further for a third or fourth key. Each key can use its own comparator, so a mix of ascending and descending fields is possible:
Comparator<Employee> byDeptThenSalaryDesc = Comparator.comparing(Employee::department) .thenComparing(Comparator.comparingInt(Employee::salary).reversed());
Null Handling in sorted()
By default, sorted() throws NullPointerException if any element is null, because compareTo cannot be invoked on a null reference. When a stream may contain nulls, use Comparator.nullsFirst() or Comparator.nullsLast() to define where nulls belong.
List<String> values = Arrays.asList("b", null, "a"); List<String> nullsLast = values.stream() .sorted(Comparator.nullsLast(Comparator.naturalOrder())) .collect(Collectors.toList()); // [a, b, null]
nullsFirst places nulls at the beginning; nullsLast places them at the end. Both wrap an existing comparator, so they compose with comparing() and thenComparing() chains. Without one of these wrappers, a null element aborts the terminal operation with an exception.
Performance and Memory Behavior
sorted() is a stateful intermediate operation. Unlike filter() or map(), it must buffer the entire stream contents before producing any output. For a stream backed by an in-memory collection, this means the operation allocates an array large enough to hold all elements and sorts it in place using a dual-pivot quicksort for primitive arrays or TimSort for object arrays.
The practical consequence is that sorted() has O(n log n) time complexity and O(n) additional memory. For large streams, especially those backed by external data such as a database cursor or a file, buffering the whole stream can be expensive. If the data source can already return ordered rows—for example, a SQL query with ORDER BY—applying sorted() in the stream duplicates that work and adds memory pressure.
Parallel streams change the sorting behavior. When a parallel stream reaches sorted(), the framework sorts chunks of the stream concurrently and then merges the results. This can improve throughput on multi-core machines for large collections, but the merge step still requires buffering the full result. For small collections, the overhead of splitting and merging usually outweighs any parallelism benefit.
Common Pitfalls with sorted()
One recurring mistake is expecting sorted() to mutate the original collection. Streams do not modify their source. The sorted result is a new stream; the original list remains in its original order. If the goal is to reorder a List in place, use Collections.sort() or List.sort() instead of a stream pipeline.
Another pitfall is sorting a stream of Optional values. Optional does not implement Comparable, so sorted() without a comparator throws ClassCastException. The same applies to custom classes that do not implement Comparable. When the element type is not comparable, always supply a comparator.
A third issue is comparator inconsistency with equals(). The Comparator contract expects that compare(a, b) == 0 implies a.equals(b). Violating this can produce surprising results in sorted() and in distinct()(), which also relies on equality. For example, a comparator that only compares one field of a multi-field record will treat two records with the same field value as equal, even when the records differ in other fields.
Choosing Between Stream Sorting and List Sorting
The decision between stream().sorted() and List.sort() depends on whether the pipeline continues after sorting. If the only goal is an ordered list, List.sort() with a comparator is more direct and avoids the overhead of building a stream and collecting the result:
employees.sort(Comparator.comparing(Employee::department));
If the pipeline continues with map(), filter(), or a grouping operation, stream().sorted() fits naturally into the chain and avoids an intermediate mutable list. The two approaches produce the same ordering; the choice is about pipeline composition and code clarity rather than a difference in sorting algorithm.
For primitive streams, IntStream, LongStream, and DoubleStream each provide a no-argument sorted(). These variants do not accept a comparator because primitive types have fixed natural ordering. Converting to a boxed stream to apply a custom comparator is rarely useful, since primitives have no properties to compare beyond their values.