Java Stream sorted() Method: Sort Streams with Comparators and Examples

0

The sorted() operation is an intermediate operation in the Java Stream API used to arrange stream elements according to a defined ordering. It is especially useful when data needs to be presented, processed, or compared in a predictable sequence.


Imagine receiving employee records in an unpredictable order but needing the highest-paid employees first, or receiving product names in random order and wanting them alphabetically. Instead of writing sorting loops yourself, a stream can express the ordering rule directly with sorted().

Why Do We Need sorted()?

Collections may contain data in insertion order, database retrieval order, or an order that has no business meaning. Sorting allows an application to establish an intentional order before the data reaches the next stage of processing.

import java.util.List;

public class SortedExample {
    public static void main(String[] args) {

        List<Integer> numbers = List.of(
            40, 10, 30, 20, 50
        );

        numbers.stream()
               .sorted()
               .forEach(System.out::println);
    }
}

The natural ordering of integers places the values in ascending order: 10, 20, 30, 40, 50.

Important: sorted() is an intermediate operation. It produces a new stream with elements arranged according to the requested ordering; it does not sort the original collection in place.

Syntax of sorted()

Java provides two commonly used forms of sorted().

stream.sorted()

This version uses the natural ordering of the stream elements.

stream.sorted(comparator)

This version uses a supplied Comparator to define custom ordering.

Form Ordering Used Typical Use
sorted() Natural ordering Numbers, strings, naturally comparable values
sorted(Comparator) Custom ordering Objects, descending order, business-specific sorting

Sorting Numbers

For numeric wrapper types such as Integer, natural ordering is ascending.

import java.util.List;

public class NumberSorting {
    public static void main(String[] args) {

        List<Integer> numbers = List.of(
            70, 20, 90, 10, 50
        );

        numbers.stream()
               .sorted()
               .forEach(System.out::println);
    }
}

The result is printed in ascending order.

Sorting Strings

Strings can also be sorted using their natural ordering.

import java.util.List;

public class StringSorting {
    public static void main(String[] args) {

        List<String> languages = List.of(
            "Python",
            "Java",
            "Kotlin",
            "C++"
        );

        languages.stream()
                 .sorted()
                 .forEach(System.out::println);
    }
}

The strings are ordered according to their natural lexicographical ordering.

Sorting in Descending Order

Natural ordering is not always what the application needs. To sort numbers from largest to smallest, provide a comparator.

import java.util.List;
import java.util.Comparator;

public class DescendingNumbers {
    public static void main(String[] args) {

        List<Integer> numbers = List.of(
            10, 50, 20, 40, 30
        );

        numbers.stream()
               .sorted(Comparator.reverseOrder())
               .forEach(System.out::println);
    }
}

The comparator reverses the natural ordering, producing descending output.

Sorting with a Lambda Comparator

A comparator can also be written using a lambda expression.

numbers.stream()
       .sorted((a, b) -> b.compareTo(a))
       .forEach(System.out::println);

Although this works, Comparator.reverseOrder() is often clearer when reversing a natural ordering is all that is required.

Sorting Objects

The real power of sorted() appears when working with objects. Suppose an application contains employee records.

import java.util.List;

public class EmployeeSorting {

    record Employee(
        String name,
        double salary
    ) {}

    public static void main(String[] args) {

        List<Employee> employees = List.of(
            new Employee("Amit", 65000),
            new Employee("Priya", 52000),
            new Employee("Rahul", 78000)
        );

        employees.stream()
                 .sorted((e1, e2) ->
                     Double.compare(
                         e1.salary(),
                         e2.salary()
                     )
                 )
                 .forEach(System.out::println);
    }
}

The comparator compares employee salaries and arranges the employees from the lowest salary to the highest.

Using Comparator.comparing()

For object sorting, Comparator.comparing() often makes the intention much clearer.

employees.stream()
         .sorted(Comparator.comparing(Employee::salary))
         .forEach(System.out::println);

The comparator extracts the salary from each employee and uses it as the sorting key.

