Module 4: Comparators and Using sort()

Module Overview

Learn about Comparators for flexible object sorting and how to use Java's sorting functionality.

Learning Objectives

  • Implement a Comparator to establish custom ordering criteria
  • Use Comparators with Collections.sort() and Arrays.sort()
  • Create multiple ordering strategies using different Comparators
  • Understand the difference between Comparable and Comparator
  • Leverage lambda expressions and method references for creating Comparators
  • Chain multiple Comparators for complex sorting logic

Understanding Comparators

What is a Comparator?

A Comparator is a functional interface that allows you to define custom comparison strategies separate from the objects being compared. Unlike Comparable (which provides a class's natural ordering), Comparators enable multiple different ordering criteria for the same class.

The interface contains a primary method:

public interface Comparator<T> {
    int compare(T o1, T o2);
    // Optional methods...
}

How compare() Works

The compare method compares two objects and returns:

  • Negative integer: if o1 should come before o2
  • Zero: if o1 and o2 are considered equal for sorting purposes
  • Positive integer: if o1 should come after o2

Basic Comparator Implementation

Here's an example of a Comparator that sorts Person objects by name:

import java.util.Comparator;

public class PersonNameComparator implements Comparator {
    @Override
    public int compare(Person p1, Person p2) {
        return p1.getName().compareTo(p2.getName());
    }
}

Using Comparators

With Collections.sort()

List people = new ArrayList<>();
people.add(new Person("Charlie", 35));
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 25));

// Sort by name using a Comparator
Collections.sort(people, new PersonNameComparator());

// Using lambda expression (Java 8+)
Collections.sort(people, (p1, p2) -> p1.getName().compareTo(p2.getName()));

With Arrays.sort()

Person[] peopleArray = new Person[3];
peopleArray[0] = new Person("Charlie", 35);
peopleArray[1] = new Person("Alice", 30);
peopleArray[2] = new Person("Bob", 25);

// Sort array using a Comparator
Arrays.sort(peopleArray, new PersonNameComparator());

With TreeSet and TreeMap

// Create a TreeSet with a custom comparator
TreeSet sortedByName = new TreeSet<>(new PersonNameComparator());
sortedByName.add(new Person("Charlie", 35));
sortedByName.add(new Person("Alice", 30));
sortedByName.add(new Person("Bob", 25));

Modern Comparator Techniques (Java 8+)

Lambda Expressions

You can use lambda expressions to create concise Comparators:

// Sort by age
Comparator byAge = (p1, p2) -> Integer.compare(p1.getAge(), p2.getAge());

// Sort by name
Comparator byName = (p1, p2) -> p1.getName().compareTo(p2.getName());

Method References

Even more concise with method references:

// Sort by name using method reference
Comparator byName = Comparator.comparing(Person::getName);

Comparator Utility Methods

The Comparator interface provides several utility methods:

// Sort by age in ascending order
Comparator byAge = Comparator.comparing(Person::getAge);

// Sort by age in descending order
Comparator byAgeReversed = Comparator.comparing(Person::getAge).reversed();

// Sort by age, then by name (multiple sort criteria)
Comparator byAgeAndThenName = Comparator
    .comparing(Person::getAge)
    .thenComparing(Person::getName);

Handling null Values

When null values might be involved:

// Handle null Person objects
Comparator nullSafeComparator = Comparator.nullsFirst(byAge);

// Handle null names within Person objects
Comparator byNullSafeName = Comparator.comparing(
    Person::getName, 
    Comparator.nullsFirst(String::compareTo)
);

Comparable vs. Comparator

Comparable Comparator
Implemented by the class being compared External to the class being compared
Provides a single, natural ordering Provides multiple, custom ordering strategies
compareTo() method compare() method
Used without an additional argument: Collections.sort(list) Requires the comparator as an argument: Collections.sort(list, comparator)
Cannot change ordering without modifying the class Can create new ordering strategies without modifying the class

