Link Search Menu Expand Document

Comparable

Table of contents

  1. Client vs implementer
  2. Implementing comparison
  3. Interfaces
  4. Internal correctness
  5. Encapsulation

Client vs implementer

Any Java programmer can define their own data types by writing Java classes. Earlier, we implemented a Balance class that represented a currency amount in dollars and cents. We discussed two ways of implementing the Balance class.

  1. Using two fields: dollars and cents.
  2. Or using only one field: just totalCents.

But computer programs, such as the Balance class, do not exist in isolation. They’re part of a larger ecosystem of programs that connect and communicate with each other to get things done. It turns out that this idea is a powerful example of abstraction: our Balance class defines public methods so that other programs written by anyone, anywhere, at anytime before or after it can rely on its functionality as long as they know how to interface with it.

Let’s understand this by asking a question: How exactly does Java print out a $1.50 balance?

Balance balance = new Balance(1, 50);
System.out.println(balance);
  1. System.out evaluates to an instance of Java’s PrintStream class that has a println method.
  2. Control is transferred to the println method in order to print out the balance.
  3. But in 1996, Sun (the original developers behind Java 1.0) could not have known exactly how we would define our Balance class. One field, two fields, or something completely different?

Sun chose to resolve this design challenge by having the println method call the balance’s toString method. By including a toString public method in the Balance class, any program written by anyone, anywhere, at anytime can get a string representation of a balance.

In this example, we consider Sun the client of our toString method. We (the programmer for the Balance class) are the implementer of the toString method.

Client
The requester of some method or service.
Implementer
The provider of some method or service.

Implementing comparison

In Autocomplete, we will implement the Term data type, which represents a possible autocompletion term. Similar to how the println method (client) depends on toString, the autocomplete algorithm (client) depends on functionality to compare any two autocompletion terms in the form of a compareTo public method.

The compareTo method takes another instance of the same class and returns an int representing the comparison relationship between the current and other instance.

  • If this < other, then this.compareTo(other) < 0.
  • If this == other, then this.compareTo(other) == 0.
  • If this > other, then this.compareTo(other) > 0.

For example, in the Balance class, we can define a compareTo method to compare the current balance’s value against another balance. (Rest of the class is omitted.)

public class Balance {
    private int totalCents;

    public int compareTo(Balance other) {
        return this.totalCents - other.totalCents;
    }
}

This way, we define larger balances as “greater than” smaller balances.

  • If this.totalCents < other.totalCents, then this.compareTo(other) < 0.
  • If this.totalCents == other.totalCents, then this.compareTo(other) == 0.
  • If this.totalCents > other.totalCents, then this.compareTo(other) > 0.

Sun anticipated programmers would need to write classes that implemented Comparable, so they provided helper methods for many commonly-used data types including int, double, and String. For objects such as String, we can be the client of the String class’s compareTo method by calling compareTo ourselves.

However, we can’t call this.totalCents.compareTo(other.totalCents) directly since totalCents is an int primitive type, not an object. Instead, the Integer and Double classes provide a special static method compare that returns the compareTo result between two numbers, x and y.

public class Balance {
    private int totalCents;

    public int compareTo(Balance other) {
        return Integer.compare(this.totalCents, other.totalCents);
    }
}
Describe a change that would make smaller balances "greater than" larger balances.

Negate the result before returning it, or subtract this.totalCents from other.totalCents.

public int compareTo(Balance other) {
    return -Integer.compare(this.totalCents, other.totalCents);
    // return other.totalCents - this.totalCents;
}

We prefer Integer.compare and Double.compare instead of subtracting numbers directly because of a subtlety when returning the difference of two double values as an int. Java truncuates double values when converting to int. For example, suppose we defined balances in double totalDollars rather than int totalCents. If we take a $1.75 balance and compare it with a $1.00 balance, the difference of $0.75 is truncated down to 0 so the two balances would be considered the same!

Interfaces

We’ve just implemented the compareTo method, but it turns out that this isn’t enough to implement a comparison relationship! One key difference between toString and compareTo is that every class contains a toString method (the default returns a gibberish string), but not every class necessarily contains a compareTo method.

The goal of compiling a Java program is to guarantee that it can run. The Java compiler checks to make sure all of the programs in the ecosystem can work together, but it can’t maintain that guarantee across all data types since compareTo is not automatically provided until a programmer writes the method.

To communicate the fact that we’ve written a valid and complete compareTo method, the class header must also implements Comparable<...> where ... is replaced with the same type.

public class Balance implements Comparable<Balance> {
    private int totalCents;

    public int compareTo(Balance other) {
        return Integer.compare(this.totalCents, other.totalCents);
    }
}

We read the class header as, “A public class Balance that can be compared against other instances of Balance.” The class is now comparable: it can be compared against other instances of the same type.

Comparable
The interface that requires a single method, compareTo. It accepts a generic type that represents the class of the other object to be compared against.

Internal correctness

A computer language is not just a way of getting a computer to perform operations but rather […] a novel formal medium for expressing ideas about methodology. Thus, programs must be written for people to read, and only incidentally for machines to execute.1

