Comparable
Table of contents
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.
- Using two fields:
dollars
andcents
. - 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);
System.out
evaluates to an instance of Java’sPrintStream
class that has aprintln
method.- Control is transferred to the
println
method in order to print out the balance. - 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
, thenthis.compareTo(other) < 0
. - If
this == other
, thenthis.compareTo(other) == 0
. - If
this > other
, thenthis.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
, thenthis.compareTo(other) < 0
. - If
this.totalCents == other.totalCents
, thenthis.compareTo(other) == 0
. - If
this.totalCents > other.totalCents
, thenthis.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.
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 ↩
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;
}
}