oop Lesson 6 20 min read

Interfaces and Comparable

Contracts, type promises, and natural ordering

Reading: Reges & Stepp: Ch. 9 §9.1, Ch. 10 §10.1

After this lesson, you will be able to:

  • Explain what an interface is and why Java uses them
  • Declare an interface and implement it in a class
  • Implement Comparable<T> to define natural ordering for custom objects
  • Write a correct compareTo method that returns negative, zero, or positive
  • Sort arrays of objects using Arrays.sort with Comparable
  • Explain the relationship between compareTo and equals

The Sorting Problem

You already know how to sort arrays of primitives:

int[] nums = {5, 2, 8, 1, 9};
Arrays.sort(nums);  // Works: [1, 2, 5, 8, 9]

String[] words = {"zebra", "apple", "banana"};
Arrays.sort(words);  // Works: [apple, banana, zebra]

Now try sorting an array of Student objects:

Student[] roster = {
    new Student("Alice", 3.8, 1001),
    new Student("Bob", 3.2, 1002),
    new Student("Charlie", 3.5, 1003)
};

Arrays.sort(roster);  // ClassCastException! Java doesn't know how to compare Students.

Java does not know how to compare Students. Should Alice come before Bob because her name is alphabetically earlier? Because her GPA is higher? Because her ID is lower? That is a design decision that only you can make. You need a way to tell Java the natural ordering for Student objects.

From CSCD 110: In Python, you could sort objects with a key function: sorted(roster, key=lambda s: s.gpa). Java takes a different approach. Instead of passing a key function from the outside, you make the class itself declare how its objects should be compared. The class implements a contract called Comparable.


Interfaces: A Contract

An interface is a Java construct that specifies a contract: “If you claim to implement this interface, you promise to provide these methods.” It declares what methods must exist, but says nothing about how they work. That is the job of the class that implements the interface.

The syntax for declaring an interface:

public interface Greetable {
    String greet();
    // Method signature only — no body, no curly braces with code
}

A class that implements the interface must provide every method the interface declares:

public class Student implements Greetable {

    private final String name;
    // ... other fields, constructor ...

    @Override
    public String greet() {
        return "Hi, I'm " + name;
    }
}

The implements keyword is the promise. The @Override annotation tells the compiler: “Verify that this method actually fulfills an interface requirement.” If you misspell the method name or get the return type wrong, the compiler catches it immediately.

Key Insight: Interfaces separate what a class must do from how it does it. The interface defines the contract; the class fulfills the promise. This is the foundation of polymorphism — code that works with the interface works with any class that implements it.


The Comparable Interface

Java provides a built-in interface called Comparable<T>. Here is the entire interface:

public interface Comparable<T> {
    int compareTo(T other);
}

One method. That is the whole contract. If a class implements Comparable<T>, it promises to provide a compareTo method that defines how objects of that class should be ordered.

The <T> is a type parameter — it gets replaced by the actual class name when you implement it. For Student, you write Comparable<Student>, so compareTo takes a Student parameter instead of a generic Object.

The compareTo Contract

The compareTo method returns an int with the following meaning:

Return Value Meaning
Negative number this comes before other
Zero this and other are equal in ordering
Positive number this comes after other

Think of it as subtraction on a number line. If this is “smaller” (should come first), the result is negative. If they are the same, the result is zero. If this is “larger” (should come later), the result is positive.

