Sorting Collections in Java: Collections.sort & Custom Orders
-
Last Updated: August 27, 2026
-
By: javahandson
-
Series
Learn sorting collections in Java the easy way. Master Collections.sort, Comparable, Comparator, multi-field sorts, reversing, and null handling with clear examples.
Sorting collections in Java is a common task that many people encounter. You might have a list of names and want to arrange them in order. Or you could have a list of orders and need to show the newest one first. These tasks sound simple, and they usually are. However, when you need a custom order, things can get a bit complicated.
I have seen many developers try to use a manual bubble sort or create their own loop, even though Java provides everything they need. This is unnecessary work. Java offers a small set of tools that can handle almost any sorting task you will encounter.
In this guide, we will walk through those tools one by one. We will start with the humble Collections.sort method. Then we will move into custom orders using Comparable and Comparator. By the end, you will know which tool to grab and when.
You only need to know what a List is and how to write a basic class. If you have added a few items to an ArrayList and printed them, you are ready to go.
Here is the ground we will cover:
Let’s begin with something straightforward. Java includes a helpful class called Collections. This class has many static methods that work with lists, sets, and other collections. One of these methods is called sort.
You pass it a list, and it sorts that list in place. No new list comes back. The one you handed in gets rearranged. That is worth remembering, because it catches people off guard.
Say you have a few names sitting in an ArrayList. You want them in alphabetical order. Here is all it takes.
List<String> names = new ArrayList<>();
names.add("Ravi");
names.add("Anita");
names.add("Kiran");
Collections.sort(names);
System.out.println(names); // [Anita, Kiran, Ravi]One line does everything here. The list now shows Anita, Kiran, and Ravi. We didn’t need to tell Java how to compare the strings. It already knows. Strings are sorted alphabetically by default, and we will understand why shortly.
Numbers work the same way. Drop a few integers into a list and call the same method.
List<Integer> scores = new ArrayList<>(); scores.add(50); scores.add(20); scores.add(80); Collections.sort(scores); System.out.println(scores); // [20, 50, 80]
Small numbers land first, big ones last. This is the default order for integers, and you did not have to lift a finger for it.
💡 Interview Insight
Collections.sort modifies the original list rather than returning a new one. If you need to keep the unsorted version too, make a copy first with new ArrayList<>(original) before you sort.
Since Java 8, the List interface has its own sort method built right in. So you can skip the Collections class if you like.
names.sort(null); // null means "use natural order"
Both do the same thing under the hood. Passing null to list.sort tells it to fall back on natural order. Some folks find names.sort(...) reads cleaner than Collections.sort(names). Pick whichever feels right to you.
You just saw how strings and numbers can sort themselves automatically without extra coding. This process is called natural ordering, and it comes from an interface named Comparable.
Classes like String, Integer, Double, and LocalDate all implement Comparable. That means each one carries a built-in rule for how it stacks up against another of its kind. When you call Collections.sort with no other hints, Java leans on that rule.
The Comparable interface has one method: compareTo. It compares the current object against another and returns an int. The sign of that number is the whole story.
So when you write "apple".compareTo("banana"), you get a negative value, because apple sits before banana. That is the engine behind every natural sort you have seen so far.
Here’s the key point: Built-in types can compare themselves easily, but your own classes can’t do this by default. For example, if you create a Student class and try to sort a list of students, Java won’t know how to order them. It needs you to tell it what order you want.
That is where you step in. You get to teach your class how to sort. And you have two ways to do it, which brings us to the next two sections.
The first way to add sorting to your own class is to implement Comparable. You bake the sort rule right into the class itself. This becomes the natural order for that type.
Use this when your class has one obvious, default way to sort. For a Student, that might be by roll number. For a Product, maybe by name. Whatever feels like the sensible default goes here.
Let’s create a Student class that includes a name and marks. We want to sort students by their marks by default. To do this, the class will implement Comparable and provide the compareTo method.
public class Student implements Comparable<Student> {
private String name;
private int marks;
public Student(String name, int marks) {
this.name = name;
this.marks = marks;
}
@Override
public int compareTo(Student other) {
return this.marks - other.marks; // sort by marks
}
public String getName() { return name; }
public int getMarks() { return marks; }
}Look at the compareTo body. We subtract the other student marks from ours. If our marks are lower, the result is negative, so we come first. That gives us a low-to-high order by marks.
Now the sort call needs nothing extra. Java sees that Student implements Comparable and uses our rule.
List<Student> students = new ArrayList<>();
students.add(new Student("Ravi", 75));
students.add(new Student("Anita", 90));
students.add(new Student("Kiran", 60));
Collections.sort(students);
for (Student s : students) {
System.out.println(s.getName() + " - " + s.getMarks());
}
// Kiran - 60
// Ravi - 75
// Anita - 90And there it is. Kiran with 60 marks comes first, Anita with 90 comes last. The class sorted itself, because we told it how.
💡 Interview Insight
The subtraction trick (this.marks – other.marks) works well for small integers. However, if the values are very large or negative, the subtraction might cause an overflow and give a wrong result. To avoid this problem, use Integer.compare(this.marks, other.marks) instead. It takes care of the tricky cases for you.
Comparable is handy, but it locks you into one order. A Student can have only one compareTo. What if today you want to sort by marks, and tomorrow by name? You cannot have two natural orders.
You need to own the class if you want to add Comparable. If the class comes from a library and you can’t change it, then you can’t do this. For both of these issues, there’s a better solution available.
The Comparator interface solves everything Comparable cannot. Instead of baking the rule into the class, you write the rule outside it. You can write as many as you want. Sort by name here, by marks there, by anything you dream up.
A Comparator is a distinct entity that specializes in comparing two objects. It is provided to the sort method, which utilizes it to determine the order of the elements. Importantly, the class of the objects being sorted remains unchanged during this process.
Where Comparable had compareTo with one argument, Comparator has compare with two. It takes both objects and returns the same kind of int: negative, zero, or positive. Same rules as before.
Comparator<Student> byName = new Comparator<Student>() {
@Override
public int compare(Student a, Student b) {
return a.getName().compareTo(b.getName());
}
};
Collections.sort(students, byName);This comparator sorts students by name. We lean on the String compareTo inside it, since names are strings and strings already know how to sort. We pass the comparator as a second argument to Collections.sort.
That anonymous class is too wordy. Since Java 8, a lambda does the same job with much less code.
// sort by name, the short way students.sort((a, b) -> a.getName().compareTo(b.getName())); // sort by marks, high to low students.sort((a, b) -> b.getMarks() - a.getMarks());
Same result, far fewer lines. Notice the second one flips the subtraction to get a high-to-low order. Small change, big difference. This is where comparators really shine over comparable.
✎ Editor note: This article uses Java 8 lambdas and Comparator helpers in places. If you need a strict pre-Java-8 baseline, the anonymous-class form from section 5.1 works everywhere. Flagged for your editorial call on version baseline.
Java 8 gave us an even cleaner way. The Comparator.comparing method builds a comparator from a getter. You just point it at the field you care about.
import static java.util.Comparator.comparing; // sort by name students.sort(comparing(Student::getName)); // sort by marks students.sort(comparing(Student::getMarks));
This reads almost like plain English. Sort, comparing student name. No subtraction, no manual compare, no fuss. For simple single-field sorts, this is the form I reach for most.
Real data often needs sorting by multiple fields. For example, you might want to order students by their marks. If two students have the same marks, you can sort them by their names. This is called a tie-breaker, and Java can handle it effectively.
Before Java 8, you wrote the tie-break logic by hand inside the comparator. It worked, but it got ugly fast.
Collections.sort(students, new Comparator<Student>() {
@Override
public int compare(Student a, Student b) {
int result = Integer.compare(a.getMarks(), b.getMarks());
if (result == 0) {
result = a.getName().compareTo(b.getName());
}
return result;
}
});First we compare marks. If that comes back as zero, meaning a tie, we fall through to compare names. It reads fine here, but stack three or four fields and it turns into a mess.
Java 8 added thenComparing to chain sorts together. You state the primary order, then the tie-breaker, then the next one. Each thenComparing kicks in only when the ones before it tie.
import static java.util.Comparator.comparing;
students.sort(
comparing(Student::getMarks)
.thenComparing(Student::getName)
);Read it top to bottom. Sort by marks first. For any students with equal marks, sort those by name. The chain makes the intent crystal clear, and adding a third field is just one more line.
💡 Interview Insight
thenComparing only runs when the earlier comparator returns zero. So the order of your chain matters. Put your primary sort field first, then the tie-breakers in the order you want them applied.
Here is a question that is more important than it seems. If two students have the same marks, when you sort by marks, do they stay in the same order as before? A stable sort keeps them in their original order. An unstable sort may change their positions.
Why care? Because stability lets you sort in steps. Sort by name first, then sort by marks. With a stable sort, students with equal marks stay in name order from the first pass. You build a multi-field sort out of simple single-field ones.
Good news. The sort behind Collections.sort and list.sort is stable. Java uses a variant of merge sort called TimSort under the hood, and merge sort keeps equal elements in place. So you can rely on this behaviour.
// step one: sort by name students.sort(comparing(Student::getName)); // step two: sort by marks // students with equal marks stay in name order students.sort(comparing(Student::getMarks));
The two-pass trick works because the sort keeps the same order for items with the same mark. In the second sort, only the items with different marks get reordered. This means that for items with tied marks, the order from the first pass stays the same. This method is a smart alternative to using thenComparing when you want to think in stages.
✎ Editor note: One thing to note: sorting arrays of primitives (like int[]) with Arrays.sort uses a different algorithm (dual-pivot quicksort) that is not stable. Stability only matters for objects anyway, since primitives have no hidden extra fields to preserve. Flagged in case you want to add a primitive-vs-object callout.
Most of the time, we sort things in ascending order, like from lowest to highest. However, there are times when you want to reverse this order, such as showing the highest marks first, the newest date first, or the largest price first. Java provides several simple ways to sort items this way.
For natural-order types, the Collections.reverseOrder method hands you a ready-made reverse comparator.
List<Integer> nums = new ArrayList<>(List.of(30, 10, 20)); Collections.sort(nums, Collections.reverseOrder()); System.out.println(nums); // [30, 20, 10]
Now the biggest number leads. This works on anything that has a natural order, so strings, dates, and numbers all play along.
When you already have a comparator, tack .reversed() onto the end. It flips whatever order that comparator produced.
import static java.util.Comparator.comparing; // marks, high to low students.sort(comparing(Student::getMarks).reversed());
We built a low-to-high comparator on marks, then reversed it. The result is high-to-low. This chains with thenComparing too, though you have to watch where you place .reversed(), since it flips everything before it.
Null values can cause problems when sorting lists. If you try to sort a list that has a null, it can lead to a NullPointerException. This happens because the comparator tries to use a method on the null, which causes an error.
Java 8 has your back here too. The Comparator.nullsFirst and nullsLast methods wrap another comparator and deal with the nulls safely.
import static java.util.Comparator.*;
List<String> names = new ArrayList<>(
Arrays.asList("Ravi", null, "Anita", null, "Kiran"));
names.sort(nullsLast(naturalOrder()));
System.out.println(names);
// [Anita, Kiran, Ravi, null, null]The real names sort normally, and the nulls settle at the back. Swap in nullsFirst and they float to the top instead. Either way, no crash. This small habit saves you from a whole class of runtime errors.
So far, we’ve worked with lists that we can sort. However, if you want a collection that stays sorted on its own, lists won’t work. When you add an item after sorting, it can end up in any position you choose. For data that always stays sorted, Java offers other options.
A TreeSet keeps its elements sorted at all times. Every time you add something, it slots into the right spot. It also drops duplicates, since it is a set.
Set<Integer> sorted = new TreeSet<>(); sorted.add(50); sorted.add(10); sorted.add(30); System.out.println(sorted); // [10, 30, 50]
You never call sort. The TreeSet handles order for you as items go in. Pass a comparator to its constructor if you want a custom order instead of the natural one.
A TreeMap does the same trick for map keys. The keys stay in sorted order, so iterating the map gives you a predictable sequence.
Map<String, Integer> ages = new TreeMap<>();
ages.put("Ravi", 25);
ages.put("Anita", 30);
ages.put("Kiran", 22);
System.out.println(ages);
// {Anita=30, Kiran=22, Ravi=25}The keys are arranged in alphabetical order, regardless of how you input them. If your job involves keeping data organized, these classes are often better than repeatedly sorting a list.
A TreeMap sorts by key. But a question comes up all the time: how do I sort a map by its values instead? Maybe you have word counts and you want the most frequent word first. You cannot do this with a plain TreeMap, so you take a different route.
To effectively manage entries, the process involves aggregating them into a list and subsequently sorting that list. This sorting is accomplished using a comparator that organizes the entries based on their values. After sorting, you can easily read back the entries in the desired order..
Map<String, Integer> counts = new HashMap<>();
counts.put("java", 5);
counts.put("sort", 9);
counts.put("list", 2);
List<Map.Entry<String, Integer>> entries =
new ArrayList<>(counts.entrySet());
entries.sort(Map.Entry.comparingByValue());
for (Map.Entry<String, Integer> e : entries) {
System.out.println(e.getKey() + " = " + e.getValue());
}
// list = 2
// java = 5
// sort = 9We copy the entry set into a list, then sort with the handy Map.Entry.comparingByValue helper. Add .reversed() to that comparator and you get the highest value first, which is what you usually want for counts and scores.
💡 Interview Insight
Interviewers love asking how to sort a HashMap. The key insight to share: a HashMap has no order at all, so you cannot sort it directly. You extract the entries into a list and sort that. Saying this out loud shows you understand that sorting and the map structure are two separate concerns.
Every method so far sorts the list in place. Sometimes you do not want that. You want a fresh sorted list and the original left alone. Streams give you a clean way to do exactly that.
A stream allows you to create a new list by sorting without changing the original list. You start a stream, use the sorted function, and collect the new result.
import java.util.stream.Collectors;
import static java.util.Comparator.comparing;
List<Student> byMarks = students.stream()
.sorted(comparing(Student::getMarks))
.collect(Collectors.toList());
// 'students' stays in its original order
// 'byMarks' is a new sorted listThe original students list does not change one bit. You get a second list, byMarks, that holds the same objects in sorted order. This is handy when the source order matters elsewhere in your code.
So which do you use? It comes down to what you need.
Be aware of one small cost. Using the stream version creates a new list, which requires more memory. For most everyday lists, this isn’t a concern. However, for large collections, the in-place sort uses less memory.
💡 Interview Insight
A common interview follow-up: does stream().sorted() modify the original list? The answer is no. Streams never change their source. That is a core promise of the Stream API, and it is a big reason people reach for streams when they need to keep data immutable.
Sorting looks easy, and mostly it is. But a handful of traps catch beginners and old hands alike. Here are the ones worth knowing.
This is something that catches many people off guard at least once. The method Collections.sort is designed to sort the list you provide, but it does so in place, meaning it changes the original list rather than creating a new, ordered version of it. As a result, if you need to refer back to the original arrangement of the elements later, you’ll find that it has been altered. To avoid losing the original order, it’s important to create a copy of the list before using the sort method.
Writing a - b in a comparator feels natural, and it works for small numbers. But with very large or negative values, the subtraction can overflow an int and flip the sign. Your sort then goes wrong in ways that are painful to debug. Use Integer.compare(a, b) and skip the whole problem.
If you try to sort a list of your own objects in Java without using a Comparable or Comparator, you will get a ClassCastException error at runtime. This message can be confusing. It simply means that Java doesn’t know how to order your objects. To fix this, you need to give Java a sorting rule, and the error will go away.
A comparator must be consistent. If a comes before b, and b comes before c, then a must come before c. Write sloppy compare logic that breaks this, and Java may throw an “IllegalArgumentException: Comparison method violates its general contract.” Keep your compare logic simple and honest, and you will not hit it.
A: Comparable defines a single natural order inside the class itself using the compareTo method, so the class knows how to sort itself. Comparator lives outside the class and lets you define many different orders using the compare method. Use Comparable for the one default order, and Comparator when you need multiple orders or cannot edit the class.
A: No. Collections.sort sorts the list in place and returns void. The list you pass in gets rearranged, and the original order is lost. If you need to keep the original, make a copy first with new ArrayList<>(original) before sorting, or use stream().sorted() to build a new list without touching the source.
A: Use Comparator.comparing for the primary field and chain thenComparing for each tie-breaker. For example, comparing(Student::getMarks).thenComparing(Student::getName) sorts by marks first, then by name whenever marks are equal. Each thenComparing only runs when the earlier comparators return zero.
A: You cannot sort a HashMap directly because it has no ordering. To sort by key, copy the data into a TreeMap. To sort by value, pull the entries into a list with new ArrayList<>(map.entrySet()), then sort that list using Map.Entry.comparingByValue(). Add .reversed() to that comparator for highest-value-first order.
A: Yes. Collections.sort and List.sort use a stable sort (TimSort), so elements that compare as equal keep their original relative order. This lets you sort in stages, such as sorting by name first and then by marks, with the name order preserved among students who share the same marks.
A: For natural-order types, pass Collections.reverseOrder() to the sort method. When you already have a comparator, call .reversed() on it, for example comparing(Student::getMarks).reversed() to sort marks high to low. Both give you descending order without writing custom compare logic.
A: Writing a – b works for small numbers but can overflow an int when values are very large or negative, which flips the sign and breaks the sort. Use Integer.compare(a, b) instead. It handles all edge cases safely and reads more clearly.
A: Wrap your comparator with Comparator.nullsLast or Comparator.nullsFirst. For example, list.sort(nullsLast(naturalOrder())) sorts the real values normally and pushes nulls to the end, avoiding the NullPointerException you would otherwise get.
Let us pull it all together. Sorting collections in Java rarely needs a hand-written loop. The built-in tools cover almost everything.
For a quick sort in natural order, Collections.sort or list.sort does the job in one line. When your own class needs a default order, implement Comparable. When you need many orders, or you cannot touch the class, reach for Comparator.
Add thenComparing for tie-breakers, .reversed() to flip direction, and nullsLast to stay safe around nulls. Grab a TreeSet or TreeMap when the data should stay sorted on its own. Learn these few tools well, and sorting stops being a chore and becomes a one-liner.