Skip to main content

Command Palette

Search for a command to run...

Comparison in Java : Comparator vs Comparable

The question is never whether to compare or not but how to compare?

Updated
•16 min read•View as Markdown
Comparison in Java : Comparator vs Comparable
K

I am Java/SpringBoot developer. Currently pursuing MSCS from UNC Chapel Hill. Actively looking for Job.

Learning Objectives :

  • Understanding broader architectural support in Java to facilitate custom object comparison/sorting

  • Two Specific Interfaces in depth : Comparable<T> vs Comparator<T>

    • Why Comparator is usually preferred ?
  • Implementation examples for both case.

  • Tips to retain learning: which interface has which method i.e. Match the following :

  • Segway to using Comparator<T> in Streams (to be covered later)


Comparison (of two similar objects) is as essential in Java as in real world. Afterall programming is nothing but emulating real world scenarios. While operators like ==, !=, <, > make it easier to compare the primitives like boolean, short, int, comparing Objects can be arduous task. And using it for higher order tasks such as sorting can be even more complicated. To make the task of comparing Objects easier java provides two interfaces.

Java Approaches of comparing two objects :

  1. java.util.Comparator<T> :

  2. java.lang.Comparable<T> :

Note : java.lang has some other Adjective(-able) interfaces like

  • Appendable - Appends the specified character sequence to this Appendabl

  • AutoCoseable - An object that may hold resources (file or socket handles) until it is closed. The close() method of an AutoCloseable object is called automatically when exiting a try-with-resources block for which the object has been declared in the resource specification header

  • Cloneable - A class implements the Cloneable interface to indicate to the Object.clone() method that it is legal for that method to make a field-for-field copy of instances of that class

  • Iterable - Implementing this interface allows an object to be the target of the enhanced for statement ( "for-each loop").

  • Readable - A Readable is a source of characters. Characters from a Readable are made available to callers of the read method via a CharBuffer.


Let us take a POJO class Person on which we can learn how to use comparison.

class Person {
    private String name;
    private int age;
    private long id;
    // constructors, getters, setters, toString()...
}
// this class is INCOMPARABLE i.e. no information provided anywhere(inside or outside) class
// on how to order two different objects of this class.

How can we sort collection of Persons ? How will any function trying to sort instances of this class be able sort them? Should it sort by person’s age or by name or in case of same age sort based on names ? You see there is no way to accord priority to one over the other.

List<Person> personsInMyContactList = new ArrayList<>();
// populate the list using 
//     database or 
//     maually using add()

Person[] personsInMyEmailList = new Person[100]; 

// How can we sort these collections ?

Before proceeding further let us see why we took only example of a List<> and an array for collection? It is because in java there are broadly only 4 types of collection

  1. Arrays

  2. List

  3. Set

  4. Map

Among which only first two makes sense to always sort. In set and map only in some cases it makes sense to sort & for such cases we have specific implementations like TreeSet<> and TreeMap<>.

Method for sorting arrays of primitives or objects is provided in Arrays utility class :

https://docs.oracle.com/javase/8/docs/api/java/util/Arrays.html#sort-T:A-java.util.Comparator-

public static <T> void sort(T[] a,
                            Comparator<? super T> c)

Method for sorting List of objects is provided in Collections utility class :

https://docs.oracle.com/javase/8/docs/api/java/util/Collections.html#sort-java.util.List-java.util.Comparator-

public static <T> void sort(List<T> list,
                            Comparator<? super T> c)

Further, Java collection framework (java.util.Collections with s) provides two broad approaches two sort any collection of objects :

  1. Using Natural order - i.e. inherent quality/characteristics of the Class

    • it is sticky - once defined it is difficult to change the meaning

    • it becomes the nature of all the instances of the class.

sort(List<T> list)

Sorts the specified list into ascending order, according to the natural ordering of its elements.

  1. Using Custom Comparator - meaning user defines what makes one object take priority over other based on relation among their attributes.
  • it is a characteristic which can be plugged in & out to a class & hence provides great flexibility.

  • It allows to decouple comparison definition from the class definition.

  • It is like Views for a database table.