When to Use Each

  • Use Comparable: When there's a clear, default way to order objects of a class
  • Use Comparator: When you need multiple ways to order objects, or when you can't modify the class

Best Practices for Using Comparators

  • Make comparators consistent with equals when possible
  • Use static final instances for common comparators
  • Consider using Comparator methods (comparing, thenComparing) for cleaner code
  • Handle null values appropriately with nullsFirst or nullsLast
  • Avoid excessive object creation with lambda expressions
  • Document the ordering criteria implemented by each comparator

Example of Static Final Comparators

public class PersonComparators {
    // Common comparators as static constants
    public static final Comparator BY_NAME = 
        Comparator.comparing(Person::getName);
        
    public static final Comparator BY_AGE = 
        Comparator.comparing(Person::getAge);
        
    public static final Comparator BY_AGE_THEN_NAME = 
        BY_AGE.thenComparing(BY_NAME);
        
    // Prevent instantiation
    private PersonComparators() {}
}

Guided Projects

Mastery Task 5: Devil in the Details

Mastery Task Guidelines

Mastery Tasks are opportunities to test your knowledge and understanding through code. When a mastery task is shown in a module, it means that we've covered all the concepts that you need to complete that task. You will want to make sure you finish them by the end of the week to stay on track and complete the unit.

Each mastery task must pass 100% of the automated tests and code styling checks to pass each unit. Your code must be your own. If you have any questions, feel free to reach out for support.

The Missing Promise CLI displays promises for the first item in an order. That's not the whole story, though: Amazon often breaks orders into multiple shipments with different delivery speeds, or separates Amazon items from the items fulfilled by different vendors. Therefore, each item has a delivery promise.

You will modify the tool to show each item's promises. Then you'll sort the promises associated with the order to make it easier for CS reps to use the tool.

Milestone 1

Modify the code so that it returns the promise(s) for each item in an order (The output should be a return statement and NOT a System.out.println()). Write unit tests covering the different cases that could occur in order promises. Make sure all the existing unit tests still pass. Run the tool with order id 900-3746403-0000002 to view an order with many different promises.

Milestone 2

Customers often ask our CS representatives about specific products that they're worried about delivery for. Our CS representatives have asked that the items in the promise history be listed by ASIN (alphabetically, from 'A' to 'Z') to make this lookup more efficient for orders with more than one item.

Modify the code so that the item promises are sorted alphabetically by their ASINs, ascending ('A' to 'Z'). Think about these questions as you come up with your recommendation:

  • Can we use sort() to accomplish this?
  • Should we use a Comparable<T> to accomplish this? Is there a natural ordering here?
  • Or should we use a Comparator<T>? Is this just one ordering that might make sense, among others?

Hint: CS representatives also suggest that it might be helpful to sort by promise dates in the future... How would you extend your design in the future?

After consulting with your Sr Engineer, you both agree that ASIN isn't a natural ordering for Promises, there are many other orderings that make sense. Following this line of thinking, create a PromiseAsinComparator in a new package, com.amazon.ata.deliveringonourpromise.comparators.

Now that you've decided to use a Comparator, what will be required?

  • What interface (exactly, including generic parameter) will your class need to implement?
  • What type should T be?
  • What method (and argument types) do we need to implement to satisfy the interface?
  • What will the comparison method logic look like? Is it comparing a single attribute from each argument? You can probably use some existing String-comparing logic somewhere....

Write a unit test that ensures that the PromiseHistory returned by getPromiseHistoryByOrderId() contains Promises sorted by ASIN in ascending order.

Run each test as follows. They should all pass

Each test should be run separately using the following commands - one command per test:

./gradlew -q clean :test

./gradlew -q clean IRT

./gradlew -q clean MasteryTaskFiveTests

Nicely done!

Now go back to the README to see what few remaining tasks you have to be done-done-done with the project

Resources