Sorting in Java puts the elements of an array, a List, a Stream or a Map in order. The order can be the natural order of the elements, such as A to Z for text, or an order that we define ourselves with a Comparator.
We sort data whenever a user expects an order, such as a playlist sorted by title or a leaderboard with the highest score first. For a custom order, such as descending or by several fields, we build a Comparator with Comparator.comparing().
The following example sorts arrays, lists, streams and objects, with the result of each line as a comment.
// 1. Arrays
int[] numbers = {5, 3, 9, 1};
Arrays.sort(numbers); // [1, 3, 5, 9]
String[] fruits = {"banana", "apple", "cherry"};
Arrays.sort(fruits, Comparator.reverseOrder()); // [cherry, banana, apple]
// 2. Lists
List<String> names = new ArrayList<>(List.of("Lokesh", "Alex", "John"));
Collections.sort(names); // [Alex, John, Lokesh]
names.sort(Comparator.reverseOrder()); // [Lokesh, John, Alex]
// 3. Streams (a new list; the source stays unchanged)
List<Integer> sorted = Stream.of(5, 3, 9).sorted().toList(); // [3, 5, 9]
// 4. Objects: by artist, then by year; by plays, highest first
songs.sort(Comparator.comparing(Song::artist).thenComparing(Song::year));
// [Blue, Echo, Sun, Rain]
songs.sort(Comparator.comparing(Song::plays).reversed()); // [Echo, Rain, Blue, Sun]
// 5. Lists that contain null
List<String> withNull = Arrays.asList("pear", null, "fig");
withNull.sort(Comparator.nullsFirst(Comparator.naturalOrder())); // [null, fig, pear]
Notice that Arrays.sort() and list.sort() change the original array or list, whereas sorted() returns a new list and leaves the source alone.
Next, we see when to use Comparable and when to use Comparator, and then we move on to arrays (including descending int[] arrays, ranges and parallelSort()), lists, maps and sets. Then we sort String values without case problems and by the rules of a language. The last part covers stable sorting and the comparator mistakes that give a wrong order without any exception.
1. Which Sort Method to Use for Which Data
Java has no single sort() method for every type, because each data structure has its own method. Some methods sort in place, which means the original array or list changes, whereas other methods leave the original alone and return a new sorted copy.

