oop Lesson 7 18 min read

Sorting Objects

Collections.sort, Arrays.sort, and natural ordering

Reading: Reges & Stepp: Ch. 10

After this lesson, you will be able to:

  • Sort arrays of objects using Arrays.sort() with Comparable
  • Sort ArrayList objects using Collections.sort()
  • Implement selection sort on object arrays using compareTo
  • Sort by multiple fields with tiebreakers
  • Explain the difference between stable and unstable sorts
  • Use Comparator for alternate sort orders

You Already Know How to Sort

In Lesson 2.3, you wrote selection sort for int[]. You compared elements with <. That worked because integers have a built-in ordering: 3 is less than 7, always.

But what about Student objects? Is Alice less than Bob? By name? By GPA? By ID? There is no built-in < for objects. The compiler has no idea how to rank your custom classes.

That is exactly the problem Comparable solves. In the previous lesson, you wrote compareTo to define a natural ordering for your objects. Now you get to use it. Once a class implements Comparable<T>, Java’s sorting methods work on it automatically.

From CSCD 110: In Python, you could sort a list of objects with sorted(students, key=lambda s: s.gpa). The lambda told Python how to extract a sort key. Java’s Comparable interface serves the same purpose, but the comparison logic lives inside the class itself, not at the call site.


Sorting with Arrays.sort()

Here is a Student class that implements Comparable<Student>, sorting by GPA (descending), then by name (ascending) as a tiebreaker:

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) {
        this.name = name;
        this.gpa = gpa;
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public double getGpa() {
        return this.gpa;
    }

    public int getId() {
        return this.id;
    }

    @Override
    public int compareTo(final Student other) {
        // Primary: GPA descending (flip arguments for descending)
        int gpaResult = Double.compare(other.gpa, this.gpa);
        if (gpaResult != 0) {
            return gpaResult;
        }
        // Tiebreaker: name ascending
        return this.name.compareTo(other.name);
    }

    @Override
    public String toString() {
        return this.name + " (ID: " + this.id + ", GPA: " + this.gpa + ")";
    }
}

Now sorting an array is one line:

public class Main {
    public static void main(final String[] args) {
        Student[] roster = {
            new Student("Zoe", 3.5, 1005),
            new Student("Alice", 3.8, 1001),
            new Student("Bob", 3.8, 1002),
            new Student("Charlie", 3.2, 1003)
        };

        System.out.println("Before sort:");
        for (final Student s : roster) {
            System.out.println("  " + s);
        }

        Arrays.sort(roster);  // Uses compareTo internally

        System.out.println("\nAfter sort:");
        for (final Student s : roster) {
            System.out.println("  " + s);
        }
    }
}

Output:

Before sort:
  Zoe (ID: 1005, GPA: 3.5)
  Alice (ID: 1001, GPA: 3.8)
  Bob (ID: 1002, GPA: 3.8)
  Charlie (ID: 1003, GPA: 3.2)

After sort:
  Alice (ID: 1001, GPA: 3.8)
  Bob (ID: 1002, GPA: 3.8)
  Zoe (ID: 1005, GPA: 3.5)
  Charlie (ID: 1003, GPA: 3.2)

Alice and Bob both have 3.8 GPA, so the tiebreaker kicks in: “Alice” comes before “Bob” alphabetically. Zoe (3.5) comes next, then Charlie (3.2). Highest GPA first.

Key Insight: Arrays.sort() does not know anything about students, GPAs, or names. It calls compareTo repeatedly and uses the return values (negative, zero, positive) to decide the order. Your compareTo method is the single source of truth for ordering.


Sorting with Collections.sort()

Arrays.sort() works on arrays. For ArrayList, use Collections.sort():

import java.util.ArrayList;
import java.util.Collections;

public class RosterDemo {
    public static void main(final String[] args) {
        ArrayList<Student> roster = new ArrayList<>();
        roster.add(new Student("Zoe", 3.5, 1005));
        roster.add(new Student("Alice", 3.8, 1001));
        roster.add(new Student("Bob", 3.8, 1002));
        roster.add(new Student("Charlie", 3.2, 1003));

        Collections.sort(roster);  // Uses compareTo internally

        for (final Student s : roster) {
            System.out.println(s);
        }
    }
}

