Comparable
java.lang.Comparable<T> lets a class define the natural ordering of its objects: the order that "obviously" applies, such as numbers from small to large, text alphabetically, or dates chronologically. A class that implements Comparable can be sorted with Collections.sort or Arrays.sort, used as a key in TreeMap or an element of TreeSet, and passed to Collections.min and max, all without any extra configuration.
The Interface
public interface Comparable<T> {
int compareTo(T other);
}
a.compareTo(b) returns an int whose sign is what matters:
| Result | Meaning |
|---|---|
| Negative | a comes before b |
| Zero | a and b are equal in ordering |
| Positive | a comes after b |
Only the sign is meaningful. compareTo may return -1, -7, or -1000 for "before"; code that uses it must test < 0, == 0, or > 0, never compare with -1 or 1 directly.
System.out.println("apple".compareTo("banana")); // -1 (a before b)
System.out.println("b".compareTo("a")); // 1
System.out.println("Zebra".compareTo("apple")); // -7 ('Z' is 90, 'a' is 97)
Classes That Are Already Comparable
Many JDK types implement Comparable, which is why they can be sorted directly:
| Type | Natural order |
|---|---|
Integer, Long, Double, and other numeric wrappers | Numeric |
String | Lexicographic by UTF-16 code units (uppercase before lowercase) |
Character, Boolean | By value (false before true) |
BigDecimal, BigInteger | Numeric |
LocalDate, LocalDateTime, Instant, Duration | Chronological / by length |
Every enum | Declaration order |
UUID, Path, File | Implementation-defined |
Implementing Comparable
A class implements Comparable<ItsOwnType> and supplies compareTo:
public class Student implements Comparable<Student> {
private final String name;
private final int rollNo;
public Student(String name, int rollNo) {
this.name = name;
this.rollNo = rollNo;
}
@Override
public int compareTo(Student other) {
return Integer.compare(this.rollNo, other.rollNo);
}
@Override
public String toString() { return rollNo + ":" + name; }
}
List<Student> cls = new ArrayList<>(List.of(
new Student("Grace", 3), new Student("Ada", 1), new Student("Linus", 2)));
Collections.sort(cls);
System.out.println(cls); // [1:Ada, 2:Linus, 3:Grace]
System.out.println(Collections.max(cls)); // 3:Grace
TreeSet<Student> roster = new TreeSet<>(cls); // sorted by roll number automatically
The type parameter <Student> means compareTo receives a Student, so no cast is needed inside.
Comparing fields correctly
Use the static compare methods of the wrapper classes, or the field's own compareTo:
Integer.compare(a, b) // int fields
Long.compare(a, b) // long fields
Double.compare(a, b) // double fields (handles NaN and -0.0 consistently)
Boolean.compare(a, b)
this.name.compareTo(other.name) // fields that are themselves Comparable
Avoid the subtraction shortcut:
return this.rollNo - other.rollNo; // WRONG for large or negative values
Subtraction can overflow. With this.rollNo = Integer.MAX_VALUE and other.rollNo = -1, the difference wraps to a negative number, reporting the larger value as "before." Integer.compare never overflows.
Multiple fields
Compare the most important field first, and move to the next only when the previous ones are equal:
@Override
public int compareTo(Employee other) {
int c = this.department.compareTo(other.department);
if (c != 0) return c;
c = Integer.compare(other.salary, this.salary); // salary descending: operands swapped
if (c != 0) return c;
return this.name.compareTo(other.name);
}
Swapping the operands (other first) reverses that field's order. The same logic can be written more compactly with Comparator chains, covered in the next lesson.
The compareTo Contract
For sorting and sorted collections to behave correctly, compareTo must define a consistent total order:
- Antisymmetry: the sign of
a.compareTo(b)is the opposite ofb.compareTo(a). - Transitivity: if
a > bandb > c, thena > c. - Consistency: if
a.compareTo(b) == 0, thenaandbcompare the same way against every other object. a.compareTo(null)should throwNullPointerException.
A broken comparison can make Collections.sort throw IllegalArgumentException: Comparison method violates its general contract, or silently produce a wrong order.
Consistency with equals
It is strongly recommended, though not required, that compareTo returns 0 exactly when equals returns true. Sorted collections (TreeSet, TreeMap) use compareTo to detect duplicates, while hash collections use equals. When the two disagree, the collections disagree too. BigDecimal is the well-known example:
BigDecimal a = new BigDecimal("2.0");
BigDecimal b = new BigDecimal("2.00");
System.out.println(a.equals(b)); // false: different scale
System.out.println(a.compareTo(b)); // 0: numerically equal
System.out.println(new HashSet<>(List.of(a, b)).size()); // 2
System.out.println(new TreeSet<>(List.of(a, b)).size()); // 1
The same two values form a set of size 2 or size 1 depending on the set implementation. When you implement both methods yourself, base them on the same fields so that this cannot happen.
Comparable and Records
Records do not implement Comparable automatically, but they can:
public record Money(long paise) implements Comparable<Money> {
@Override
public int compareTo(Money other) {
return Long.compare(paise, other.paise);
}
}
The record's generated equals compares paise too, so the natural ordering is consistent with equals.
Comparable Versus Comparator
Comparable | Comparator | |
|---|---|---|
| Package | java.lang | java.util |
| Method | compareTo(T other) | compare(T a, T b) |
| Defined | Inside the class being compared | Outside, as a separate object |
| How many orderings | One: the natural order | As many as you need |
| Used by default | Collections.sort(list), new TreeSet<>() | When passed explicitly: list.sort(cmp), new TreeSet<>(cmp) |
| When to use | There is one obvious order | Alternative orders, or classes you cannot modify |
Give a class a natural ordering only when one ordering is clearly the right default: version numbers, money amounts, dates. A Product has no single obvious order (by name? price? rating?), so it is better sorted with comparators supplied where needed.
Practical Example: Semantic Version Numbers
Software versions like 1.10.0 and 1.9.12 must be compared part by part as numbers. Sorting them as strings gives the wrong answer, because character by character "1.10.0" comes before "1.9.12".
import java.util.*;
public final class Version implements Comparable<Version> {
private final int major, minor, patch;
public Version(int major, int minor, int patch) {
if (major < 0 || minor < 0 || patch < 0) throw new IllegalArgumentException("negative part");
this.major = major;
this.minor = minor;
this.patch = patch;
}
public static Version parse(String text) {
String[] p = text.strip().split("\\.");
if (p.length != 3) throw new IllegalArgumentException("expected MAJOR.MINOR.PATCH: " + text);
return new Version(Integer.parseInt(p[0]), Integer.parseInt(p[1]), Integer.parseInt(p[2]));
}
@Override
public int compareTo(Version other) {
int c = Integer.compare(major, other.major);
if (c != 0) return c;
c = Integer.compare(minor, other.minor);
if (c != 0) return c;
return Integer.compare(patch, other.patch);
}
@Override
public boolean equals(Object o) {
return o instanceof Version v && compareTo(v) == 0;
}
@Override
public int hashCode() { return Objects.hash(major, minor, patch); }
@Override
public String toString() { return major + "." + minor + "." + patch; }
public static void main(String[] args) {
List<String> tags = List.of("1.10.0", "1.2.3", "2.0.0", "1.9.12", "1.2.10");
List<String> asText = new ArrayList<>(tags);
Collections.sort(asText);
System.out.println("String order: " + asText);
List<Version> versions = new ArrayList<>();
for (String t : tags) versions.add(Version.parse(t));
Collections.sort(versions);
System.out.println("Version order: " + versions);
Version installed = Version.parse("1.9.12");
Version latest = Collections.max(versions);
System.out.println("Update available: " + (installed.compareTo(latest) < 0) + " (latest " + latest + ")");
TreeSet<Version> released = new TreeSet<>(versions);
System.out.println("Next after 1.2.10: " + released.higher(Version.parse("1.2.10")));
}
}
Output:
String order: [1.10.0, 1.2.10, 1.2.3, 1.9.12, 2.0.0]
Version order: [1.2.3, 1.2.10, 1.9.12, 1.10.0, 2.0.0]
Update available: true (latest 2.0.0)
Next after 1.2.10: 1.9.12
The string sort compares characters, so "1.10.0" lands first and "1.2.10" before "1.2.3". Version.compareTo compares major, then minor, then patch numerically, stopping at the first difference, which produces the correct order. Because Version is Comparable, Collections.sort, Collections.max, and TreeSet all work with no further code, and higher finds the next release. equals is defined in terms of compareTo and hashCode uses the same three fields, so Version behaves consistently in both hash-based and sorted collections. The class is final with final fields, so its ordering can never change after an object is placed in a sorted collection.