Implementing compareTo

The compareTo method is the sole member of the Comparable interface, and is not a member of Object. However, it's quite similar in nature to equals and hashCode. It provides a means of fully ordering objects.

Implementing Comparable allows:

The compareTo method needs to satisfy the following conditions. These conditions have the goal of allowing objects to be fully sorted, much like the sorting of a database result set on all fields. One can greatly increase the performance of compareTo by comparing first on items which are most likely to differ.

When a class extends a concrete Comparable class and adds a significant field, a correct implementation of compareTo cannot be constructed. The only alternative is to use composition instead of inheritance. (See Effective Java for more information.)

Compare the various types of fields as follows:

If the task is to perform a sort of items which are stored in a relational database, then it is usually much preferred to let the database perform the sort using the ORDER BY clause, rather than in code.

An alternative to implementing Comparable is passing Comparator objects as parameters. Be aware that if a Comparator compares only one of several significant fields, then the Comparator is very likely not synchronized with equals.

All primitive wrapper classes implement Comparable. Note that Boolean did not implement Comparable until version 1.5, however.

Example 1

import static java.util.Comparator.comparing;
import static java.util.Comparator.nullsLast;
import static java.util.Comparator.naturalOrder;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;

/** @since Java 8. */
public final class ArtGalleryVisit implements Comparable<ArtGalleryVisit> {

  enum Rating {
    POOR, FAIR, GOOD, POSSIBLY_SUBLIME_BUT_I_NEED_TO_THINK_ABOUT_IT;
  }

  /**
   Some implementations might throw a checked exception from this sort of 
   constructor, to carry messages about problems with the data.
   @param rating the only nullable field 
  */
  public ArtGalleryVisit(
    String galleryName, BigDecimal admissionPrice, LocalDate date, Rating rating
  ) {
    this.galleryName = galleryName;
    this.admissionPrice = admissionPrice;
    this.date = date;
    this.rating = rating;
    validate();
  }

  @Override public boolean equals(Object aThat) {
    //unusual: multiple return statements
    if (this == aThat) return true;
    if (!(aThat instanceof ArtGalleryVisit)) return false;
    ArtGalleryVisit that = (ArtGalleryVisit)aThat;
    for(int i = 0; i < this.getSigFields().length; ++i){
      if (!Objects.equals(this.getSigFields()[i], that.getSigFields()[i])){
        return false; 
      }
    }
    return true;
  }
  
  @Override public int hashCode() {
    return Objects.hash(getSigFields());
  }

  @Override public int compareTo(ArtGalleryVisit that) {
    int result = COMPARATOR.compare(this, that);
    //optional: you may want to include this assertion (at least during development)
    //note that assertions are disabled by default
    if (result == 0) {
      assert this.equals(that) : 
        this.getClass().getSimpleName() + ": compareTo inconsistent with equals."
      ;
    }
    return result;
  }  
  
  public String getGalleryName() { return galleryName; }
  public BigDecimal getAdmissionPrice() { return admissionPrice; }
  public LocalDate getDate() { return date; }
  
  /** Returns an Optional because this field is nullable. */
  public Optional<Rating> getRating() { 
    return Optional.ofNullable(rating); 
  }
  
  // PRIVATE
  
  private String galleryName;
  private BigDecimal admissionPrice;
  private LocalDate date;
  private Rating rating; //nullable
  
  /** 
   To avoid creating an object each time a comparison is done, you should 
   usually store it in a static private field.
  */
  private Comparator<ArtGalleryVisit> COMPARATOR = getComparator();
  
  /** Be consistent with equals: use the same fields as getSigFields().*/
  private static Comparator<ArtGalleryVisit> getComparator(){
    Comparator<ArtGalleryVisit> result = 
      comparing(ArtGalleryVisit::getDate)
      .thenComparing(ArtGalleryVisit::getGalleryName)
      .thenComparing(ArtGalleryVisit::getAdmissionPrice)
      //nullable: no method exists, so use a lambda
      .thenComparing(t -> t.rating, nullsLast(naturalOrder())) 
    ;
    return result;
  }
  
  /** 
   The equals, hashCode and compareTo methods will usually reference
   the same fields, in the same order.
  */
  private Object[] getSigFields() {
    return new Object[] {
      //start with the data that's most likely to differ  
      date, galleryName, admissionPrice, rating
    };
  }  
  
  private void validate() {
    //..elided: check for null, range, and so on
    //some implementations might throw a checked exception from this method 
  }
}
 

Example 2

import java.util.*;

/** @since Java 7 */
public final class Account implements Comparable<Account> {
  
  enum AccountType {CASH, MARGIN, RRSP};

   public Account (
      String firstName,
      String lastName,
      int accountNumber,
      int balance,
      boolean isNewAccount,
      AccountType accountType
  ) {
      //..detailed parameter validations elided
      this.firstName = Objects.requireNonNull(firstName);
      this.lastName = Objects.requireNonNull(lastName);
      this.accountNumber = accountNumber;
      this.balance = balance;
      this.isNewAccount = isNewAccount;
      this.accountType = accountType;
   }