sort(List<T> list, Comparator<? super T> c)

Sorts the specified list according to the order induced by the specified comparator.


Comparable - int compareTo(T o) : Natural Ordering

// not marked Functional Interface : why ? answer later in this blog
public interface Comparable<T> {
    public int compareTo(T o);
}

Note : Java expects you to override these methods following certain rule.

According to the Javadoc for Comparable.compareTo(T o) and Comparator.compare(T o1, T o2):

Returns a negative integer, zero, or a positive integer as the first object is less than, equal to, or greater than the second object.

But while which one is `first` in Comparator.compare(T o1, T o2) it is not clear which one is first in Comparable.compareTo(T o) . Let's see this :

Comparable and Comparator are two sides of the exact same coin:

        Comparable Interface                     Comparator Interface
   ──────────────────────    ───────────────────────
       int compareTo(T o)                       int compare(T o1, T o2)
   
        this  ===> FIRST object                  o1   ===> FIRST object
        o     ===> SECOND object                 o2   ===> SECOND object
   
        Mental Math: (this - o)                  Mental Math: (o1 - o2)

Let’s see how we can use it for our custom class :

class Person implements Comparable<Person> {
    private String name;
    private int age;
    private long id;
    // constructors, 
    // Getters & Setters, 
    // toString() etc. 

    @Override
    int compareTo(Person that) {
        // user defines the comparsion logic
        if (this.age < that.age){
            return -1;
        }
        return (this.age == that.age) ? 0 : 1;
        // we can write much complex logic here e.g. if age is equal THEN compare name... 
        // and so on 
}
// This class is now COMPARABLE in true sense as it has natural(default) ordering definition
// embedded in class definition itself.

Above implementation makes the hitherto incomparable Person truly Comparable because now calling functions know the its natural comparison characteristics - what makes one object greater (or smaller or equal) than other. In other words HOW TO COMPARE has been clearly defined in the class definition.

java.util.Collections

static <T extends Comparable<? super T>> void sort(List<T> list) Sorts the specified list into ascending order, according to the natural ordering of its elements.
static <T> void sort(List<T> list, Comparator<? super T> c) Sorts the specified list according to the order induced by the specified comparator.

List<Person> people = new ArrayList<>(); // populate the list using database or maually using add()

Person[] peopleArr= new Person[100]; 

// How can we sort these collections ?

// NATURAL ORDER SORTING : 
//     meaning sorted based on comparison definition in the overridden method 
//     inside the Person class
Collections.sort(people); // Only works with list. 

// Soring array of objects 
Arrays.sort(peopleArr);

Now most of the commonly used Class like String, Integer, Character, Double provides natural ordering definition i.e. these classes implement Comparable interface.

Classes Implementing Comparable

Class Natural Ordering
Byte Signed numerical
Character Unsigned numerical
Long Signed numerical
Integer Signed numerical
Short Signed numerical
Double Signed numerical
Float Signed numerical
BigInteger Signed numerical
BigDecimal Signed numerical
Boolean Boolean.FALSE < Boolean.TRUE
File System-dependent lexicographic on path name
String Lexicographic
Date Chronological
CollationKey Locale-specific lexicographic

This gives us insight on when to use COMPARABLE? When you want to create default ordering/sorting logic for instances of a class use Comparable.

How to sort a collection using natural ordering ?

Collections.sort(objList);//list of String, Integer... or any class implementing Combarable
Arrays.sort(objArr); // array of  String, Integer... or any class implementing Combarable

Are only lists and arrays supported for sorting objects based on natural ordering? No.

Class Interface Sorting Behavior Time Complexity (Add)
TreeSet NavigableSet Fully Sorted (Tree) O(log n)
TreeMap NavigableMap Sorted by Key (Tree) O(log n)
PriorityQueue Queue Head is Min (Heap) O(log n)
ConcurrentSkipListSet NavigableSet Thread-safe Sorted O(log n)

Mor on this later.

Problem with sorting based on Natural Ordering ?

Shor answer : rigidity.

At any fixed time Comparable provides only one way to order its implementing class objects.

What if in some other scenarios comparison based on person’s name or id or some completely new attribute e.g. weight which was not originally envisaged but later added due to new requirements ? Surely you are forced to edit the int compareTo(Person that) every time there is change in comparison logic. But this is not a the problem. Afterall you need to make change somewhere. The real problem is poor backward compatibility and code maintainability. The comparision logic is strictly tied to the class.

You see one way to see the entire comparison story is that it provides us with views , specific lenses to see the data(collection) based on ordering we want to see.

At one place we might want to view people based on their ascending age, elsewhere it might be desirable to view people according to descending weights. So it might be very convenient to decouple the comparison logic from the class itself. If so, we can define the comparison logic in some external classes and then we can create multiple views.

Thankfully, Comparator<T> provides us this flexibility.

Comparator - int compare(T o1, T o2) : Custom Ordering

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2); // the only fuction which mandatory needs to be implemented