The output is identical. Both methods rely on the same compareTo method.

Method Works on Import
Arrays.sort(arr) Arrays (T[]) java.util.Arrays
Collections.sort(list) Lists (ArrayList<T>, etc.) java.util.Collections

Both require that the element type implements Comparable<T>. If it does not, the code will not compile.

Check Your Understanding

You have an ArrayList<Student> where Student implements Comparable<Student>. Which call sorts it?


How Sorting Algorithms Use compareTo

Behind the scenes, Arrays.sort() uses a sophisticated algorithm (a variant of mergesort for objects). But the principle is the same as the selection sort you wrote in Lesson 2.3. The only change: instead of comparing primitives with <, the algorithm calls compareTo.

Here is selection sort adapted for objects. Compare this to the int[] version you already know:

Sorting int[]:              Sorting Comparable[]:
─────────────               ────────────────────
if (arr[j] < arr[minIndex]) if (arr[j].compareTo(arr[minIndex]) < 0)

That is it. The algorithm structure is identical. Only the comparison changes.

Selection Sort on Objects: Complete Implementation

public class ObjectSorter {
    /**
     * Sorts an array of Comparable objects using selection sort.
     * @param arr the array to sort
     */
    public static <T extends Comparable<T>> void selectionSort(final T[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            int minIndex = i;

            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j].compareTo(arr[minIndex]) < 0) {
                    minIndex = j;
                }
            }

            // Swap
            T temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
}

The signature <T extends Comparable<T>> means: “this method accepts an array of any type T, as long as T implements Comparable<T>.” You can call it on Student[], String[], or Integer[]:

Student[] students = { /* ... */ };
ObjectSorter.selectionSort(students);

String[] words = {"zebra", "apple", "banana"};
ObjectSorter.selectionSort(words);

Big Picture: Generics (<T extends Comparable<T>>) let you write one sorting method that works for any comparable type. You will study generics more deeply in CSCD 211, but this pattern shows the payoff: write the algorithm once, reuse it everywhere.

Check Your Understanding

In the selection sort for objects, what replaces the primitive comparison arr[j] < arr[minIndex]?


Sorting by Different Fields

Your compareTo method defines one natural ordering. But what if you need different orderings in different parts of your program?

Scenario 1: Sort by One Field

@Override
public int compareTo(final Student other) {
    return this.name.compareTo(other.name);  // Alphabetical by name
}

Scenario 2: Multiple Fields with Tiebreakers

@Override
public int compareTo(final Student other) {
    int gpaResult = Double.compare(other.gpa, this.gpa);  // GPA descending
    if (gpaResult != 0) {
        return gpaResult;
    }
    return this.name.compareTo(other.name);  // Name ascending as tiebreaker
}

Scenario 3: Descending Order

The Trick: To reverse the sort order, flip the arguments to the comparison. Instead of Double.compare(this.gpa, other.gpa) (ascending), write Double.compare(other.gpa, this.gpa) (descending). Do not negate the result — negating Integer.MIN_VALUE overflows.


Stable vs. Unstable Sorts

A stable sort preserves the relative order of equal elements. An unstable sort does not guarantee this.

Suppose you sort students by GPA and two students both have 3.8:

Before sort:  [Zoe 3.8, Alice 3.8, Bob 3.2]

Stable sort:   [Zoe 3.8, Alice 3.8, Bob 3.2]  ← Zoe still before Alice
Unstable sort: [Alice 3.8, Zoe 3.8, Bob 3.2]  ← Order may change

In Java, Arrays.sort() for objects uses a stable mergesort. Arrays.sort() for primitives uses a unstable dual-pivot quicksort. Collections.sort() is always stable.

This matters when you sort by multiple criteria in stages: sort by name first, then stable-sort by GPA, and the name order is preserved within each GPA group.


Comparator: An Alternative Ordering

Comparable defines the natural ordering inside the class. But sometimes you need a different ordering without changing the class. Java provides Comparator for this.

import java.util.Arrays;
import java.util.Comparator;