In this course, we will primarily study two measures of correctness as it relates to code quality.

External correctness
A program where the desired outcome is correctly produced by a computer.

However, external correctness is not the only criteria for determining the quality of a program.

There are numerous ways a software project can fail: projects can be over budget, they can ship late, they can fail to be useful, or they can simply not be useful enough. Evidence clearly shows that success is highly contextual and stakeholder-dependent: success might be financial, social, physical and even emotional, suggesting that software engineering success is a multifaceted variable that cannot explained simply by user satisfaction, profitability or meeting requirements, budgets and schedules.2

Whereas [external correctness criteria] are concerned with how software behaves technically according to specifications, some qualities concern properties of how developers interact with code:2

  • Verifiability is the effort required to verify that software does what it is intended to do. For example, it is hard to verify a safety critical system without either proving it correct or testing it in a safety-critical context (which isn’t safe). Take driverless cars, for example: for Google to test their software, they’ve had to set up thousands of paid drivers to monitor and report problems on the road. In contrast, verifying that a simple static HTML web page works correctly is as simple as opening it in a browser.
  • Maintainability is the effort required to correct, adapt, or perfect software. This depends mostly on how comprehensible and modular an implementation is.
  • Reusability is the effort required to use a program’s components for purposes other than those for which it was originally designed. APIs are reusable by definition, whereas black box embedded software (like the software built into a car’s traction systems) is not.

Together, we call these criteria for how developers interact with code, “internal correctness.”

Internal correctness
A program where the desired outcome is easily understood by other human programmers.

Writing high-quality programs takes deliberate practice, so these guidelines are not meant to be memorized at first. Instead, we’ll learn how to write high-quality programs through a process called code review, the practice of reviewing code with an eye towards code quality.

Documenting code

The goal of documenting code is to describe what happens without going into detail about how.

For example, say we added a nextDay method to the CalendarDate class that returns a new CalendarDate for the following day. In order to work as expected, the implementation of the method might increment the month if the current day is already the last day of the month (which varies across different months). Or, it might increment the year if the current date is New Year’s Eve.

But these implementation details aren’t relevant to the client of the CalendarDate class. We document code by adding inline code comments. Ideally, a client should only have to read the method comment (a line preceded by //) and the method signature (return type, name, parameters) to know how to use a method.

public class CalendarDate {
    private int year;
    private int month;
    private int day;

    // Returns a new CalendarDate representing the next day, wrapping month or year as necessary.
    public CalendarDate nextDay() {
        ...
    }
}

Every method needs a descriptive comment about its behavior without implementation details. Comments should describe the behavior of a method (what it does) without its implementation (how it does it). If someone is really interested in seeing exactly how a method works, they can read the code.

  1. Harold Abelson, Gerald Jay Sussman, Julie Sussman. 1984. “Preface to the First Edition.” Structure and Interpretation of Computer Programs. https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book-Z-H-7.html#%_chap_Temp_4 

  2. Amy J. Ko. 2020. “Quality.” Cooperative Software Development. http://faculty.washington.edu/ajko/books/cooperative-software-development/#/quality  2

Encapsulation

One important design decision about the CalendarDate class is the use of private fields. By declaring fields private, they allow access and modification only by code defined inside the implementing class. This helps to ensure external correctness by preventing clients from unexpectedly modifying important values.

public class CalendarDate {
    private int year;
    private int month;
    private int day;
}
public class Calendar {
    public static void main(String[] args) {
        CalendarDate welcome = new CalendarDate(2020, 9, 30);
        // private access prevents the following line of code from compiling
        welcome.day = 31;
    }
}

In programming, this idea of preventing unexpected changes to the fields of an object is encapsulation. However, the private keyword prevents both access and modification, so it prevents clients from accessing (reading) the value of the current day.

Getters and setters

To selectively allow access to fields, implementers can add getter methods that return copies of fields.

public class CalendarDate {
    private int year;
    private int month;
    private int day;

    public int year() {
        return year;
    }

    public int month() {
        return month;
    }

    public int day() {
        return day;
    }
}

This allows clients to access the current day without allowing unintended modification.

CalendarDate welcome = new CalendarDate(2020, 9, 30);
int wednesday = welcome.day();
wednesday = 31;
Why doesn't reassigning wednesday change the welcome object?

int wednesday is a local variable assigned a copy of the number representing the welcome day. Changes to the local variable won’t affect the encapsulated welcome object.

To selectively allow modification of fields, implementers can add setter methods.

public class CalendarDate {
    private int year;
    private int month;
    private int day;

    public void setYear(int year) {
        this.year = year;
    }
}

Since getters and setters are methods, they can include extra code to validate and check any of the given parameters.

public class CalendarDate {
    private int year;
    private int month;
    private int day;

    public void setDate(int year, int month, int day) {
        // Check that the date is valid!
        validateDate(year, month, day);
        this.year = year;
        this.month = month;
        this.day = day;
    }
}