The examples sort a small playlist in which each song is a Song record (a short way to write an immutable data class). The natural order of a Song is its title.
record Song(String title, String artist, int year, int plays) implements Comparable<Song> {
@Override
public int compareTo(Song other) {
return this.title.compareTo(other.title);
}
}
List<Song> songs = new ArrayList<>(List.of(
new Song("Rain", "Ben", 2021, 300),
new Song("Blue", "Ava", 2019, 120),
new Song("Echo", "Ava", 2023, 450),
new Song("Sun", "Ben", 2019, 80)));
The result comments show only the song titles, because the example class overrides toString() to print the title.
2. Comparable vs Comparator
To sort, Java compares two elements at a time and decides which one comes first. Java gets the answer from one of two interfaces.
- Comparable is implemented by the class itself. Its compareTo() method defines the natural order, the one default order of the class. For example, String values go in alphabetical order, and LocalDate values go from the oldest date to the newest.
- Comparator is a separate object that we pass to the sort method. It defines any extra order, such as “by plays, highest first”, without changing the class.
Both methods return a number that tells the order. A negative number means the first element comes first, and a positive number means the second element comes first. Zero means both elements are equal.
| Comparable | Comparator | |
|---|---|---|
| Package | java.lang | java.util |
| Method | compareTo(T other) | compare(T a, T b) |
| Where it is defined | Inside the class being sorted | In a separate object, often a lambda |
| How many orders | One (the natural order) | Any number |
| Used by | Collections.sort(list), Arrays.sort(a), sorted(), TreeMap, TreeSet | list.sort(c), Arrays.sort(a, c), sorted(c), new TreeMap<>(c) |
| Works for classes we cannot change | No | Yes (String, JDK classes, library classes) |
We implement Comparable only when a class has one obvious order, and for every other order we write a Comparator, often next to a Comparable class.
2.1. Sorting by the Natural Order
A class that implements Comparable needs no extra argument. The methods Collections.sort() and list.sort(null) call compareTo(), and so does sorted() on a stream.
Collections.sort(songs); // [Blue, Echo, Rain, Sun]
songs.sort(null); // same result, null means natural order
List<Song> copy = songs.stream().sorted().toList();
When a class does not implement Comparable, Collections.sort(list) does not compile for that class. The calls list.sort(null) and stream.sorted() do compile, but they throw a ClassCastException at runtime.
2.2. Building a Comparator with comparing() and thenComparing()
Since Java 8, we rarely write a Comparator class by hand. Instead, we call Comparator.comparing() and pass it a key extractor (a function that reads the field to compare, such as the plays count), and it returns a ready Comparator. A lambda expression or a method reference such as Song::plays can serve as the key extractor.
songs.sort(Comparator.comparingInt(Song::plays)); // [Sun, Blue, Rain, Echo]
songs.sort(Comparator.comparing(Song::plays).reversed()); // [Echo, Rain, Blue, Sun]
songs.sort(Comparator.comparing(Song::artist)
.thenComparing(Song::plays, Comparator.reverseOrder())); // [Echo, Blue, Rain, Sun]
songs.sort(Comparator.comparing(Song::year).reversed()
.thenComparing(Song::title)); // [Echo, Rain, Blue, Sun]
Comparator<Song> byArtist = (a, b) -> a.artist().compareTo(b.artist()); // lambda form
Each call returns a new Comparator, so we chain the calls and read the chain from left to right, with the most important sort key first.
| Method | What it does |
|---|---|
| comparing(Song::artist) | Compares by one field that is Comparable |
| comparingInt(Song::plays) | Same, for an int field. Skips wrapping each value in an Integer object |
| thenComparing(Song::year) | Breaks ties. Runs only when the previous comparison returns 0 |
| thenComparing(Song::plays, Comparator.reverseOrder()) | Breaks ties with its own order, here descending |
| reversed() | Reverses the whole comparator built so far, not only the last field |
| Comparator.naturalOrder(), Comparator.reverseOrder() | Natural order and its reverse, for Comparable types |
The position of reversed() in a chain matters. The method reversed() reverses everything built before it, so comparing(Song::artist).thenComparing(Song::year).reversed() also sorts the artists from Z to A, and the result is [Rain, Sun, Echo, Blue].
To make only one field descending, we pass Comparator.reverseOrder() to thenComparing(), or we call reversed() before adding the next field. The same chains work when we sort on multiple fields of any class, and when we write the Comparator as a lambda.
2.3. Sorting Lists That Contain null
The natural order cannot compare null, so list.sort(Comparator.naturalOrder()) throws a NullPointerException when the list contains a null element. Comparator.nullsFirst() and Comparator.nullsLast() solve the problem, because each one wraps another comparator and puts the null elements at one end of the list.
List<String> withNull = Arrays.asList("pear", null, "fig");
withNull.sort(Comparator.naturalOrder()); // NullPointerException
withNull.sort(Comparator.nullsFirst(Comparator.naturalOrder())); // [null, fig, pear]
withNull.sort(Comparator.nullsLast(Comparator.naturalOrder())); // [fig, pear, null]
// null fields inside objects
songs.sort(Comparator.comparing(Song::artist, Comparator.nullsLast(Comparator.naturalOrder())));
The same wrappers work inside sorted() when we sort a stream with null values.
3. Sorting Arrays
The java.util.Arrays class has one sort() method for each primitive type, such as int or double, and two sort() methods for object arrays. All of them sort the array in place and return void, so we read the result from the same array.
3.1. Why an int[] Cannot Take a Comparator
A Comparator<T> works only with objects, because Java generics (the <T> part) accept only object types. An int[] holds primitive int values, not Integer objects, so no sort(int[], Comparator) method exists, and javac rejects the call with a “no suitable method found” error.
error: no suitable method found for sort(int[],Comparator<T#1>)
method Arrays.<T#2>sort(T#2[],Comparator<? super T#2>) is not applicable
(inference variable T#2 has incompatible bounds
equality constraints: int
upper bounds: Object)
For a primitive array in descending order, we sort ascending and then reverse, or we box the values into a stream. Boxing means wrapping each int in an Integer object, and an Integer[] array accepts a Comparator.
// Option 1: box, sort with a Comparator, unbox
int[] desc = IntStream.of(5, 3, 9, 1)
.boxed()
.sorted(Comparator.reverseOrder())
.mapToInt(Integer::intValue)
.toArray(); // [9, 5, 3, 1]
// Option 2: sort ascending, then swap from both ends (no boxing)
int[] numbers = {5, 3, 9, 1};
Arrays.sort(numbers); // [1, 3, 5, 9]
for (int i = 0, j = numbers.length - 1; i < j; i++, j--) {
int tmp = numbers[i];
numbers[i] = numbers[j];
numbers[j] = tmp;
} // [9, 5, 3, 1]
// Object arrays take a Comparator
Integer[] boxed = {5, 3, 9, 1};
Arrays.sort(boxed, Comparator.reverseOrder()); // [9, 5, 3, 1]
Song[] songArray = songs.toArray(new Song[0]);
Arrays.sort(songArray, Comparator.comparingInt(Song::plays)); // [Sun, Blue, Rain, Echo]
Option 1 creates one Integer object per element, and on large arrays those objects cost memory, so option 2 is the faster choice there. Any other way to reverse an array also works after the ascending sort.
The two kinds of arrays also use different sorting algorithms, and only the object arrays get a stable sort.
| Array type | Algorithm in JDK 25 | Stable? | Comparator |
|---|---|---|---|
| int[], long[], double[], char[] and other primitives | Dual-Pivot Quicksort | Does not matter (two equal int values look the same) | No |
| Object[], Integer[], String[], Song[] | TimSort (a merge sort that runs faster on partly sorted data) | Yes | Yes |
3.2. Sorting Part of an Array
Arrays.sort(a, fromIndex, toIndex) sorts only the part of the array that starts at fromIndex (included) and stops before toIndex (excluded), so elements outside the part keep their positions.
int[] range = {9, 7, 5, 3, 1, 0};
Arrays.sort(range, 1, 4); // [9, 3, 5, 7, 1, 0], indexes 1 to 3 sorted
String[] words = {"fig", "pear", "apple", "kiwi"};
Arrays.sort(words, 0, 2, Comparator.reverseOrder()); // [pear, fig, apple, kiwi]
An index outside the array throws ArrayIndexOutOfBoundsException, and a fromIndex larger than toIndex throws IllegalArgumentException: fromIndex(4) > toIndex(2).
3.3. Arrays.parallelSort() for Large Arrays
Arrays.parallelSort() splits a large array into parts, lets several threads sort the parts at the same time, and then merges the results. The threads come from the common ForkJoinPool, a shared pool of worker threads in the JDK. The method parallelSort() has the same versions as sort(), including a range and a Comparator, and gives the same result.
int[] big = new Random(42).ints(1_000_000, 0, 1000).toArray();
int[] copy = big.clone();
Arrays.parallelSort(big);
Arrays.sort(copy);
boolean same = Arrays.equals(big, copy); // true
String[] words = {"pear", "fig", "apple"};
Arrays.parallelSort(words, Comparator.reverseOrder()); // [pear, fig, apple]
The method parallelSort() does not always run in parallel. In the JDK 25 source, the method sorts on one thread when the array is small, which means up to 4,096 elements for primitive arrays and 8,192 for object arrays, and also when the machine has only one core for the pool.
Parallel sorting is faster only for large arrays, and the method shares the common pool with parallel streams, so we measure with real data before switching.
4. Sorting a List
A List has three sort options. Collections.sort(list) and Collections.sort(list, c) have existed since Java 1.2, and since Java 8 the Collections.sort() methods call list.sort(), so list.sort() does the same work, called on the list itself.
List<String> names = new ArrayList<>(List.of("Lokesh", "Alex", "John"));
// in place
Collections.sort(names); // [Alex, John, Lokesh]
names.sort(Comparator.reverseOrder()); // [Lokesh, John, Alex]
names.sort(null); // [Alex, John, Lokesh], natural order
// new list, the source keeps its order
List<String> source = new ArrayList<>(List.of("Lokesh", "Alex", "John"));
List<String> copy = source.stream().sorted().toList(); // copy = [Alex, John, Lokesh]
// source = [Lokesh, Alex, John]
We use list.sort() when the list itself should change, for example to sort an ArrayList in ascending or descending order. We use stream().sorted() when the original order must stay, or when sorting a stream is one step in a pipeline next to filter() and map().
An unmodifiable list (a list that cannot be changed) cannot be sorted in place. The methods List.of(), Stream.toList() and Collections.unmodifiableList() return unmodifiable lists, and sort() on them throws UnsupportedOperationException.
List.of("b", "a").sort(null); // UnsupportedOperationException
Stream.of("b", "a").toList().sort(null); // UnsupportedOperationException
new ArrayList<>(List.of("b", "a")).sort(null); // OK: [a, b]
Arrays.asList() is a special case. The list has a fixed size, so we cannot add or remove elements, but it allows set(), so sort() works on it, whereas add() throws an UnsupportedOperationException.
5. Sorting a Map by Key or by Value
A HashMap keeps no order, so “sorting a map” means copying the entries into a map type that keeps an order. A TreeMap keeps its keys sorted, whereas a LinkedHashMap keeps the order in which the entries were added. For example, a team page that reads member ages from a HashMap shows the names in hash order until we copy the entries into one of the two ordered maps.
Map<String, Integer> ages = Map.of("Lokesh", 37, "John", 40, "Alex", 25);
// 1. By key: TreeMap sorts the keys
Map<String, Integer> byKey = new TreeMap<>(ages); // {Alex=25, John=40, Lokesh=37}
Map<String, Integer> byKeyDesc = new TreeMap<>(Comparator.reverseOrder());
byKeyDesc.putAll(ages); // {Lokesh=37, John=40, Alex=25}
// 2. By value: sort the entries, collect into a LinkedHashMap
Map<String, Integer> byValue = ages.entrySet().stream()
.sorted(Map.Entry.comparingByValue())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(a, b) -> a, LinkedHashMap::new)); // {Alex=25, Lokesh=37, John=40}
Map<String, Integer> byValueDesc = ages.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(a, b) -> a, LinkedHashMap::new)); // {John=40, Lokesh=37, Alex=25}
The four-argument toMap() matters, because its fourth argument, LinkedHashMap::new, tells the collector which map to create. The two-argument Collectors.toMap() returns a HashMap and loses the sorted order. With the two-argument version, the same stream prints {Alex=25, John=40, Lokesh=37} instead of the value order.
The call Map.Entry.<String, Integer>comparingByValue() names the types on purpose, because Java cannot work out the generic types on its own before reversed(). We use the same two patterns to sort a map by keys or to sort a map by values with any other comparator. Of the Java Map classes, only the ordered ones keep the result of the sort.
6. Keeping Data Sorted with TreeSet and TreeMap
When elements arrive over time, sorting a list again after each insert wastes work. Say a music app shows the genre tags of a playlist in order, and users add tags one at a time. A TreeSet or a TreeMap keeps its content sorted on every insert instead, and each insert takes O(log n) time, so the cost grows slowly as the collection grows. Both classes also offer range methods, such as headSet().
TreeSet<String> tags = new TreeSet<>(List.of("rock", "jazz", "pop", "jazz"));
// [jazz, pop, rock], the duplicate jazz is dropped
String first = tags.first(); // jazz
String last = tags.last(); // rock
SortedSet<String> before = tags.headSet("pop"); // [jazz], elements before "pop"
A TreeSet uses the comparator, not equals(), to decide whether two elements are duplicates. When the comparator returns 0, the TreeSet treats the new element as a duplicate and drops it, so a comparator that compares only one field drops elements without any error. In the example, Echo has the same artist as Blue, and Sun has the same artist as Rain.
TreeSet<Song> byArtist = new TreeSet<>(Comparator.comparing(Song::artist));
byArtist.addAll(songs); // [Blue, Rain], size 2: Echo and Sun were dropped
TreeSet<Song> byArtistTitle = new TreeSet<>(
Comparator.comparing(Song::artist).thenComparing(Song::title));
byArtistTitle.addAll(songs); // [Blue, Echo, Rain, Sun], size 4
The same rule applies to TreeMap keys. A PriorityQueue (a queue that always returns the smallest element first) takes the same comparators, but a loop over a PriorityQueue does not see the elements in sorted order.
7. Sorting Strings
The natural order of String compares the UTF-16 code of each character, which is the number that UTF-16 gives that character. All uppercase letters come before all lowercase letters, so “Bea” sorts before “alex”, and for lists that people read, we want another order.
7.1. Case-Insensitive Sorting
String.CASE_INSENSITIVE_ORDER is a ready Comparator that ignores case, using the same rule as compareToIgnoreCase(). A contact list is a typical case, because users type names in any case and still expect “alex” next to “Alice”.
List<String> names = new ArrayList<>(List.of("bob", "Alice", "alex", "Bea"));
names.sort(null); // [Alice, Bea, alex, bob]
names.sort(String.CASE_INSENSITIVE_ORDER); // [alex, Alice, Bea, bob]
names.sort(Comparator.comparing(String::length)
.thenComparing(String.CASE_INSENSITIVE_ORDER)); // [Bea, bob, alex, Alice]
The same comparators sort a String array in alphabetical order, and they also sort the characters of one String alphabetically.
7.2. Locale-Aware Sorting with Collator
The natural order and the case-insensitive order both sort accented letters in the wrong place. Take the letter e with an acute accent, written \u00e9 as a Java escape. Its code is higher than the code of every ASCII letter, so the natural order puts “\u00e9clair” after “zebra”. A java.text.Collator fixes the order, because it compares strings by the rules of a language, and it is itself a Comparator.
List<String> words = new ArrayList<>(List.of("zebra", "\u00e9clair", "eclair", "Apple"));
words.sort(null); // [Apple, eclair, zebra, \u00e9clair]
words.sort(Collator.getInstance(Locale.FRENCH)); // [Apple, eclair, \u00e9clair, zebra]
Collator collator = Collator.getInstance(Locale.ENGLISH);
collator.setStrength(Collator.PRIMARY); // ignore case and accents
int primary = collator.compare("eclair", "\u00c9clair"); // 0, equal
int natural = "eclair".compareTo("\u00c9clair"); // -100
We use a Collator whenever the sorted text is shown to users in a language other than plain English. The strength setting decides which differences count.
- Collator.PRIMARY compares base letters only.
- Collator.SECONDARY also compares accents.
- Collator.TERTIARY, the default, also compares case.
8. Is Sorting in Java Stable?
Yes for object arrays and lists, and ordered streams sort stably too. A stable sort keeps equal elements in the order they already had, which lets us sort in two passes, first by the secondary field and then by the main field. In the example, the first sort orders the songs by plays, and the second sort groups them by artist while the plays order stays inside each artist.
songs.sort(Comparator.comparingInt(Song::plays).reversed()); // [Echo, Rain, Blue, Sun]
songs.sort(Comparator.comparing(Song::artist)); // [Echo, Blue, Rain, Sun]

