AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

09 Interfaces / Lesson 4 of 5

Comparable, Iterable and designing with interfaces

14 min · +10 XP · 2 exercises · 5 questions

Read the text14 min
Exercises0/2
Quiz5 q

Quick answer

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.

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) == 0 iff a.equals(b). TreeSet and TreeMap use compareTo instead of equals, so an inconsistency means a TreeSet and a HashSet disagree about which elements are duplicates. BigDecimal (2.0 vs 2.00) is the standard cautionary example.
  • Throws NullPointerException for 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

  1. Small. One to five methods. Comparable has one; Iterable has one. A twenty-method interface is implemented rarely and mocked painfully. Split by role (interface segregation): Readable and Writable, not ReadWritable.
  2. Named for the capability. Closeable, Runnable, Repository<T>, Shape. Avoid I prefixes and Impl suffixes; ArrayList implements List, not ListImpl implements IList.
  3. Stable. Adding abstract methods breaks implementors; add default methods or a new interface (SortedSet extends Set).
  4. Documented contracts. What compareTo(null) does, whether iterator() is repeatable, whether close() is idempotent — the Javadoc is the real interface.
  5. Depend on interfaces at boundaries. Parameters and return types of public APIs should be interfaces (List, not ArrayList; Map, not HashMap) so callers and implementations can change independently.
  6. Provide a skeleton when the interface has many methods derivable from a few (AbstractList), or defaults when they derive from the abstract ones.
  7. 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 UnsupportedOperationException from half the methods — the Liskov violation in interface form. (Collection does 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

  • "Comparable vs Comparator?" Natural order inside the class vs external orderings passed to sorts; one vs many.
  • "Why must compareTo be consistent with equals?" Sorted collections use compareTo to decide duplicates.
  • "How does for-each work on your class?" Implement Iterable; the loop calls iterator(), then hasNext/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 with equals; unlocks sorting and sorted collections.
  • Comparator for every other ordering; build with comparing/thenComparing/reversed.
  • Iterable.iterator() returning a fresh Iterator gives for-each and forEach; 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.

Mark as readSign in to keep your place.

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) == 0 iff a.equals(b). TreeSet and TreeMap use compareTo instead of equals, so an inconsistency means a TreeSet and a HashSet disagree about which elements are duplicates. BigDecimal (2.0 vs 2.00) is the standard cautionary example.
  • Throws NullPointerException for 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

  1. Small. One to five methods. Comparable has one; Iterable has one. A twenty-method interface is implemented rarely and mocked painfully. Split by role (interface segregation): Readable and Writable, not ReadWritable.
  2. Named for the capability. Closeable, Runnable, Repository<T>, Shape. Avoid I prefixes and Impl suffixes; ArrayList implements List, not ListImpl implements IList.
  3. Stable. Adding abstract methods breaks implementors; add default methods or a new interface (SortedSet extends Set).
  4. Documented contracts. What compareTo(null) does, whether iterator() is repeatable, whether close() is idempotent — the Javadoc is the real interface.
  5. Depend on interfaces at boundaries. Parameters and return types of public APIs should be interfaces (List, not ArrayList; Map, not HashMap) so callers and implementations can change independently.
  6. Provide a skeleton when the interface has many methods derivable from a few (AbstractList), or defaults when they derive from the abstract ones.
  7. 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 UnsupportedOperationException from half the methods — the Liskov violation in interface form. (Collection does 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

  • "Comparable vs Comparator?" Natural order inside the class vs external orderings passed to sorts; one vs many.
  • "Why must compareTo be consistent with equals?" Sorted collections use compareTo to decide duplicates.
  • "How does for-each work on your class?" Implement Iterable; the loop calls iterator(), then hasNext/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 with equals; unlocks sorting and sorted collections.
  • Comparator for every other ordering; build with comparing/thenComparing/reversed.
  • Iterable.iterator() returning a fresh Iterator gives for-each and forEach; 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=0

Ranges 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=18

In this module: Interfaces

← Functional interfaces and the shape of a lambda · Sealed interfaces — closed sets of types →