Instructor Tip: When sorting objects by one property, prefer Comparator.comparing() over writing manual comparison logic. It communicates the business rule much more clearly.

Sorting Objects in Descending Order

To sort employees by salary from highest to lowest, reverse the comparator.

employees.stream()
         .sorted(
             Comparator.comparing(Employee::salary)
                       .reversed()
         )
         .forEach(System.out::println);

This is a common pattern for leaderboards, salary reports, product rankings, and other applications where the highest values should appear first.

Sorting by String Property

You can sort objects using a string property such as an employee's name.

employees.stream()
         .sorted(Comparator.comparing(Employee::name))
         .forEach(System.out::println);

The employees are arranged alphabetically according to their names.

Sorting by Multiple Properties

Business requirements often say something like: "Sort employees by department, and when two employees belong to the same department, sort them by salary."

employees.stream()
         .sorted(
             Comparator.comparing(Employee::department)
                       .thenComparing(Employee::salary)
         )
         .forEach(System.out::println);

The thenComparing() method defines the secondary ordering rule. This is much cleaner than manually writing nested comparison conditions.

Multiple Properties with Different Directions

Each sorting key can have its own direction.

employees.stream()
         .sorted(
             Comparator.comparing(Employee::department)
                       .thenComparing(
                           Comparator.comparing(Employee::salary)
                                     .reversed()
                       )
         )
         .forEach(System.out::println);

Employees are grouped by department in ascending order, while salaries within each department are ordered from highest to lowest.

Sorting After filter()

A common pipeline filters data first and sorts only the elements that remain.

employees.stream()
         .filter(employee -> employee.salary() > 60000)
         .sorted(Comparator.comparing(Employee::salary))
         .forEach(System.out::println);

This pipeline avoids sorting employees who will ultimately be discarded. When the filter removes many elements, this can reduce unnecessary sorting work.

Sorting Before map()

Sometimes you need to sort objects according to a property and then transform them into another representation.

List<String> names = employees.stream()
    .sorted(
        Comparator.comparing(Employee::salary)
                  .reversed()
    )
    .map(Employee::name)
    .toList();

System.out.println(names);

The employees are sorted by salary first. Only after the correct order has been established are their names extracted.

Sorting After map()

If the transformed value is the value that should be ordered, mapping first can be the better choice.

List<String> names = employees.stream()
    .map(Employee::name)
    .sorted()
    .toList();

System.out.println(names);

Here, the stream becomes a stream of names, and those names are then sorted alphabetically.

The general rule is simple: sort before map when the original object's property determines the order; sort after map when the transformed value determines the order.

Sorting with Null Values

Null values require special attention when sorting. A comparator that does not account for null values may throw a NullPointerException.

import java.util.Comparator;
import java.util.List;

List<String> names = List.of(
    "Amit",
    null,
    "Priya",
    "Rahul"
);

names.stream()
     .sorted(Comparator.nullsLast(Comparator.naturalOrder()))
     .forEach(System.out::println);

The nullsLast() wrapper places null values after non-null values. You can use nullsFirst() when the opposite behavior is required.

Important: Always decide how null values should behave before sorting nullable data. Do not assume the comparator will handle null automatically.

Case-Insensitive String Sorting

Natural string ordering is case-sensitive. If a user-facing application requires case-insensitive ordering, provide an appropriate comparator.

import java.util.Comparator;
import java.util.List;

List<String> names = List.of(
    "rahul",
    "Amit",
    "priya",
    "Neha"
);

names.stream()
     .sorted(String.CASE_INSENSITIVE_ORDER)
     .forEach(System.out::println);

This is often more appropriate for names and other human-readable text.

Does sorted() Modify the Source?

No. A stream pipeline does not normally sort the original collection.

import java.util.List;

public class SourceSorting {
    public static void main(String[] args) {

        List<Integer> numbers = List.of(
            30, 10, 20
        );

        List<Integer> sorted = numbers.stream()
                                      .sorted()
                                      .toList();

        System.out.println("Original: " + numbers);
        System.out.println("Sorted: " + sorted);
    }
}

The original list remains unchanged, while the stream pipeline produces the values in sorted order.