The Javadoc promises a stable sort for each of these methods, so a two-pass sort works with any of them.
- Arrays.sort(Object[]), Arrays.sort(T[], Comparator) and Arrays.parallelSort() for object arrays
- Collections.sort() and the default List.sort() implementation
- Stream.sorted() on ordered streams, such as streams from a List. Unordered streams, such as HashSet.stream(), have no such promise.
A chained comparator such as comparing(Song::artist).thenComparing(…) gives the same result in one pass and is easier to read. So the two-pass style is mostly useful when the sort keys are chosen at runtime, for example in a table where the user clicks columns to sort.
9. Common Sorting Mistakes
Most sorting bugs give a wrong order without any exception, and four mistakes come up again and again in code reviews.
9.1. Subtracting Values in compare()
Never write return a – b; in a comparator. For large values with opposite signs, the subtraction overflows, because the result does not fit in an int, so the value wraps around and gets the wrong sign.
List<Integer> values = new ArrayList<>(List.of(-2_000_000_000, 2_000_000_000, 0));
values.sort((a, b) -> a - b); // [0, 2000000000, -2000000000], wrong
int diff = -2_000_000_000 - 2_000_000_000; // 294967296, positive instead of negative
values.sort(Integer::compare); // [-2000000000, 0, 2000000000], correct
We use Integer.compare(), Long.compare(), Double.compare() or Comparator.comparingInt() instead, and the same rule applies to compareTo() methods written as this.id – other.id.
9.2. Calling reversed() at the End of a Chain
The method reversed() reverses everything before it, as we saw in section 2.2. So a chain such as comparing(Song::artist).thenComparing(Song::year).reversed() reverses the artist order too. To make one field descending, we pass Comparator.reverseOrder() to that field only.
9.3. Sorting a List.of() or toList() Result
List.of() and Stream.toList() return unmodifiable lists, so calling sort() on them throws UnsupportedOperationException. We copy the elements into an ArrayList first, or we sort inside the stream with sorted() before toList().
9.4. Comparators That Break the Contract
A comparator must follow a few rules, called its contract. If a < b and b < c, then a < c must hold as well, and compare(a, b) must have the opposite sign of compare(b, a). Comparators in real code break the contract in a few typical ways.
- The comparator never returns 0.
- The comparator mixes up the signs.
- The comparator reads a field that changes during the sort.
For example, a comparator that never returns 0 fails on a list of 10,000 random numbers.
many.sort((x, y) -> x < y ? -1 : 1); // 10,000 random numbers between 0 and 99
// IllegalArgumentException: Comparison method violates its general contract!
TimSort detects some broken comparators and throws the IllegalArgumentException, but in the other cases the order is wrong and no error appears. Comparators built with comparing() and thenComparing() avoid the problem.
10. Sorting in Java FAQs
10.1. How Do We Sort in Descending Order in Java?
For the natural order, we pass Comparator.reverseOrder(), and for a custom comparator, we call reversed() on it. Primitive arrays need the workarounds from section 3.1.
names.sort(Comparator.reverseOrder());
songs.sort(Comparator.comparingInt(Song::plays).reversed());
Integer[] boxed = {5, 3, 9, 1};
Arrays.sort(boxed, Collections.reverseOrder()); // [9, 5, 3, 1], same as Comparator.reverseOrder()
10.2. Which Algorithm Does Arrays.sort() Use and What Is Its Time Complexity?
Arrays.sort() uses Dual-Pivot Quicksort for primitive arrays, and TimSort for object arrays and lists. Both run in O(n log n) time, so the work grows a little faster than the number of elements. TimSort needs far fewer comparisons when the input is already partly sorted. Dual-Pivot Quicksort is a variant of quicksort, and TimSort builds on merge sort.
10.3. How Do We Sort a Set in Java?
A HashSet has no order, so we copy its elements either into a TreeSet, which is sorted and has no duplicates, or into a sorted list.
Set<String> tags = new HashSet<>(Set.of("rock", "jazz", "pop"));
TreeSet<String> sortedSet = new TreeSet<>(tags); // [jazz, pop, rock]
List<String> sortedList = tags.stream().sorted().toList(); // [jazz, pop, rock]
10.4. How Do We Check Whether an Array or List Is Already Sorted?
The JDK has no isSorted() method, so we compare the elements ourselves. To check whether an array is sorted, we compare each element with its neighbor, using either a loop or a stream. The same comparator logic works for a List.
11. Conclusion
For most code, list.sort(Comparator.comparing(…)) and Arrays.sort() sort the data in place, and stream().sorted() sorts inside a pipeline. Comparable defines the one natural order of a class, and Comparator chains define every other order, including descending fields and null handling.
To sort a map by key, we copy the map into a TreeMap, and to sort by value, we collect the sorted entries into a LinkedHashMap. A Collator handles text in human languages, and stability makes two-pass sorts predictable. Finally, Integer.compare() instead of subtraction avoids the overflow bug.
12. References
- Arrays Javadoc (Java 25)
- Comparator Javadoc (Java 25)
- Comparable Javadoc (Java 25)
- Collections Javadoc (Java 25)
- Stream Javadoc (Java 25)
- Collator Javadoc (Java 25)
Happy Learning !!