Comparable, Comparator Interfaces


Why compareTo() and compare() exist?

In Java, objects don’t know how to compare themselves by default.

For example: If you create a List<Employee> with id, name, and salary, Java doesn’t know whether to sort them by id, or name, or salary.

So Java provides two mechanisms:

  • Comparable (compareTo) → defines a natural order inside the class.

  • Comparator (compare) → allows creating custom orders outside the class.

They are mainly used for:

  • Sorting (Collections.sort, stream().sorted())

  • Maintaining ordered collections (TreeSet, TreeMap)

  • Searching algorithms (binarySearch)

  • Ensuring uniqueness based on comparison



1) Comparable<T> Interface

Definition

  • Declared in: java.lang.Comparable<T>

  • Used to define the natural ordering of objects.

  • A class implements Comparable when its objects can be compared naturally.

  • Only one natural order is allowed per class.


Method of Comparable interface

int compareTo(T other)

    Definition:

  • Compares the current object (this) with the given object (other).

  • Defines the natural order of the class.

  • Implemented inside the class.

  • Return values:

    • 0 → this object is equal to other

    • <0 → this object comes before other in sorting order

    • >0 → this object comes after other in sorting order

  • Example use cases: sorting students by roll number, employees by ID, products by price.

  • Limitation: Only one implementation per class → best used when there’s a single obvious natural order.

  • Analogy: Think of roll numbers in a school – every student has a unique, natural order (1,2,3…). This is like compareTo. You can’t have multiple “natural” roll number orders for the same student list.