// other default and static methods...
    default Comparator<T> reversed() {}
    default Comparator<T> thenComparing(Comparator<? super T> other) {}
    default <U> Comparator<T> thenComparing(){}
    public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {}
    public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {}
}

Let’s see how we can use it for our custom class :

class Person {
    private String name;
    private int age;
    private long id;
    private long id;
    // constructors, getters, setters, toString()...
}

Custom Comparator to order person based on name :

class PersonNameComparator implements Comparator<Person> {
    @Override
        public int compare(Person o1, Person o2) {
            return o1.name.compareTo(o2.name);//internally uses natural ordering of String
        }
    // overwriding other defaultmethods is optional. static methods cannot be overridden
}

Another comparator which also providers a tie breaker in case of same age

class PersonAgeThenNameComparator implements Comparator<Person> {
    @Override
        public int compare(Person o1, Person o2) {
            if(this.age < o.age) return -1;
            if(this.age > o.age) return +1;
            // (this.age == o.age) 
            return o1.name.compareTo(o2.name); // TIE-BREAK RULE
        }
}

The advantage of this way is very clear. We can write comparator on demand. Unlike in the case of Comparable, the comparison logic is not rigidly tied to the class. The code blocks relying on PersonNameComparator can still continue using the same comparator. And the new code blocks which requires to use PersonAgeThenNameComparator can easily use it without even being aware of any other comparator.


Combining power of Comparable & Comparator

Now a question arises :

Can a Comparable class(overriding int compareTo() ) be sorted by custom comparator(overriding int compare() ) ?

The answer is a resounding YES.

In fact, this is the standard design pattern in Java. A custom Comparator always overrides the natural ordering defined by Comparable.

The Rule of Priority

  1. Default: If you call Collections.sort(list), Java uses the class's compareTo() method (Natural Order).

  2. Override: If you call Collections.sort(list, comparator), Java ignores the compareTo() method completely and uses your Comparator instead.

Code Example

Let's create a Student class that sorts by ID by default, but we will force it to sort by Name using a Comparator.

import java.util.*;

// 1. Class implements Comparable (Default: Sort by ID)
class Student implements Comparable<Student> {
    int id;
    String name;

    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }

    // Natural Ordering logic
    @Override
    public int compareTo(Student other) {
        return this.id - other.id; // Ascending ID
    }

    @Override
    public String toString() { return name + "(" + id + ")"; }
}

public class Main {
    public static void main(String[] args) {
        List<Student> classRoom = new ArrayList<>();
        classRoom.add(new Student(3, "Charlie"));
        classRoom.add(new Student(1, "Alice"));
        classRoom.add(new Student(2, "Bob"));

        // SCENARIO 1: Natural Ordering (Uses Comparable)
        Collections.sort(classRoom);
        System.out.println("By ID (Default):   " + classRoom);
        // Output: [Alice(1), Bob(2), Charlie(3)]

        // SCENARIO 2: Custom Comparator (Overrides Comparable)
        // We define a new rule: Sort by Name
        Comparator<Student> nameSorter = new Comparator<Student>() {
            @Override
            public int compare(Student s1, Student s2) {
                return s1.name.compareTo(s2.name);
            }
        };

        Collections.sort(classRoom, nameSorter);
        System.out.println("By Name (Custom):  " + classRoom);
        // Output: [Alice(1), Bob(2), Charlie(3)] -> Wait, Alice/Bob/Charlie is alphabetical too.
        // Let's assume Z-names to prove it works differently:
        // If we had [Zack(1), Adam(2)], ID sort -> Zack, Adam. Name sort -> Adam, Zack.
    }
}

