How to Implement Comparable and Iterable in Java
Comparable gives a Java class a natural order; Iterable lets for-each loop over it. The compareTo contract, Comparator, iterators and interface design.
- Course: Java study plan
- Module: Interfaces
- Kind: Lesson
- Reading time: 14 min
- Runtime: OpenJDK 18
How do you implement Comparable in Java?
To implement Comparable in Java, declare implements Comparable<T> and write compareTo(T o) to return a negative number, zero or a positive number for less than, equal or greater than. Compare the most significant field first with Integer.compare or Double.compare, never subtraction, and keep it consistent with equals. Collections.sort, TreeMap and PriorityQueue then use that natural order.
Lesson
Two interfaces from the JDK teach more about interface design than any abstract discussion: Comparable, which gives a type a natural order, and Iterable, which lets it be looped over with for-each. Implementing both correctly is expected of any value type you write, and both illustrate how a small contract unlocks a large amount of library behaviour. The second half of the lesson turns those lessons into guidelines for interfaces of your own.
Comparable<T>: natural order
public final class Version implements Comparable<Version> {
private final int major, minor, patch;
@Override
public int compareTo(Version o) {
int c = Integer.compare(major, o.major);
if (c != 0) return c;
c = Integer.compare(minor, o.minor);
if (c != 0) return c;
return Integer.compare(patch, o.patch);
}
}
compareTo returns negative, zero or positive for less-than, equal, greater-than. The contract:
- Antisymmetric:
sgn(a.compareTo(b)) == -sgn(b.compareTo(a)). - Transitive.
- Consistent with equals (strongly recommended):
a.compareTo(b) == 0iffa.equals(b).TreeSetandTreeMapusecompareToinstead ofequals, so an inconsistency means aTreeSetand aHashSetdisagree about which elements are duplicates.BigDecimal(2.0vs2.00) is the standard cautionary example. - Throws
NullPointerExceptionfor a null argument (it is not meant to handle null).
Idioms: compare fields most-significant first; use Integer.compare/Long.compare/Double.compare, never subtraction (overflow) or == on doubles; for strings compareTo or compareToIgnoreCase; for a chain of fields, Comparator.comparing(...).thenComparing(...) inside compareTo reads well:
private static final Comparator<Version> ORDER =
Comparator.comparingInt((Version v) -> v.major).thenComparingInt(v -> v.minor).thenComparingInt(v -> v.patch);
@Override public int compareTo(Version o) { return ORDER.compare(this, o); }
What implementing it buys: Collections.sort(list), Arrays.sort(array), list.sort(null), Collections.max/min, TreeSet/TreeMap keys, stream().sorted(), PriorityQueue ordering — all without passing a comparator.
Comparator<T>: other orders
A type has one natural order but many useful ones. Comparator is an external ordering passed to the sort:
list.sort(Comparator.comparing(Person::lastName).thenComparing(Person::firstName));
list.sort(Comparator.comparingInt(Person::age).reversed());
list.sort(Comparator.comparing(Person::nickname, Comparator.nullsLast(Comparator.naturalOrder())));
Implement Comparable for the order (if there is one obvious order); supply Comparators for the rest. A class with no obvious order (Employee?) should implement neither and let callers pass comparators.
Iterable<T>: for-each over your type
public class Range implements Iterable<Integer> {
private final int lo, hi;
public Range(int lo, int hi) { this.lo = lo; this.hi = hi; }
@Override
public Iterator<Integer> iterator() {
return new Iterator<>() {
private int next = lo;
@Override public boolean hasNext() { return next < hi; }
@Override public Integer next() {
if (!hasNext()) throw new NoSuchElementException();
return next++;
}
};
}
}
for (int i : new Range(0, 5)) System.out.print(i); // 01234
The for-each loop desugars to for (Iterator<T> it = x.iterator(); it.hasNext(); ) { T v = it.next(); … }. Implementing Iterable means providing an Iterator — an object with hasNext()/next() (and optionally remove()). Rules: next() without a preceding successful hasNext() must throw NoSuchElementException, not return garbage; each call to iterator() returns a fresh, independent iterator; Iterable also gives you forEach(Consumer) and spliterator() as defaults, so StreamSupport.stream(range.spliterator(), false) opens streams.
Lazy iteration is the point: Range never materialises a list. Iterators over trees, files and generated sequences all follow this shape.
Guidelines for your own interfaces
- Small. One to five methods.
Comparablehas one;Iterablehas one. A twenty-method interface is implemented rarely and mocked painfully. Split by role (interface segregation):ReadableandWritable, notReadWritable. - Named for the capability.
Closeable,Runnable,Repository<T>,Shape. AvoidIprefixes andImplsuffixes;ArrayList implements List, notListImpl implements IList. - Stable. Adding abstract methods breaks implementors; add default methods or a new interface (
SortedSet extends Set). - Documented contracts. What
compareTo(null)does, whetheriterator()is repeatable, whetherclose()is idempotent — the Javadoc is the real interface. - Depend on interfaces at boundaries. Parameters and return types of public APIs should be interfaces (
List, notArrayList;Map, notHashMap) so callers and implementations can change independently. - Provide a skeleton when the interface has many methods derivable from a few (
AbstractList), or defaults when they derive from the abstract ones. - One method → consider functional. If it is a single operation, a functional interface lets callers use lambdas.
Interfaces and testing
An interface at a boundary — PaymentGateway, Clock, Repository — is what makes the code that uses it testable: the test supplies a fake implementation (InMemoryRepository, a fixed Clock) and exercises the logic without a network or a database. This is the practical reason for "program to an interface" in application code: not flexibility for its own sake, but substitutability where it matters. java.time.Clock is the JDK's own example.
Anti-patterns
- Header interfaces: one interface per class, mirroring every method, implemented once. Noise without substitutability.
- Constant interfaces (Module 9 lesson 1).
- God interfaces implemented by throwing
UnsupportedOperationExceptionfrom half the methods — the Liskov violation in interface form. (Collectiondoes exactly this for optional operations and is widely regarded as a compromise, not a model.) - Leaking implementation types through interface methods (
ArrayList<T> items()on an interface).
Interview angle
- "
ComparablevsComparator?" Natural order inside the class vs external orderings passed to sorts; one vs many. - "Why must
compareTobe consistent withequals?" Sorted collections usecompareToto decide duplicates. - "How does for-each work on your class?" Implement
Iterable; the loop callsiterator(), thenhasNext/next. - "What is interface segregation?" Small, role-specific interfaces so implementors depend only on what they use.
Key takeaways
Comparable.compareTo: most significant field first,Integer.compare, consistent withequals; unlocks sorting and sorted collections.Comparatorfor every other ordering; build withcomparing/thenComparing/reversed.Iterable.iterator()returning a freshIteratorgives for-each andforEach;next()past the end throws.- Design interfaces small, capability-named, stable, documented, and use them at boundaries — that is also what makes code testable.
Common questions
What is the difference between Comparable and Comparator in Java?
Comparable gives a class its one natural order through compareTo, written inside the class. A Comparator is an external ordering passed to a sort or a sorted collection, so a type can have many — built with Comparator.comparing, thenComparing and reversed.
Why should compareTo be consistent with equals?
TreeSet and TreeMap use compareTo instead of equals to decide which elements are duplicates. If the two disagree, a TreeSet and a HashSet given the same elements keep different ones; BigDecimal, where 2.0 and 2.00 compare as equal but are not equals, is the classic example.
How do you make a class work with a for-each loop in Java?
Implement Iterable<T> by returning a fresh Iterator<T> from iterator(). The for-each loop calls iterator() once, then hasNext() and next() until the elements run out; next() past the end must throw NoSuchElementException.
Why should you not use subtraction in compareTo?
a - b overflows when the values are far apart — a large positive minus a large negative wraps round to a negative number — and the order silently breaks. Integer.compare(a, b) and Long.compare always give the correct sign, and Double.compare also handles NaN and negative zero.
What is the interface segregation principle?
Interfaces should be small and role-specific, so implementors depend only on the methods they use: Readable and Writable rather than one ReadWritable. Comparable and Iterable each have a single abstract method, which is why so many classes implement them.
Exercises
A countdown you can loop over
Write class Countdown implements Iterable<Integer> that iterates from a start value down to 1 (an empty sequence if the start is below 1). Its iterator() must return a fresh iterator each call, and next() past the end must throw NoSuchElementException.
Read an integer n and n start values. For each, iterate with a for-each loop and print the values space-separated (an empty line for an empty sequence), then a second line sum=<sum> computed by a second for-each over the same object.
Example: 2 then 3 0 →
3 2 1
sum=6
sum=0Ranges you can loop and sort
Write record Range(int from, int to) implements Iterable<Integer>, Comparable<Range>: iterating yields from up to but excluding to; the natural order is by length ascending, then by from. Read n ranges, print each in sorted order as [from,to) size=<len> sum=<sum of its elements via for-each>.
Example: 3 then 5 8, 0 2, 10 12 →
[0,2) size=2 sum=1
[10,12) size=2 sum=21
[5,8) size=3 sum=18In this module: Interfaces
← Functional interfaces and the shape of a lambda · Sealed interfaces — closed sets of types →