public class SortByName {
    public static void main(final String[] args) {
        Student[] roster = {
            new Student("Zoe", 3.5, 1005),
            new Student("Alice", 3.8, 1001),
            new Student("Bob", 3.2, 1002)
        };

        // Sort by name (ascending), ignoring the natural GPA ordering
        Arrays.sort(roster, new Comparator<Student>() {
            @Override
            public int compare(final Student a, final Student b) {
                return a.getName().compareTo(b.getName());
            }
        });

        for (final Student s : roster) {
            System.out.println(s);
        }
    }
}

Output:

Alice (ID: 1001, GPA: 3.8)
Bob (ID: 1002, GPA: 3.2)
Zoe (ID: 1005, GPA: 3.5)

Comparator is a separate object that defines a comparison. You pass it as a second argument to Arrays.sort() or Collections.sort(). The sort method uses the Comparator instead of compareTo.

Think of it this way:

  • Comparable: “I know how to compare myself to another object of my type.” (Lives in the class.)
  • Comparator: “I know how to compare two objects of some type.” (Lives outside the class.)

You will use Comparator more in CSCD 211. For now, know it exists and that it solves the problem of needing multiple sort orders for the same class.

Check Your Understanding

A Student class sorts by GPA descending via compareTo. You need to sort a Student[] by name instead, without changing the Student class. What do you use?


Complete Example: The Roster Pipeline

Here is a complete program that creates students, sorts them, and prints a report. This is the pattern you will use in labs: create, sort, process.

import java.util.ArrayList;
import java.util.Collections;

public class RosterReport {
    public static void main(final String[] args) {
        // 1. Build the roster
        ArrayList<Student> roster = new ArrayList<>();
        roster.add(new Student("Zoe", 3.5, 1005));
        roster.add(new Student("Alice", 3.8, 1001));
        roster.add(new Student("Bob", 3.8, 1002));
        roster.add(new Student("Charlie", 3.2, 1003));
        roster.add(new Student("Diana", 3.9, 1004));

        // 2. Sort (uses compareTo: GPA desc, then name asc)
        Collections.sort(roster);

        // 3. Print sorted roster
        System.out.println("=== Student Roster (by GPA) ===");
        for (int i = 0; i < roster.size(); i++) {
            Student s = roster.get(i);
            System.out.printf("%d. %s%n", i + 1, s);
        }

        // 4. Print top 3
        System.out.println("\nTop 3 Students:");
        for (int i = 0; i < Math.min(3, roster.size()); i++) {
            Student s = roster.get(i);
            System.out.printf("  %s — GPA %.1f%n", s.getName(), s.getGpa());
        }
    }
}

Output:

=== Student Roster (by GPA) ===
1. Diana (ID: 1004, GPA: 3.9)
2. Alice (ID: 1001, GPA: 3.8)
3. Bob (ID: 1002, GPA: 3.8)
4. Zoe (ID: 1005, GPA: 3.5)
5. Charlie (ID: 1003, GPA: 3.2)

Top 3 Students:
  Diana — GPA 3.9
  Alice — GPA 3.8
  Bob — GPA 3.8

This pattern — build data, sort it, extract results — shows up constantly in real programs. Reading records from a file, sorting by relevance, and printing the top results is exactly how search engines, spreadsheets, and grade books work.


Quick Reference

Task Code
Sort array Arrays.sort(arr)
Sort ArrayList Collections.sort(list)
Sort with custom order Arrays.sort(arr, comparator)
Descending order Flip arguments in Double.compare
Tiebreaker Check primary field first, return secondary if primary is 0
Selection sort on objects Replace < with .compareTo(...) < 0

Summary

Sorting objects requires a defined ordering. The Comparable interface provides that ordering through compareTo, and Java’s built-in Arrays.sort() and Collections.sort() methods use it automatically. The selection sort algorithm you learned for primitives transfers directly to objects — the only change is replacing < with compareTo.

To sort by multiple fields, check the primary field first and fall back to a tiebreaker when the primary comparison returns zero. To reverse the order, flip the arguments to Double.compare or compareTo. For alternate orderings without modifying the class, use a Comparator.

Next lesson: Objects in Memory — references, aliasing, null, and garbage collection. You will finally see what happens inside the JVM when you write new Student(...).