  /**
  * @param that is a non-null Account.
  */
  @Override public int compareTo(Account that) {
    final int BEFORE = -1;
    final int EQUAL = 0;
    final int AFTER = 1;

    //this optimization is usually worthwhile, and can
    //always be added
    if (this == that) return EQUAL;

    //primitive numbers follow this form
    if (this.accountNumber < that.accountNumber) return BEFORE;
    if (this.accountNumber > that.accountNumber) return AFTER;

    //booleans follow this form
    if (!this.isNewAccount && that.isNewAccount) return BEFORE;
    if (this.isNewAccount && !that.isNewAccount) return AFTER;

    //objects, including type-safe enums, follow this form
    //note that null objects will throw an exception here
    int comparison = this.accountType.compareTo(that.accountType);
    if (comparison != EQUAL) return comparison;

    comparison = this.lastName.compareTo(that.lastName);
    if (comparison != EQUAL) return comparison;

    comparison = this.firstName.compareTo(that.firstName);
    if (comparison != EQUAL) return comparison;

    if (this.balance < that.balance) return BEFORE;
    if (this.balance > that.balance) return AFTER;

    //all comparisons have yielded equality
    //verify that compareTo is consistent with equals (optional)
    assert this.equals(that) : "compareTo inconsistent with equals.";

    return EQUAL;
  }

   /**
   * Define equality of state.
   */
   @Override public boolean equals(Object aThat) {
     if (this == aThat) return true;
     if (!(aThat instanceof Account)) return false;
     Account that = (Account)aThat;
     for(int i = 0; i < this.getSigFields().length; ++i){
       if (!Objects.equals(this.getSigFields()[i], that.getSigFields()[i])){
         return false;
       }
     }
     return true;     
   }

   /**
   * A class that overrides equals must also override hashCode.
   */
   @Override public int hashCode() {
     return Objects.hash(getSigFields());     
   }

   //PRIVATE

   private String firstName; //non-null
   private String lastName;  //non-null
   /** These are not Integer, Boolean here, in order to exercise different cases:*/
   private int accountNumber;
   private int balance;
   private boolean isNewAccount;

   /**
   * Type of the account, expressed as a type-safe enumeration (non-null).
   */
   private AccountType accountType;
   
   private Object[] getSigFields() {
     Object[] result = {
      accountNumber, firstName, lastName, balance, isNewAccount, accountType
     };
     return result;     
   }

   /** Exercise compareTo.  */
   public static void main (String[] aArguments) {
     //Note the difference in behaviour in equals and compareTo, for nulls:
     String text = "blah";
     Integer number = 10;
     //x.equals(null) always returns false:
     log("false: " + text.equals(null));
     log("false: " + number.equals(null) );
     //x.compareTo(null) always throws NullPointerException:
     //System.out.println( text.compareTo(null) );
     //System.out.println( number.compareTo(null) );

     Account flaubert = new Account(
      "Gustave", "Flaubert", 1003, 0, true, AccountType.MARGIN
     );

     //all of these other versions of "flaubert" differ from the
     //original in only one field
     Account flaubert2 = new Account(
       "Guy", "Flaubert", 1003, 0, true, AccountType.MARGIN
     );
     Account flaubert3 = new Account(
       "Gustave", "de Maupassant", 1003, 0, true, AccountType.MARGIN
     );
     Account flaubert4 = new Account(
       "Gustave", "Flaubert", 2004, 0, true, AccountType.MARGIN
     );
     Account flaubert5 = new Account(
       "Gustave", "Flaubert", 1003, 1, true, AccountType.MARGIN
     );
     Account flaubert6 = new Account(
       "Gustave", "Flaubert", 1003, 0, false, AccountType.MARGIN
     );
     Account flaubert7 = new Account(
       "Gustave", "Flaubert", 1003, 0, true, AccountType.CASH
     );

     log( "0: " +  flaubert.compareTo(flaubert) );
     log( "first name +: " +  flaubert2.compareTo(flaubert) );
     //Note capital letters precede small letters
     log( "last name +: " +  flaubert3.compareTo(flaubert) );
     log( "acct number +: " +  flaubert4.compareTo(flaubert) );
     log( "balance +: " +  flaubert5.compareTo(flaubert) );
     log( "is new -: " +  flaubert6.compareTo(flaubert) );
     log( "account type -: " +  flaubert7.compareTo(flaubert) );
   }
   
   private static void log(String text) {
     System.out.println(text);
   }
   
} 

A sample run of this class gives:
>java -cp . Account
false: false
false: false
0: 0
first name +: 6
last name +: 30
acct number +: 1
balance +: 1
is new -: -1
account type -: -1

In the old JDK 1.4, there are two differences:

See Also :
Type Safe Enumerations
Implementing equals
Implementing hashCode
Don't perform basic SQL tasks in code
Modernize old code