sorted() Is a Stateful Operation

Unlike operations such as map() and filter(), sorting generally needs to consider multiple elements together before it can produce the correctly ordered result.

This makes sorted() a stateful intermediate operation. The stream implementation may need to buffer elements before producing them in their final sorted order.

Performance Insight: Sorting can be more expensive than simple stateless operations because the implementation generally needs information about multiple elements rather than processing each element independently.

sorted() and Large Data Sets

When processing large data sets, avoid sorting unless the ordering is genuinely required. If you only need to know whether any element satisfies a condition, sorting the entire stream first is unnecessary.

boolean found = numbers.stream()
                       .anyMatch(number -> number > 100);

There is no reason to sort the numbers before checking whether a matching value exists. Choose the stream operation that directly expresses the business requirement.

Sorting and Limit

A popular requirement is finding the top few elements, such as the top three highest salaries.

employees.stream()
         .sorted(
             Comparator.comparing(Employee::salary)
                       .reversed()
         )
         .limit(3)
         .forEach(System.out::println);

The stream is ordered from highest salary to lowest, and only the first three elements are retained by limit().

Common Beginner Mistakes

  • Assuming sorted() modifies the original list: Stream sorting does not normally reorder the source collection.
  • Using sorted() without a meaningful requirement: Sorting adds processing work, so use it when order matters.
  • Forgetting custom ordering: Object types may require a comparator to define how they should be sorted.
  • Ignoring null values: Nullable values need an explicit null-handling strategy.
  • Writing complicated comparator logic unnecessarily: Prefer Comparator.comparing() and thenComparing() when they clearly express the requirement.
  • Sorting before an effective filter: When possible, remove irrelevant elements before sorting to reduce unnecessary work.
  • Confusing natural order with business order: Alphabetical or numeric natural ordering may not match the application's actual business rules.

Best Practices

  • Use sorted() only when ordering is required by the application.
  • Prefer Comparator.comparing() for sorting objects by a property.
  • Use thenComparing() for clear secondary and tertiary sorting rules.
  • Handle nullable values explicitly with nullsFirst() or nullsLast() when appropriate.
  • Filter unnecessary elements before sorting when doing so is logically correct.
  • Use method references when they make comparator definitions easier to read.
  • Remember that sorting is stateful and may require additional memory and processing compared with simple stateless operations.

Interview Insights

Question: What is sorted() in Java Streams?

Answer: sorted() is an intermediate operation that arranges stream elements according to natural ordering or a supplied comparator.

Question: What is the difference between sorted() and sorted(Comparator)?

Answer: sorted() uses natural ordering, while sorted(Comparator) allows custom ordering rules.

Question: Does sorted() modify the original collection?

Answer: No. Sorting through a stream does not normally modify the source collection.

Question: Can sorted() be used with custom objects?

Answer: Yes. A custom comparator can define how objects should be ordered, commonly using Comparator.comparing().

Question: Is sorted() stateful?

Answer: Yes. Sorting generally needs to consider multiple elements together, so it is classified as a stateful intermediate operation.

Quick Revision

Concept Key Point
sorted() Orders stream elements using natural ordering
sorted(Comparator) Orders elements using custom rules
Type Intermediate operation
Natural order Provided by the element type's natural comparison
Object sorting Use Comparator.comparing() and related methods
Multiple keys Use thenComparing()
Descending order Use reversed() or Comparator.reverseOrder()
Null handling Use nullsFirst() or nullsLast() when required
Source collection Not normally modified by stream sorting
Performance Sorting is stateful and can require additional processing and memory

Final Takeaway

The sorted() operation gives Java streams a clean way to express ordering requirements without manually managing sorting loops. Use the no-argument version when natural ordering is appropriate and a comparator when the application has a specific business rule. For professional code, Comparator.comparing(), thenComparing(), and reversed() provide a powerful vocabulary for readable sorting logic. The key lesson is simple: sort only when order matters, and make the ordering rule obvious to the next developer reading your code.

Post a Comment

0Comments
Post a Comment (0)