Java Collections Framework - Comparable & Comparator
When working with Java collections, we often need to sort objects.
For example, suppose we have a list of students:
Ashish - 25
Rahul - 20
Priya - 22We may want to sort them:
- By age
- By name
- By salary
- By ID
- By any other property
Java provides two important interfaces for defining sorting logic:
- Comparable
- Comparator
Both are available in the java.lang and java.util packages respectively.
import java.util.Comparator;Comparable is part of:
java.lang.Comparableand Comparator is part of:
java.util.ComparatorWhy do we need Comparable and Comparator?
Java knows how to compare many standard data types.
For example:
List<Integer> numbers =
Arrays.asList(30, 10, 20);
Collections.sort(numbers);Output
[10, 20, 30]Similarly, strings can be sorted:
List<String> names =
Arrays.asList("Rahul", "Ashish", "Priya");
Collections.sort(names);Output
[Ashish, Priya, Rahul]But what happens when we create our own class?
For example:
class Student {
int id;
String name;
int age;
}Java does not automatically know whether students should be sorted by:
- ID
- Name
- Age
This is where Comparable and Comparator are useful.
Comparable
What is Comparable?
Comparable is an interface used to define the natural ordering of objects of a class.
It is available in:
java.langThe interface contains the method:
int compareTo(T o)A class implements Comparable when it wants to define its own default or natural sorting order.
Basic Syntax
class Student implements Comparable<Student> {
@Override
public int compareTo(Student other) {
return 0; // Placeholder: replace with your comparison rule.
}
}The important method is:
compareTo()How compareTo() Works
The compareTo() method returns an integer.
The result has three possible meanings:
| Result | Meaning |
|---|---|
| Negative value | Current object comes before the other object |
| 0 | Objects are considered equal for ordering |
| Positive value | Current object comes after the other object |
You don't need to return exactly -1, 0, or 1. Any negative, zero, or positive integer has the corresponding meaning.
For example:
return this.age - other.age;conceptually means:
Negative -> this.age is smaller
Zero -> ages are equal
Positive -> this.age is greaterHowever, for integer comparisons, methods such as Integer.compare() are generally safer because subtraction can overflow for extreme integer values.
Example of Comparable
Suppose we have a Student class:
class Student implements Comparable<Student> {
private int age;
private String name;
public Student(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
@Override
public int compareTo(Student other) {
return Integer.compare(this.age, other.age);
}
}Here, we have defined the natural ordering of Student based on age.
Now create students:
List<Student> students = new ArrayList<>();
students.add(new Student(25, "Ashish"));
students.add(new Student(20, "Rahul"));
students.add(new Student(22, "Priya"));We can sort them:
Collections.sort(students);The students will be sorted by age:
Rahul - 20
Priya - 22
Ashish - 25Comparable and Collections.sort()
When a class implements Comparable, we can use:
Collections.sort(list);Example
Collections.sort(students);The sorting algorithm uses the class's compareTo() method to determine the order.
Comparable with List.sort()
Modern Java also provides:
students.sort(null);Passing null tells the list to use the elements' natural ordering.
For a Student class implementing Comparable<Student>:
students.sort(null);will use:
compareTo()Comparable Example - Sort Employees by Salary
Suppose we want the natural ordering of an Employee to be based on salary.
class Employee implements Comparable<Employee> {
private int id;
private String name;
private double salary;
private String department = "General";
public int getId() { return id; }
public String getName() { return name; }
public double getSalary() { return salary; }
public String getDepartment() { return department; }
public Employee(int id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
@Override
public int compareTo(Employee other) {
return Double.compare(this.salary, other.salary);
}
@Override
public String toString() {
return id + " " + name + " " + salary;
}
}Now:
Collections.sort(employees);will sort employees according to salary.
Comparator
What is Comparator?
Comparator is an interface used to define a custom ordering for objects.
It is available in:
java.utilThe main method is:
int compare(T o1, T o2)Unlike Comparable, the comparison logic is normally defined outside the class being sorted.
Why do we need Comparator?
Suppose a Student class has:
- id
- name
- age
Maybe we want to sort students by age today.
Tomorrow, we may want to sort them by name.
Another time, we may want to sort them by ID.
If we put every possible sorting rule inside compareTo(), the class becomes difficult to manage.
Comparator allows us to define different sorting strategies separately.
Basic Comparator Syntax
Comparator<Student> comparator =
(s1, s2) -> Integer.compare(s1.getAge(), s2.getAge());Then:
students.sort(comparator);Comparator Example
Consider:
class Student {
private int id;
private String name;
private int age;
public Student(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}We can sort by age:
students.sort(
(s1, s2) ->
Integer.compare(s1.getAge(), s2.getAge())
);Sorting by Name
We can define another Comparator:
students.sort(
Comparator.comparing(Student::getName)
);This sorts students alphabetically by name.
Sorting by ID
students.sort(
Comparator.comparingInt(Student::getId)
);This sorts students according to their ID.
Sorting in Descending Order
By default, many comparison methods produce ascending order.
We can reverse the Comparator:
students.sort(
Comparator.comparingInt(Student::getAge)
.reversed()
);This sorts students from highest age to lowest age.
Comparator.comparing()
Java provides convenient methods for creating Comparators.
For example:
Comparator<Student> byName =
Comparator.comparing(Student::getName);Then:
students.sort(byName);Comparator.comparingInt()
For integer properties:
Comparator<Student> byAge =
Comparator.comparingInt(Student::getAge);Then:
students.sort(byAge);Comparator.comparingDouble()
For double properties:
Comparator<Employee> bySalary =
Comparator.comparingDouble(Employee::getSalary);Sorting by Multiple Fields
One of the most useful features of Comparator is the ability to combine multiple sorting conditions.
Suppose we want:
- Sort by department.
- If department is the same, sort by salary.
We can write:
employees.sort(
Comparator.comparing(Employee::getDepartment)
.thenComparingDouble(Employee::getSalary)
);The first comparison is performed using department.
If two employees have the same department, salary is used as the next comparison.
thenComparing()
The thenComparing() method allows us to add another sorting condition.
Example
students.sort(
Comparator.comparing(Student::getName)
.thenComparingInt(Student::getAge)
);This means:
First sort by name
↓
If names are equal
↓
Sort by ageComparator Using Lambda Expression
A Comparator can be written using a lambda expression.
students.sort(
(s1, s2) ->
Integer.compare(s1.getAge(), s2.getAge())
);Here:
s1 -> first student
s2 -> second studentThe comparison result determines their ordering.
Comparator Using Anonymous Class
Before lambda expressions became available, Comparator was commonly written using an anonymous class.
Comparator<Student> comparator =
new Comparator<Student>() {
@Override
public int compare(
Student s1,
Student s2) {
return Integer.compare(
s1.getAge(),
s2.getAge()
);
}
};Then:
students.sort(comparator);This is still valid Java, although lambda expressions are usually more concise when applicable.
Comparable vs Comparator
The most important difference is where the comparison logic is defined.
| Comparable | Comparator |
|---|---|
| Defines natural ordering | Defines custom ordering |
| Implemented by the class being compared | Usually defined separately from the class |
| Uses compareTo() | Uses compare() |
| Located in java.lang | Located in java.util |
| Usually provides one natural ordering | Can provide multiple different orderings |
| Modifies the class definition | Does not require modifying the class |
Comparable Example
class Student implements Comparable<Student> {
private int age;
@Override
public int compareTo(Student other) {
return Integer.compare(this.age, other.age);
}
}The class itself defines its natural ordering.
Comparator Example
Comparator<Student> byAge =
Comparator.comparingInt(Student::getAge);The sorting rule is defined separately.
When Should We Use Comparable?
Use Comparable when:
- A class has a clear natural ordering.
- You want the class itself to define that ordering.
- There is one primary/default way objects should normally be sorted.
For example, a Product class might naturally be ordered by product ID.
When Should We Use Comparator?
Use Comparator when:
- Multiple sorting orders are required.
- You cannot or do not want to modify the class.
- Sorting rules depend on the particular operation.
- You need flexible and reusable sorting strategies.
For example, employees can be sorted by:
- Salary
- Name
- Department
- Experience
- ID
Each can be represented by a different Comparator.
Comparable with TreeSet
Comparable is also important when working with sorted collections such as TreeSet.
For example:
class Student implements Comparable<Student> {
private int age;
@Override
public int compareTo(Student other) {
return Integer.compare(this.age, other.age);
}
}Now a TreeSet<Student> can use the natural ordering:
TreeSet<Student> students =
new TreeSet<>();
students.add(student1);
students.add(student2);
students.add(student3);The elements are maintained according to the ordering defined by compareTo().
Comparator with TreeSet
A TreeSet can also receive a Comparator.
TreeSet<Student> students =
new TreeSet<>(
Comparator.comparingInt(Student::getAge)
);Now the TreeSet uses the supplied Comparator to determine ordering.
Comparator with TreeMap
Comparator is also useful with TreeMap.
For example:
TreeMap<String, Integer> map =
new TreeMap<>(Comparator.reverseOrder());The keys are maintained in reverse order.
Comparing Strings
Strings already implement Comparable.
For example:
String a = "Apple";
String b = "Banana";
System.out.println(a.compareTo(b));The result is negative because "Apple" comes before "Banana" according to their natural ordering.
For case-insensitive comparison:
Comparator<String> comparator =
String.CASE_INSENSITIVE_ORDER;Comparable Contract
When implementing Comparable, the comparison should follow a consistent contract.
The important relationship is:
compareTo() < 0means the current object comes before the other object.
compareTo() == 0means the objects are equivalent for ordering.
compareTo() > 0means the current object comes after the other object.
A comparison should also generally be:
Reflexive in ordering
An object compared with itself should return:
object.compareTo(object) == 0Antisymmetric in sign
If:
a.compareTo(b)is positive, then:
b.compareTo(a)should be negative, and vice versa.
Transitive
If:
A < B
B < Cthen the ordering should also indicate:
A < CFollowing these rules helps prevent unpredictable behavior in sorted collections and sorting operations.
Comparator Contract
A Comparator should follow similar consistency rules.
For:
compare(a, b)the result means:
Negative -> a comes before b
Zero -> a and b are equivalent for this ordering
Positive -> a comes after bThe comparison should be consistent and transitive.
Important: compareTo() == 0 and equals()
One important concept is that:
compareTo() == 0does not automatically mean:
equals() == trueThe two concepts are related but not identical.
For example, a Comparator may compare two employees only by salary:
Comparator<Employee> bySalary =
Comparator.comparingDouble(Employee::getSalary);Two different employees with the same salary will compare as:
0even though they may not be equal according to equals().
This becomes especially important with sorted collections such as TreeSet and TreeMap.
Why Comparator is More Flexible
Suppose we have:
class Employee {
int id;
String name;
double salary;
public int getId() { return id; }
public String getName() { return name; }
public double getSalary() { return salary; }
}We can create multiple sorting rules:
Sort by ID
Comparator<Employee> byId =
Comparator.comparingInt(Employee::getId);Sort by Name
Comparator<Employee> byName =
Comparator.comparing(Employee::getName);Sort by Salary
Comparator<Employee> bySalary =
Comparator.comparingDouble(Employee::getSalary);Now we can choose the required sorting strategy:
employees.sort(byId);or:
employees.sort(byName);or:
employees.sort(bySalary);The Employee class does not need to change.
Complete Comparable Example
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class Student implements Comparable<Student> {
private int id;
private String name;
private int age;
public Student(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
@Override
public int compareTo(Student other) {
return Integer.compare(this.age, other.age);
}
@Override
public String toString() {
return name + " - " + age;
}
}
public class HelloWorld {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(
new Student(101, "Ashish", 25)
);
students.add(
new Student(102, "Rahul", 20)
);
students.add(
new Student(103, "Priya", 22)
);
Collections.sort(students);
System.out.println(students);
}
}Output
[Rahul - 20, Priya - 22, Ashish - 25]The ordering is defined by:
compareTo()Complete Comparator Example
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
class Student {
private int id;
private String name;
private int age;
public Student(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return name + " - " + age;
}
}
public class HelloWorld {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(
new Student(101, "Ashish", 25)
);
students.add(
new Student(102, "Rahul", 20)
);
students.add(
new Student(103, "Priya", 22)
);
students.sort(
Comparator.comparingInt(Student::getAge)
);
System.out.println(students);
}
}Output
[Rahul - 20, Priya - 22, Ashish - 25]Here, the Student class does not implement Comparable.
The sorting rule is supplied separately using Comparator.
Simple Real-World Example
Imagine an employee management system.
An employee has:
- ID
- Name
- Salary
- Experience
Different screens may need different sorting:
Employee List
↓
Sort by Name
Payroll
↓
Sort by Salary
Employee Search
↓
Sort by ID
Experience Report
↓
Sort by ExperienceUsing Comparator, each sorting rule can be created independently.
This makes the code more flexible and easier to maintain.
Quick Comparison
| Feature | Comparable | Comparator |
|---|---|---|
| Package | java.lang | java.util |
| Main method | compareTo() | compare() |
| Sorting type | Natural ordering | Custom ordering |
| Defined | Inside the class | Usually outside the class |
| Number of orderings | Usually one natural ordering | Multiple possible orderings |
| Modify original class? | Yes | No |
| Lambda-friendly | Not directly as a sorting strategy | Yes |
| Useful with TreeSet | Yes | Yes |
| Useful with TreeMap | Yes | Yes |
Key Takeaway
Comparable and Comparator are used to define how objects should be ordered.
Comparable
Use Comparable when a class has a clear natural ordering.
class Student implements Comparable<Student> {
private int age;
@Override
public int compareTo(Student other) {
return Integer.compare(this.age, other.age);
}
}Comparator
Use Comparator when you need custom or multiple sorting rules.
students.sort(
Comparator.comparingInt(Student::getAge)
);A simple way to remember the difference is: Comparable ↓ "I know my natural order."
Comparator ↓ "Tell me how you want to order these objects."
Understanding these two interfaces is essential when working with Collections.sort(), List.sort(), TreeSet, TreeMap, and custom Java objects.
See you in next page....🙂