Example 1 : Comparable single parameter (sort by id ascending; without/with Streams)
import java.util.*; class Employee implements Comparable<Employee> { int id; String name; double salary; Employee(int id, String name, double salary) { this.id = id; this.name = name; this.salary = salary; } // Natural order: ascending by id @Override public int compareTo(Employee other) { return Integer.compare(this.id, other.id); // safe vs subtraction } @Override public String toString() { return id + " - " + name + " (₹" + salary + ")"; } } public class ComparableSingle { public static void main(String[] args) { List<Employee> list = Arrays.asList( new Employee(3, "Amit", 60000), new Employee(1, "Raj", 50000), new Employee(2, "Neha", 70000) ); // Without Stream API Collections.sort(list); // uses compareTo() System.out.println("Comparable sort (no stream): " + list); // With Stream API List<Employee> sorted = list.stream().sorted().toList(); System.out.println("Comparable sort (with stream): " + sorted); // Descending using Collections.reverseOrder() Collections.sort(list, Collections.reverseOrder()); System.out.println("Comparable reverseOrder (desc): " + list); } }

Notes / Output: Sorted ascending by id, streams use same compareTo, reverseOrder uses natural order reversed.


Example 2 — Comparable multiple parameters (sort by id, then name)

import java.util.*; class Employee implements Comparable<Employee> { int id; String name; Employee(int id, String name) { this.id = id; this.name = name; } // Natural order: by id, then by name @Override public int compareTo(Employee other) { int cmp = Integer.compare(this.id, other.id); if (cmp != 0) return cmp; return this.name.compareTo(other.name); } @Override public String toString() { return id + " - " + name; } } public class ComparableMulti { public static void main(String[] args) { List<Employee> list = Arrays.asList( new Employee(2, "Ravi"), new Employee(2, "Anil"), new Employee(1, "Suresh") ); Collections.sort(list); // uses compareTo (id then name) System.out.println("Comparable multi-param (id then name): " + list); // Stream usage System.out.println("Stream sorted (same): " + list.stream().sorted().toList()); } }

Notes: If id ties, name breaks tie. Good for deterministic natural order.


Example 3 — Comparable used by TreeSet (uniqueness decided by compareTo)

import java.util.*; class Employee implements Comparable<Employee> { int id; String name; Employee(int id, String name) { this.id = id; this.name = name; } @Override public int compareTo(Employee other) { return Integer.compare(this.id, other.id); } @Override public String toString() { return id + " - " + name; } } public class ComparableTreeSet { public static void main(String[] args) { TreeSet<Employee> set = new TreeSet<>(); set.add(new Employee(3,"Alice")); set.add(new Employee(1,"Bob")); set.add(new Employee(2,"Charlie")); set.add(new Employee(1,"DuplicateBob")); // compareTo says id==1 -> duplicate System.out.println("TreeSet (Comparable): " + set); } }

Notes: DuplicateBob not added because compareTo returned 0 vs existing id 1 — TreeSet treats it as duplicate.




2) Comparator<T> Interface

Definition

  • Declared in: java.util.Comparator<T>

  • Used to define custom orderings external to the class.

  • Multiple comparators can be defined for the same class.


Methods of Comparator interface

Abstract Methods

  1. compare(T o1, T o2)

    • Compares two objects (o1 and o2) to determine their relative order.

    • Implemented outside the class (via lambda, anonymous class, or separate class).

    • Return values:

      • <0 → o1 comes before o2

      • 0 → o1 is equal to o2

      • >0 → o1 comes after o2

    • Use cases:

      • Sorting employees by salary, then by name.

      • Sorting products by price descending.

      • Sorting students by marks → age → name.

    • Unlike Comparable, multiple Comparators can exist for different orders.

    • Analogy: Different teachers grading the same class — one may sort students by marks, another by age, another by height.

  2. equals(Object obj)

    • Inherited from Object.

    • Rarely overridden.


Default Methods (Java 8+)

  • reversed() – Returns a comparator that reverses the current comparator.

  • thenComparing(Comparator other) – Chains comparators for tie-breaking.

  • thenComparing(Function keyExtractor) – Chains another field-based comparison.

  • thenComparing(Function, Comparator) – Chains comparison by extracted key with custom comparator.

  • thenComparingInt/Long/Double(ToXFunction) – Primitive-specific versions for efficiency.


Static Methods (Java 8+)

  • comparing(Function keyExtractor) – Creates comparator based on key extraction.

  • comparing(Function, Comparator) – Same as above but with custom comparator.

  • comparingInt/Long/Double(ToXFunction) – Comparators for primitive fields.

  • naturalOrder() – Comparator using natural order (Comparable).

  • reverseOrder() – Comparator using reverse natural order.

  • nullsFirst(Comparator cmp) – Treats null as smaller than non-nulls.

  • nullsLast(Comparator cmp) – Treats null as larger than non-nulls.


Comparator Examples


Example 4 — compare(T o1, T o2) via Anonymous Class & Lambda (sort by name ascending)

import java.util.*; class Employee1 { int id; String name; Employee1(int id, String name) { this.id = id; this.name = name; } @Override public String toString() { return id + " - " + name; } } public class ComparatorCompareDemo { public static void main(String[] args) { List<Employee1> list = Arrays.asList( new Employee1(3,"Amit"), new Employee1(1,"Raj"), new Employee1(2,"Neha") ); // anonymous class Collections.sort(list, new Comparator<Employee1>() { @Override public int compare(Employee1 e1, Employee1 e2) { return e1.name.compareTo(e2.name); } }); System.out.println("Anonymous comparator (name asc): " + list); // lambda list = Arrays.asList(new Employee1(3,"Amit"), new Employee1(1,"Raj"), new Employee1(2,"Neha")); list.sort((a,b) -> a.name.compareTo(b.name)); System.out.println("Lambda comparator (name asc): " + list); // Stream API list = Arrays.asList(new Employee1(3,"Amit"), new Employee1(1,"Raj"), new Employee1(2,"Neha")); List<Employee1> streamSorted = list.stream().sorted(Comparator.comparing(e -> e.name)).toList(); System.out.println("Stream sorted (name asc): " + streamSorted); } }

Example 5 — Comparator.comparing(...) and comparingInt/Long/Double

import java.util.*; class Employee2 { int id; String name; double salary; Employee2(int id,String name,double salary) {this.id=id;this.name=name;this.salary=salary;} @Override public String toString() { return id + "-" + name + "-" + salary; } } public class ComparingStaticDemo { public static void main(String[] args) { List<Employee2> list = Arrays.asList( new Employee2(3,"Amit",60000), new Employee2(1,"Raj",50000), new Employee2(2,"Neha",70000) ); // comparing (uses Comparable on key) list.sort(Comparator.comparing(e -> e.name)); System.out.println("Comparator.comparing(name): " + list); // comparingInt (primitive specialized) list.sort(Comparator.comparingInt(e -> e.id)); System.out.println("Comparator.comparingInt(id): " + list); // comparingDouble list.sort(Comparator.comparingDouble(e -> e.salary)); System.out.println("Comparator.comparingDouble(salary): " + list); // Stream API List<Employee2> streamSorted = list.stream() .sorted(Comparator.comparingDouble(e -> e.salary)) .toList(); System.out.println("Stream sorted by salary asc: " + streamSorted); } }

Example 6 — reversed() and reverseOrder()

import java.util.*; public class ReversedDemo { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(3,1,2,5,4); // natural ascending numbers.sort(Comparator.naturalOrder()); System.out.println("naturalOrder (asc): " + numbers); // reverse natural numbers.sort(Comparator.reverseOrder()); System.out.println("reverseOrder (desc): " + numbers); // reversed() on a comparator (strings by length then reversed) List<String> names = Arrays.asList("Ravi","Amit","Priya","Z"); names.sort(Comparator.comparingInt(String::length).reversed()); System.out.println("reversed comparator (length desc): " + names); } }

Example 7 — thenComparing(Comparator) and thenComparing(keyExtractor)

import java.util.*; class Employee3 { int id; String name; double salary; Employee3(int id,String name,double salary){this.id=id;this.name=name;this.salary=salary;} @Override public String toString(){return id+"-"+name+"-₹"+salary;} } public class ThenComparingDemo { public static void main(String[] args) { List<Employee3> list = Arrays.asList( new Employee3(1,"Amit",60000), new Employee3(2,"Amit",55000), new Employee3(3,"Neha",70000), new Employee3(4,"Amit",60000) ); // Compare by name, then salary, then id Comparator<Employee3> cmp = Comparator.comparing((Employee3 e) -> e.name) .thenComparingDouble(e -> e.salary) .thenComparingInt(e -> e.id); list.sort(cmp); System.out.println("thenComparing (name, salary, id): " + list); // Streams List<Employee3> streamSorted = list.stream().sorted(cmp).toList(); System.out.println("Stream thenComparing: " + streamSorted); } }

Example 8 — thenComparing(keyExtractor, keyComparator)

import java.util.*; class Employee4 { int id; String name; Employee4(int id,String name){this.id=id;this.name=name;} @Override public String toString(){return id+"-"+name;} } public class ThenComparingWithKeyComparator { public static void main(String[] args) { List<Employee4> list = Arrays.asList( new Employee4(2,"amit"), new Employee4(1,"Amit"), new Employee4(3,"neha") ); // Case-insensitive sort by name, then id Comparator<Employee4> cmp = Comparator.comparing((Employee4 e) -> e.name, String.CASE_INSENSITIVE_ORDER) .thenComparingInt(e -> e.id); list.sort(cmp); System.out.println("Case-insensitive name then id: " + list); } }

Example 9 — thenComparingInt/Long/Double (primitive chaining)

import java.util.*; class Player { String name; int score; long timeMs; double accuracy; Player(String name,int score,long timeMs,double accuracy){this.name=name;this.score=score;this.timeMs=timeMs;this.accuracy=accuracy;} @Override public String toString(){return name+"(score="+score+",time="+timeMs+",acc="+accuracy+")";} } public class ThenComparingPrimitive { public static void main(String[] args) { List<Player> players = Arrays.asList( new Player("A", 100, 1200L, 0.95), new Player("B", 100, 1100L, 0.90), new Player("C", 90, 1300L, 0.99) ); Comparator<Player> cmp = Comparator.comparingInt((Player p) -> p.score).reversed() .thenComparingLong(p -> p.timeMs) .thenComparingDouble((Player p) -> p.accuracy).reversed(); players.sort(cmp); System.out.println("Players sorted (score desc, time asc, accuracy desc): " + players); } }

Example 10 — comparing(keyExtractor, keyComparator)

import java.util.*; class Product { String name; Product(String name){ this.name = name; } @Override public String toString(){ return name; } } public class ComparingWithKeyComparator { public static void main(String[] args) { List<Product> products = Arrays.asList( new Product("apple"), new Product("Banana"), new Product("apricot") ); Comparator<Product> cmp = Comparator.comparing((Product p) -> p.name, String.CASE_INSENSITIVE_ORDER); products.sort(cmp); System.out.println("Products (case-insensitive): " + products); } }

Example 11 — nullsFirst() and nullsLast()

import java.util.*; public class NullsFirstLastDemo { public static void main(String[] args) { List<String> list = Arrays.asList("Zara", null, "Arjun", "Meena"); List<String> a = new ArrayList<>(list); a.sort(Comparator.nullsFirst(Comparator.naturalOrder())); System.out.println("nullsFirst: " + a); List<String> b = new ArrayList<>(list); b.sort(Comparator.nullsLast(Comparator.naturalOrder())); System.out.println("nullsLast: " + b); } }

Example 12 — naturalOrder() and reverseOrder()

import java.util.*; public class NaturalReverseOrderDemo { public static void main(String[] args) { List<Integer> nums = Arrays.asList(5,1,3,2,4); nums.sort(Comparator.naturalOrder()); System.out.println("naturalOrder: " + nums); nums.sort(Comparator.reverseOrder()); System.out.println("reverseOrder: " + nums); } }

Example 13 — Overriding equals(Object) for Comparator

import java.util.*; class NameComparator implements Comparator<String> { @Override public int compare(String a, String b) { return a.compareTo(b); } @Override public boolean equals(Object obj) { return obj != null && obj.getClass() == NameComparator.class; } } public class ComparatorEqualsDemo { public static void main(String[] args) { Comparator<String> c1 = new NameComparator(); Comparator<String> c2 = new NameComparator(); System.out.println("c1.equals(c2)? " + c1.equals(c2)); // true } }

Example 14 — TreeSet with Comparator (by name)

import java.util.*; class EmployeeName { int id; String name; EmployeeName(int id,String name){this.id=id;this.name=name;} @Override public String toString(){ return id+"-"+name; } } public class TreeSetComparatorExample { public static void main(String[] args) { TreeSet<EmployeeName> setByName = new TreeSet<>(Comparator.comparing(e -> e.name)); setByName.add(new EmployeeName(3,"Alice")); setByName.add(new EmployeeName(1,"Bob")); setByName.add(new EmployeeName(2,"Charlie")); setByName.add(new EmployeeName(4,"Bob")); // duplicate name System.out.println("TreeSet (Comparator by name): " + setByName); } }

Example 15 — TreeMap with Comparator (descending keys)

import java.util.*; public class TreeMapComparatorExample { public static void main(String[] args) { TreeMap<Integer,String> map = new TreeMap<>(Comparator.reverseOrder()); map.put(3,"Alice"); map.put(1,"Bob"); map.put(2,"Charlie"); System.out.println("TreeMap (keys desc): " + map); } }

Comparable vs Comparator in Java

Aspect

Comparable (compareTo)

Comparator (compare)

Package

java.lang

java.util

Method

int compareTo(T other)

int compare(T o1, T o2)

Location of logic

Implemented inside the class

Defined outside the class (anonymous, lambda, separate class)

Order types

Only one natural order per class

Multiple custom orders possible

Modifies source code?

Yes (must modify class)

No (can sort without touching class code)

Default order

Defines the natural/default order of objects

Provides alternative/custom orders

Chaining

Not supported

Supported (thenComparing, reversed)

TreeSet / TreeMap

Uses natural order

Uses comparator passed in constructor

Static methods

None

Many helpers (comparing, comparingInt, reverseOrder, nullsFirst …)

Flexibility

Less flexible

Highly flexible

Best use case

When class has a single obvious natural order (e.g., Student roll number, Employee ID)

When you need multiple different sorting strategies (e.g., Employee by name, by salary, by ID)


Key Takeaways
  • Use Comparable (compareTo) when:

    • The class has one natural order.

    • Example: Students sorted by roll number.

    • Implemented inside the class itself.

  • Use Comparator (compare) when:

    • You want multiple sorting strategies.

    • Example: Employees sorted by name, salary, or id differently.

    • Defined outside the class (lambda, anonymous, or separate comparator class).

  • Streams & Java 8+ → Comparator is more powerful (comparing, thenComparing, reversed, etc.)

  • TreeSet / TreeMap rely on whichever comparison mechanism you give them:

    • If class implements Comparable → uses compareTo.

    • If you pass Comparator → uses compare.

⚡ So the golden rule is:
πŸ‘‰ Comparable → default natural order (one per class)
πŸ‘‰ Comparator → custom / multiple orders (flexible)




πŸ“˜ A real world case study : Employee Sorting


Example: real-world case study for employee sorting where we use Comparable for the natural order (Employee by id) and multiple Comparators for custom orders (namesalary).

import java.util.*; // Employee implements Comparable (natural order: by id) class Employee implements Comparable<Employee> { int id; String name; double salary; Employee(int id, String name, double salary) { this.id = id; this.name = name; this.salary = salary; } // Natural order: ascending by id @Override public int compareTo(Employee other) { return Integer.compare(this.id, other.id); } @Override public String toString() { return id + " - " + name + " (₹" + salary + ")"; } } public class EmployeeSortingCaseStudy { public static void main(String[] args) { List<Employee> employees = Arrays.asList( new Employee(3, "Amit", 60000), new Employee(1, "Raj", 50000), new Employee(2, "Neha", 70000), new Employee(4, "Priya", 60000) ); // 1) Using Comparable (natural order by id) Collections.sort(employees); System.out.println("Sorted by id (Comparable): " + employees); // 2) Using Comparator (by name ascending) employees.sort(Comparator.comparing(e -> e.name)); System.out.println("Sorted by name (Comparator): " + employees); // 3) Using Comparator (by salary descending) employees.sort(Comparator.comparingDouble((Employee e) -> e.salary).reversed()); System.out.println("Sorted by salary desc (Comparator): " + employees); // 4) Using Comparator (by salary, then by name) employees.sort( Comparator.comparingDouble((Employee e) -> e.salary) .thenComparing(e -> e.name) ); System.out.println("Sorted by salary then name (Comparator): " + employees); } }

Output (Conceptual)

Sorted by id (Comparable): [1 - Raj (₹50000.0), 2 - Neha (₹70000.0), 3 - Amit (₹60000.0), 4 - Priya (₹60000.0)] Sorted by name (Comparator): [3 - Amit (₹60000.0), 2 - Neha (₹70000.0), 4 - Priya (₹60000.0), 1 - Raj (₹50000.0)] Sorted by salary desc (Comparator): [2 - Neha (₹70000.0), 3 - Amit (₹60000.0), 4 - Priya (₹60000.0), 1 - Raj (₹50000.0)] Sorted by salary then name (Comparator): [1 - Raj (₹50000.0), 3 - Amit (₹60000.0), 4 - Priya (₹60000.0), 2 - Neha (₹70000.0)]

Key Learning from Case Study

  • Comparable → used for default / natural order (ID).

  • Comparator → used for multiple flexible orders (Name, Salary, Salary+Name).

  • Both can co-exist in the same class.

  • Streams API can also use the same comparators:

    employees.stream().sorted(Comparator.comparing(e -> e.name)).toList();


Example: An extend case study to include both TreeSet and TreeMap examples using Comparable (natural order) and Comparator (custom order) for employee sorting.
import java.util.*; // Employee implements Comparable (natural order: by id) class Employee implements Comparable<Employee> { int id; String name; double salary; Employee(int id, String name, double salary) { this.id = id; this.name = name; this.salary = salary; } // Natural order: ascending by id @Override public int compareTo(Employee other) { return Integer.compare(this.id, other.id); } @Override public String toString() { return id + " - " + name + " (₹" + salary + ")"; } } public class EmployeeCollectionDemo { public static void main(String[] args) { // ------------------------- // 1) TreeSet using Comparable (natural order: id) // ------------------------- TreeSet<Employee> setById = new TreeSet<>(); setById.add(new Employee(3, "Amit", 60000)); setById.add(new Employee(1, "Raj", 50000)); setById.add(new Employee(2, "Neha", 70000)); setById.add(new Employee(1, "DuplicateRaj", 55000)); // duplicate id → ignored System.out.println("TreeSet (Comparable by id): " + setById); // ------------------------- // 2) TreeSet using Comparator (by name) // ------------------------- TreeSet<Employee> setByName = new TreeSet<>(Comparator.comparing(e -> e.name)); setByName.add(new Employee(3, "Amit", 60000)); setByName.add(new Employee(1, "Raj", 50000)); setByName.add(new Employee(2, "Neha", 70000)); setByName.add(new Employee(4, "Raj", 65000)); // duplicate name → ignored System.out.println("TreeSet (Comparator by name): " + setByName); // ------------------------- // 3) TreeMap using Comparable (natural order: id as key) // ------------------------- TreeMap<Employee, String> mapById = new TreeMap<>(); mapById.put(new Employee(3, "Amit", 60000), "Team A"); mapById.put(new Employee(1, "Raj", 50000), "Team B"); mapById.put(new Employee(2, "Neha", 70000), "Team C"); System.out.println("TreeMap (Comparable by id): " + mapById); // ------------------------- // 4) TreeMap using Comparator (by salary descending) // ------------------------- TreeMap<Employee, String> mapBySalaryDesc = new TreeMap<>( Comparator.comparingDouble((Employee e) -> e.salary).reversed() ); mapBySalaryDesc.put(new Employee(3, "Amit", 60000), "Team A"); mapBySalaryDesc.put(new Employee(1, "Raj", 50000), "Team B"); mapBySalaryDesc.put(new Employee(2, "Neha", 70000), "Team C"); mapBySalaryDesc.put(new Employee(4, "Priya", 60000), "Team D"); System.out.println("TreeMap (Comparator by salary desc): " + mapBySalaryDesc); } }

Output (Conceptual)

TreeSet (Comparable by id): [1 - Raj (₹50000.0), 2 - Neha (₹70000.0), 3 - Amit (₹60000.0)] TreeSet (Comparator by name): [3 - Amit (₹60000.0), 2 - Neha (₹70000.0), 1 - Raj (₹50000.0)] TreeMap (Comparable by id): {1 - Raj (₹50000.0)=Team B, 2 - Neha (₹70000.0)=Team C, 3 - Amit (₹60000.0)=Team A} TreeMap (Comparator by salary desc): {2 - Neha (₹70000.0)=Team C, 3 - Amit (₹60000.0)=Team A, 4 - Priya (₹60000.0)=Team D, 1 - Raj (₹50000.0)=Team B}

Key Learnings

  • TreeSet

    • Uses compareTo if class implements Comparable.

    • Uses the given Comparator if provided.

    • Considers elements duplicates if comparison result = 0.

  • TreeMap

    • Orders keys by their compareTo (if Comparable).

    • Can use a custom Comparator for flexible ordering (e.g., salary, name).

    • Keys with comparison result = 0 overwrite each other.


πŸ‘‰ This shows how Comparable and Comparator co-exist and control ordering + uniqueness in Java collections.