Check Your Understanding
If a.compareTo(b) returns a negative number, what does that mean?</p>
Aa should come before b in sorted order
Bb should come before a in sorted order
Ca and b are equal
DThe comparison failed
A negative return value means this (the object calling compareTo) should come before the argument. Think of it like subtraction: if a is "less than" b, the difference is negative, and a should appear first in ascending order.
--- ## Implementing Comparable on Student To make Students sortable, the class implements `Comparable` and provides a `compareTo` method. Let's sort by GPA in descending order (highest first): ```java import java.util.Arrays; public class Student implements Comparable { private final String name; private final double gpa; private final int id; public Student(final String name, final double gpa, final int id) { if (name == null || name.isBlank()) { throw new IllegalArgumentException("name cannot be null or blank"); } if (gpa < 0.0 || gpa > 4.0) { throw new IllegalArgumentException("gpa must be between 0.0 and 4.0"); } if (id <= 0) { throw new IllegalArgumentException("id must be positive"); } this.name = name; this.gpa = gpa; this.id = id; } public String getName() { return name; } public double getGpa() { return gpa; } public int getId() { return id; } @Override public int compareTo(final Student other) { // Sort by GPA descending (highest first) return Double.compare(other.gpa, this.gpa); } @Override public String toString() { return name + " (GPA: " + gpa + ")"; } } ``` Now `Arrays.sort` works: ```java Student[] roster = { new Student("Alice", 3.8, 1001), new Student("Bob", 3.2, 1002), new Student("Charlie", 3.5, 1003) }; Arrays.sort(roster); for (Student s : roster) { System.out.println(s); } // Output: // Alice (GPA: 3.8) // Charlie (GPA: 3.5) // Bob (GPA: 3.2) ``` ### Why Double.compare Instead of Subtraction? You might be tempted to write `return (int) (other.gpa - this.gpa)`. Do not do this. Casting a `double` difference to `int` truncates: `3.8 - 3.5` becomes `0.3`, which truncates to `0` — Java would think the two students are equal. `Double.compare` returns the correct negative, zero, or positive integer without truncation. For `int` fields, subtraction works safely in most cases: `return this.id - other.id` produces the correct sign. But beware of overflow with extreme values — `Integer.compare(this.id, other.id)` is always safe. > **Common Pitfall:** Never cast a `double` difference to `int` for comparison. Use `Double.compare(a, b)` for doubles and `Integer.compare(a, b)` for ints when overflow is a concern. --- ## Sorting by Name Instead The natural ordering is a design choice. If you want alphabetical order by name instead of GPA order, change `compareTo`: ```java @Override public int compareTo(final Student other) { // Sort by name alphabetically (ascending) return this.name.compareTo(other.name); } ``` This works because `String` already implements `Comparable` — its `compareTo` method compares characters lexicographically (dictionary order). You are delegating to an existing `compareTo` implementation. --- ## Why Interfaces Matter `Arrays.sort` is written to work with *any* object that implements `Comparable`. It does not know about `Student` specifically. The method signature is effectively: ``` Arrays.sort(Comparable[] arr) ``` This means one sorting algorithm works for `Student[]`, `String[]`, `Integer[]`, and any future class that implements `Comparable`. That is the power of interfaces — they allow **polymorphism**, where the same code operates on different types through a shared contract. ``` Arrays.sort(Comparable[] arr) │ ▼ ┌─────────────────────┐ │ Comparable │ │ int compareTo(T) │ └────┬────┬────┬───────┘ │ │ │ String Integer Student ``` Java's built-in classes already implement `Comparable`: | Class | Natural Ordering | |-------|-----------------| | `String` | Alphabetical (lexicographic) | | `Integer` | Numerical (ascending) | | `Double` | Numerical (ascending) | That is why `Arrays.sort(String[])` and `Arrays.sort(Integer[])` "just work" — those classes already fulfill the `Comparable` contract.
Check Your Understanding
Why does Arrays.sort work with both String[] and Student[]?</p>
AArrays.sort has separate overloads for every class
BBoth String and Student implement Comparable, and Arrays.sort works with any Comparable
CJava automatically generates compareTo for all classes
DArrays.sort uses == to compare objects
Arrays.sort is written against the Comparable interface, not against specific classes. Any class that implements Comparable can be sorted by Arrays.sort. This is polymorphism — one method, many types, united by a shared contract.
--- ## Multiple Sort Criteria What if two students have the same GPA and you want to break the tie alphabetically by name? Chain comparisons: check the primary criterion first, and only check the secondary criterion if the primary returns zero (a tie). ```java @Override public int compareTo(final Student other) { // Primary: GPA descending int result = Double.compare(other.gpa, this.gpa); if (result != 0) { return result; } // Tiebreaker: name ascending return this.name.compareTo(other.name); } ``` The pattern is always the same: compare the most important field first. If it is a tie (`result == 0`), compare the next field. You can chain as many tiebreakers as you need. ```java Student[] roster = { new Student("Charlie", 3.5, 1003), new Student("Alice", 3.5, 1001), // same GPA as Charlie new Student("Bob", 3.8, 1002) }; Arrays.sort(roster); for (Student s : roster) { System.out.println(s); } // Output: // Bob (GPA: 3.8) // Alice (GPA: 3.5) ← same GPA, but "Alice" < "Charlie" alphabetically // Charlie (GPA: 3.5) ``` --- ## Consistency with equals There is one important rule: if two objects are equal according to `equals()`, they should return zero from `compareTo()`. This is called being **consistent with equals**. ```java Student s1 = new Student("Alice", 3.8, 1001); Student s2 = new Student("Alice", 3.8, 1001); s1.equals(s2); // true (same ID from Lesson 4.4) s1.compareTo(s2); // should be 0 ``` If `equals` says two objects are the same, but `compareTo` says one comes before the other, collections like `TreeSet` will behave unpredictably. A `TreeSet` uses `compareTo` for ordering, so inconsistency means an "equal" object might not be found in the set. > **Key Insight:** Design `equals` and `compareTo` together. If `equals` compares by ID, make sure `compareTo` returns zero when IDs match. The simplest way is to include the identity field (like `id`) as the final tiebreaker in your compare chain.
Check Your Understanding
If s1.equals(s2) returns true, what should s1.compareTo(s2) return?</p>
AA negative number
BZero
CA positive number
DIt does not matter
For consistency with equals, if two objects are equal, compareTo must return zero. If this contract is violated, sorted collections like TreeSet and TreeMap may lose data or behave unpredictably. Always design equals and compareTo together.
--- ## Complete Example: Sortable Student Roster Putting it all together — a complete program that creates students, sorts them, and prints the roster: ```java import java.util.Arrays; public class RosterDemo { public static void main(final String[] args) { Student[] roster = { new Student("Diana", 3.9, 1004), new Student("Bob", 3.2, 1002), new Student("Alice", 3.5, 1001), new Student("Charlie", 3.5, 1003) }; System.out.println("Before sorting:"); for (Student s : roster) { System.out.println(" " + s); } Arrays.sort(roster); System.out.println("\nAfter sorting (by GPA desc, then name asc):"); for (Student s : roster) { System.out.println(" " + s); } } } ``` Output: ``` Before sorting: Diana (GPA: 3.9) Bob (GPA: 3.2) Alice (GPA: 3.5) Charlie (GPA: 3.5) After sorting (by GPA desc, then name asc): Diana (GPA: 3.9) Alice (GPA: 3.5) Charlie (GPA: 3.5) Bob (GPA: 3.2) ``` Diana has the highest GPA and sorts first. Alice and Charlie share a 3.5 GPA, so the tiebreaker sorts them alphabetically. Bob has the lowest GPA and sorts last. --- ## Quick Reference | Concept | Syntax | |---------|--------| | Declare interface | `public interface Name { returnType method(params); }` | | Implement interface | `public class X implements Name { ... }` | | Implement Comparable | `public class X implements Comparable { ... }` | | compareTo contract | Negative = before, zero = equal, positive = after | | Compare doubles | `Double.compare(a, b)` — never cast to `int` | | Compare strings | `this.name.compareTo(other.name)` — delegates to String's compareTo | | Compare ints safely | `Integer.compare(a, b)` | | Sort an array | `Arrays.sort(arr)` — requires elements to implement `Comparable` | --- ## Summary An interface is a contract that declares what methods a class must provide, without specifying how they work. The `Comparable` interface requires a single method — `compareTo(T other)` — that returns a negative number, zero, or a positive number to define ordering. Implementing `Comparable` on your class enables `Arrays.sort` to sort arrays of your objects. You choose the natural ordering: by GPA, by name, by ID, or any combination using chained comparisons. Use `Double.compare` for doubles and `Integer.compare` for ints to avoid truncation and overflow bugs. Keep `compareTo` consistent with `equals`: if two objects are equal, `compareTo` must return zero. Design both methods together, and include the identity field as a final tiebreaker when chaining multiple criteria. > **Big Picture:** Interfaces are the foundation of flexible, reusable design in Java. In CSCD 211, you will learn about inheritance and polymorphism, which build directly on what you learned here. In CSCD 212, you will use interfaces to design systems where components can be swapped without changing the code that uses them. Understanding `Comparable` now is your first step into that world. **Next lesson:** Sorting objects — selection sort and insertion sort applied to `Comparable` arrays.