Why is this useful?

Think of an E-commerce product page (like Amazon):

  1. Default (Comparable): Search results usually have a "Relevance" score. The Product class compareTo might implement this complex relevance logic.

  2. User Selection (Comparator): The user clicks "Sort by Price: Low to High". You don't rewrite the class; you just pass a PriceComparator to the list. The user clicks "Sort by Rating". You pass a RatingComparator.

Summary

  • Comparable = The Default setting.

  • Comparator = The User Preference setting (Wins).


Lets now delve into tricks to memorize the differences.

Comparable<T> Comparator<T>
Parts of Speech adjective noun
represents State(Identity) Strategy(Function)
package java.lang java.util
SAM n/a @FunctionalInterface
function public int compareTo(T o) int compare(T o1, T o2)
trick (always one t) Comparable - compareTo() Comparator - compare()
relationship IS-A HAS-A
trait of an object tool to compare two objects
Purpose natural ordering custom ordering
Views only one view one view per Comparator

Why Comparator is marked @FunctionalInterface but not Comparable ?

Short answer: Comparable represents "State" (Identity), while Comparator represents "Strategy" (Function).

Parts of Speech :

  • "Comparable" adjective - "able to be compared" or "worthy of comparison"

  • "Comparator" noun - a person or thing that compares

The Method Signature Difference

  • Comparator<T> (compare(T o1, T o2)):

    • Takes two external objects.

    • It does not rely on its own internal state (this).

    • It is a pure function: f(x,y) —> {int}.

    • Verdict: This fits the definition of a Lambda function perfectly.

  • Comparable<T> (compareTo(T o)):

    • Takes one external object.

    • The other object is this (the instance itself).

    • It fundamentally relies on the internal state of the object it is attached to.

    • Verdict: This is not a "function" in the functional programming sense; it is an object method.

The Conceptual "Is-A" vs "Has-A"

  • Comparable: An object IS comparable. A Student object is comparable to another Student. It is intrinsic to the class definition. You implement it inside the class.

  • Comparator: An object HAS A comparator. You create a separate "Sorting Machine" (Strategy) to sort Students by grade, or by name. This "Machine" is a function.

Summary

  • Comparator is a tool/function used to compare two other things. → Functional Interface.

  • Comparable is a trait of an object itself. It requires this instance to exist. → Regular Interface.

Finally,

Trick to recall which one has what function name : only one t in InterfaceName-MethodName pair

Comparable< T> int compare**T**o(T o) Interface w/o t has method with t
Compara**t**or< T> int compare(T o1, T o2); Interface w/ t has method w/o t

The Underlying Design Pattern

Strategy Pattern

We can find some similarity between : (extending Thread vs implementing Runnable) to create a thread vs (implementing Comparable & Comparator) to make a class comparable.

Both pairs are the same pattern wearing different clothes — the Strategy Pattern:

Strategy Pattern:
  Separate WHAT is done from WHO does it / HOW it is triggered

Threading:  Task (Runnable)   ←→  Executor (Thread / pool)
Sorting:    Order (Comparator) ←→  Sorter  (Collections.sort / Stream.sorted)

The "baked-in" versions (extends Thread, Comparable) violate this separation. The "separated" versions (Runnable, Comparator) honour it — which is exactly why both are the preferred, flexible choice in modern Java.

References

https://docs.oracle.com/javase/tutorial/collections/interfaces/order.html

I take help of LLMs (chatGPT, Gemini) to understand these concepts.