Interfaces and Comparable
Contracts, type promises, and natural ordering
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
compareTomethod that returns negative, zero, or positive - Sort arrays of objects using
Arrays.sortwithComparable - Explain the relationship between
compareToandequals
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 calledComparable.
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.
If a.compareTo(b) returns a negative number, what does that mean?
Implementing Comparable on Student
To make Students sortable, the class implements Comparable<Student> and provides a compareTo method. Let’s sort by GPA in descending order (highest first):
import java.util.Arrays;
public class Student implements Comparable<Student> {
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:
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
doubledifference tointfor comparison. UseDouble.compare(a, b)for doubles andInteger.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:
@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<String> — 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<T> │
│ 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.
Why does Arrays.sort work with both String[] and Student[]?
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).
@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.
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.
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
equalsandcompareTotogether. Ifequalscompares by ID, make surecompareToreturns zero when IDs match. The simplest way is to include the identity field (likeid) as the final tiebreaker in your compare chain.
If s1.equals(s2) returns true, what should s1.compareTo(s2) return?
Complete Example: Sortable Student Roster
Putting it all together — a complete program that creates students, sorts them, and prints the roster:
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<X> { ... } |
| 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<T> 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
Comparablenow is your first step into that world.
Next lesson: Sorting objects — selection sort and insertion sort applied to Comparable arrays.