1. Introduction to Java
2. Reference Types and Semantics
3. Method Specifications and Testing
4. Loop Invariants
5. Analyzing Complexity
6. Recursion
7. Sorting Algorithms
8. Classes and Encapsulation
9. Interfaces and Polymorphism
10. Inheritance
11. Additional Java Features
12. Collections and Generics
13. Linked Data
14. Iterating over Data Structures
15. Stacks and Queues
9. Interfaces and Polymorphism

9. Interfaces and Polymorphism

When we drive a car, we know that we should push down on the accelerator pedal to make the car move faster and turn the steering wheel to change its direction, but most of us lack the knowledge of the underlying mechanical systems that make this happen. When we plug an appliance into a wall outlet or use an HDMI cable to connect our computer to a projector, we can access power or transmit data without needing to worry about the intricacies of the electrical grid or the signaling protocol. These physical devices present an interface to us, an agreed upon standard for how the two sides (the driver and the car, the outlet and the appliance, the computer and the projector) should work together.

A similar idea applies in encapsulated object-oriented programs, which maintain a clear boundary between the implementer and client of a class. As the author of the class, the implementer has full knowledge of its inner workings, including how it represents its state with fields and how it interacts with this state in the definitions of each of its instance methods. The client does not need all of this information; having access to it would in many cases create more of a cognitive burden than a benefit. Instead, the client needs to know what behaviors are provided by a class so they can leverage them in their code.

This separation between what (high-level behaviors) and how (fine-grained implementation details), an abstraction barrier, is one of the key benefits of object orientation. In today’s lecture, we’ll expand on this idea, introducing a Java construct called an interface, which helps us formalize the distinction between declaring (promising) and defining (implementing) behaviors. Interfaces are perhaps the most important and powerful features in Java, and a lot of newer Java syntax and language features are built around interfaces. We will use interfaces heavily throughout the rest of the course because of their close relationship with abstract data types.

In the latter portion of today’s lecture, we’ll see that interfaces establish subtype relationships and enable polymorphism. Just as the physical driving interface (accelerator pedal, steering wheel, turn signals, etc.) can be adopted by many different automobiles (from gas to hybrid to electric cars, compact cars to large trucks), allowing a person who learns to drive one vehicle to easily transition to any other vehicle, the object-oriented principle of polymorphism allows us to leverage subtype relationships so that one piece of code can seamlessly interact with objects of many different types.

Interfaces in Java

As a running example throughout today’s lecture, let us imagine that we are designing a personal finance application that will allow users to view and manage different types of accounts (checking accounts, savings accounts, CDs, investment accounts, etc.). There are some common actions that a user should be able to take for each of these accounts:

Since the accounts can be managed by different teams at a bank (or even different banks or financial institutions), how can we be sure that all of the accounts will support these operations? One solution is to define an Account interface that all of the different account types will implement.

Definition: Interface (Java)

An interface is a Java construct that creates a new type, just as a class does. Interfaces guarantee the presence of certain behaviors (i.e., method signatures) for this type, but do not specify how they should be implemented. Unlike a class, an interface does not have fields and usually does not provide definitions for its instance methods, only signatures.

In our personal finance application, we’d like our code to interact with an Account type, which we model as an interface. Just like classes, interfaces are defined in their own file with a matching name and use similar syntax to declare their scope.

Account.java

1
2
3
4
/** Models an account in our personal finance app. */
public interface Account {

}
1
2
3
4
/** Models an account in our personal finance app. */
public interface Account {

}

Declaring Methods in an Interface

Within the interface, we will write the method signatures (names, return types, parameters, and of course specifications) for all of the behaviors that Account must provide. Rather than enclosing a method in a { ... } block delimiting the method’s scope, we’ll simply end the signature with a semicolon (;). The function of the interface is to promise the availability of a method, but not to provide its implementation details.

Remark:

The terminology around creating and implementing interfaces becomes somewhat subtle, so we stop to emphasize one important distinction.

  • When we use the word declare, this roughly means "introduce the name of". This is the word we used for a variable declaration like "int i;", a piece of code that tells the compiler that i will be an int variable within this scope but does not assign a value to i. Similarly, an interface declares many methods since it tells us the names of methods that can be accessed for its type.
  • When we use the word define, this roughly means "provides the details of". Think about a dictionary definition, that tells us exactly how the word works. An interface doesn't define methods since it is missing their implementation details. Soon, we'll see that the method definitions are provided by a class implementing the interface.

As noted in the above definition, our interface declaration will not include any fields. Let’s add some method signatures to our Account interface that support some of the desired behaviors listed above.

Account.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
 * Models an account in our personal finance app.
 */
public interface Account {
  /**
   * Return the name associated to this account.
   */
  String name();

  /**
   * Returns the total balance, in cents, of the account.
   */
  int balance();

  /**
   * Attempts to deposit the specified `amount`, in cents, to the balance of 
   * the account, and returns whether this transaction was successful. If this
   * transaction is successful, it is logged with the given `memo`. Otherwise,
   * no changes are made to this account and no transaction is logged.
   * Requires that `amount > 0`.
   */
  boolean depositFunds(int amount, String memo);

  /**
   * Attempts to transfer the specified `amount`, in cents, from this account 
   * to `receivingAccount` and returns whether this transaction was successful. 
   * If this transaction is successful, it is logged to both accounts. Otherwise, 
   * no changes are made to either account and no transaction is logged. 
   * Requires that `amount > 0`.
   */
  boolean transferFunds(Account receivingAccount, int amount);

  /**
   * Called once at the end of each month to return a `String` summarizing the 
   * account's initial balance that month, all transactions made during that 
   * month, and its final balance. 
   */
  String transactionReport();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
 * Models an account in our personal finance app.
 */
public interface Account {
  /**
   * Return the name associated to this account.
   */
  String name();

  /**
   * Returns the total balance, in cents, of the account.
   */
  int balance();

  /**
   * Attempts to deposit the specified `amount`, in cents, to the balance of 
   * the account, and returns whether this transaction was successful. If this
   * transaction is successful, it is logged with the given `memo`. Otherwise,
   * no changes are made to this account and no transaction is logged.
   * Requires that `amount > 0`.
   */
  boolean depositFunds(int amount, String memo);

  /**
   * Attempts to transfer the specified `amount`, in cents, from this account 
   * to `receivingAccount` and returns whether this transaction was successful. 
   * If this transaction is successful, it is logged to both accounts. Otherwise, 
   * no changes are made to either account and no transaction is logged. 
   * Requires that `amount > 0`.
   */
  boolean transferFunds(Account receivingAccount, int amount);

  /**
   * Called once at the end of each month to return a `String` summarizing the 
   * account's initial balance that month, all transactions made during that 
   * month, and its final balance. 
   */
  String transactionReport();
}

One can imagine expanding this interface to include other useful account features.

Remark:

Notice that we did not include visibility modifiers on any of the methods we declared in the interface. All methods in an interface must be public, so this visibility is added by default.

Remark:

In this introduction to interfaces, we are eliding many of their more technical details and corner cases, such as their interaction with static methods, the possibility of adding default method definitions, etc. We may introduce some of these as the course progresses, but for now, it is reasonable to think of interfaces as just a collection of method declarations under a new type name.

Implementing an Interface

Just from this interface, we have information that we need to interact with Accounts in client code. Let’s suppose that we have an Account variable checking that refers to a checking account with $550 and an Account variable savings that refers to a savings account with $2,300. Suppose we write the code:

1
2
3
4
checking.depositFunds(4000, "Check from John");
checking.transferFunds(savings, 10000);
System.out.println(checking.name());
System.out.println(checking.transactionReport());
1
2
3
4
checking.depositFunds(4000, "Check from John");
checking.transferFunds(savings, 10000);
System.out.println(checking.name());
System.out.println(checking.transactionReport());

Then, the Account method specifications tell us we should see a reported initial balance of $550 in the checking account, a $40 deposit, a $100 outgoing transfer, and a final balance of $490. How can this code actually work? Where are the method definitions? Even before this, how can we construct these “Account objects” when the interface provides no information about an Account’s fields? The answer to these questions is that interfaces cannot be instantiated directly. Instead, a class provides the definition of an interface’s methods, and we use an instance of this class to realize the interface. For example, we can define a CheckingAccount class to model checking accounts, with the following syntax.

CheckingAccount.java

1
2
3
4
/** Models a checking account in our personal finance app. */
public class CheckingAccount implements Account {

}
1
2
3
4
/** Models a checking account in our personal finance app. */
public class CheckingAccount implements Account {

}

Notice the use of the new keyword implements, in the clause “implements Account” of this class declaration. This clause tells us that the CheckingAccount class guarantees to provide definitions for all of the methods promised in the Account interface. If you write this class definition in IntelliJ, you will see an error appear with a suggestion to add these method signatures. In this way, the implements clause is a powerful piece of the class’ specifications; it assures the existence of a set of behaviors within the implementing class (that all satisfy the specifications outlined in the interface).

Aside from the required inclusion of certain methods, we’ll define a class implementing an interface as usual. First, we’ll decide on its state representation (and class invariant), and then we’ll add method definitions to model the behaviors that the class supports. In the case of our CheckingAccount, we’ll have fields for the account’s name (a String), its current balance (an int), and a StringBuilder object, transactions, that stores a log of its transactions.

Remark:

Our modeling of a checking account is far from ideal. At this point in the course, we have not yet introduced data structures that we'd use in a more principled design. For example, storing transactions in a StringBuilder doesn't give us an easy way to locate a particular transaction, categorize or filter transactions by date, etc. Data structures provide these convenient behaviors. The purpose of this code example is less about the design decisions of our personal finance application. Rather, it is meant to illustrate the syntax for using interfaces and motivate the principles of polymorphic design that we will discuss later in the lecture.

Our completed definition of the CheckingAccount class is given below.

CheckingAccount.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/** Models a checking account in our personal finance app. */
public class CheckingAccount implements Account {
  /** The name of this account. */
  private String name;

  /** The current balance, in cents, of this account. */
  private int balance;

  /** The current month's transactions. */
  private StringBuilder transactions;

  /**
   * Constructs a checking account with given `name` and initial `balance`.
   */
  public CheckingAccount(String name, int balance) {
    this.name = name;
    this.balance = balance;
    this.resetTransactionLog();
  }

  /**
   * Reassigns the transaction log to a new `StringBuilder` object that 
   * contains this month's initial account balance
   */
  private void resetTransactionLog() {
    this.transactions = new StringBuilder("Initial Balance: ");
    this.transactions.append(centsToString(this.balance));
    this.transactions.append("\n"); // newline
  }

  /**
   * Converts the given number of `cents` into a String in "$X.XX" format.
   */
  private static String centsToString(int cents) {
    int dollars = cents / 100;
    cents = cents % 100;
    return ("$" + dollars + "." + (cents < 10 ? "0" : "") + cents);
  }

  @Override
  public String name() {
    return this.name;
  }

  @Override
  public int balance() {
    return this.balance;
  }

  /**
   * Deposits the specified `amount`, in cents, to the balance of the account, 
   * logs this transaction with the given `memo`, and returns `true` to indicate 
   * that this deposit was successful. Requires that `amount > 0`.
   */
  @Override
  public boolean depositFunds(int amount, String memo) {
    assert amount > 0;
    this.balance += amount;
    this.transactions.append(" - Deposit ");
    this.transactions.append(centsToString(amount));
    this.transactions.append(": ");
    this.transactions.append(memo);
    this.transactions.append("\n");
    return true; // we can always add funds to a checking account
  }

  @Override
  public boolean transferFunds(Account receivingAccount, int amount) {
    assert amount > 0;
    if (amount > this.balance) {
      return false; // insufficient funds
    }
    if (!receivingAccount.depositFunds(amount, "Transfer from " + this.name)) {
      return false; // could not add funds
    }
    this.balance -= amount;
    this.transactions.append(" - Transfer ");
    this.transactions.append(centsToString(amount));
    this.transactions.append(" to ");
    this.transactions.append(receivingAccount.name());
    this.transactions.append("\n");
    return true;
  }

  @Override
  public String transactionReport() {
    this.transactions.append("Final Balance: ");
    this.transactions.append(centsToString(this.balance));
    this.transactions.append("\n");

    String report = this.transactions.toString();
    this.resetTransactionLog();
    return report;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/** Models a checking account in our personal finance app. */
public class CheckingAccount implements Account {
  /** The name of this account. */
  private String name;

  /** The current balance, in cents, of this account. */
  private int balance;

  /** The current month's transactions. */
  private StringBuilder transactions;

  /**
   * Constructs a checking account with given `name` and initial `balance`.
   */
  public CheckingAccount(String name, int balance) {
    this.name = name;
    this.balance = balance;
    this.resetTransactionLog();
  }

  /**
   * Reassigns the transaction log to a new `StringBuilder` object that 
   * contains this month's initial account balance
   */
  private void resetTransactionLog() {
    this.transactions = new StringBuilder("Initial Balance: ");
    this.transactions.append(centsToString(this.balance));
    this.transactions.append("\n"); // newline
  }

  /**
   * Converts the given number of `cents` into a String in "$X.XX" format.
   */
  private static String centsToString(int cents) {
    int dollars = cents / 100;
    cents = cents % 100;
    return ("$" + dollars + "." + (cents < 10 ? "0" : "") + cents);
  }

  @Override
  public String name() {
    return this.name;
  }

  @Override
  public int balance() {
    return this.balance;
  }

  /**
   * Deposits the specified `amount`, in cents, to the balance of the account, 
   * logs this transaction with the given `memo`, and returns `true` to indicate 
   * that this deposit was successful. Requires that `amount > 0`.
   */
  @Override
  public boolean depositFunds(int amount, String memo) {
    assert amount > 0;
    this.balance += amount;
    this.transactions.append(" - Deposit ");
    this.transactions.append(centsToString(amount));
    this.transactions.append(": ");
    this.transactions.append(memo);
    this.transactions.append("\n");
    return true; // we can always add funds to a checking account
  }

  @Override
  public boolean transferFunds(Account receivingAccount, int amount) {
    assert amount > 0;
    if (amount > this.balance) {
      return false; // insufficient funds
    }
    if (!receivingAccount.depositFunds(amount, "Transfer from " + this.name)) {
      return false; // could not add funds
    }
    this.balance -= amount;
    this.transactions.append(" - Transfer ");
    this.transactions.append(centsToString(amount));
    this.transactions.append(" to ");
    this.transactions.append(receivingAccount.name());
    this.transactions.append("\n");
    return true;
  }

  @Override
  public String transactionReport() {
    this.transactions.append("Final Balance: ");
    this.transactions.append(centsToString(this.balance));
    this.transactions.append("\n");

    String report = this.transactions.toString();
    this.resetTransactionLog();
    return report;
  }
}

Let’s make some observations about this code.

  1. As usual, we expect Javadoc specifications for the class, all of its fields, and all of its methods. Rather than providing full specifications for some of the methods (in particular, all of the methods declared in the Account interface), we’ve simply added the “@Override” annotation. This annotation signals that this method’s signature is pulled “down” from the interface that it implements, and it points the client to refer to the interface for its specifications. When the specifications of a method exactly match what is documented in the interface, this “@Override” annotation is sufficient. Otherwise, if the class definition refines the specifications in some way (i.e., adds additional information beyond what was given in the interface), new complete specifications should be provided. For example, a deposit into a CheckingAccount will always be successful, so the specifications of depositFunds() are refined to reflect this.
  2. We’ve included additional private helper methods resetTransactionLog() and centsToString() in the CheckingAccount class that were not mentioned in the interface. This is alright, since an interface does not preclude us from adding methods. In fact, we’ll soon see that we can even add additional public methods to a class implementing an interface.

We can similarly define a SavingsAccount class that also implements the Account interface. A SavingsAccount has additional state that models its interest rate, and it earns interest that compounds each month based on its current balance. An additional public method interestRate() provides the client access to this field, and we add interest to the balance from within the transactionReport() method by calling a private helper method accrueMonthlyInterest(). We’ll note in passing that much of the code in the SavingsAccount and CheckingAccount classes is identical, and we’ll revisit this in our next lecture.

SavingsAccount.java

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/** Models a savings account in our personal finance app. */
public class SavingsAccount implements Account {

  /** The name of this account. */
  private String name;

  /** The current balance, in cents, of this account. */
  private int balance;

  /** The current (nominal) APR of this account. */
  private double rate;

  /** The current month's transaction report. */
  private StringBuilder transactions;

  /**
   * Constructs a savings account with given `name`, initial `balance`, and interest `rate`.
   */
  public SavingsAccount(String name, int balance, double rate) {
    this.name = name;
    this.balance = balance;
    this.rate = rate;
    this.resetTransactionLog();
  }

  /**
   * Reassigns the transaction log to a new `StringBuilder` object that contains this month's
   * initial account balance
   */
  private void resetTransactionLog() {
    this.transactions = new StringBuilder("Initial Balance: ");
    this.transactions.append(centsToString(this.balance));
    this.transactions.append("\n"); // newline
  }

  /**
   * Converts the given number of `cents` into a String in "$X.XX" format.
   */
  private static String centsToString(int cents) {
    int dollars = cents / 100;
    cents = cents % 100;
    return ("$" + dollars + "." + (cents < 10 ? "0" : "") + cents);
  }

  @Override
  public String name() {
    return this.name;
  }

  @Override
  public int balance() {
    return this.balance;
  }

  /**
   * Returns the current (nominal) APR of this account.
   */
  public double interestRate() {
    return this.rate;
  }

  /**
   * Deposits the specified `amount`, in cents, to the balance of the account, 
   * logs this transaction with the given `memo`, and returns `true` to indicate 
   * that this deposit was successful. Requires that `amount > 0`.
   */
  @Override
  public boolean depositFunds(int amount, String memo) {
    assert amount > 0;
    this.balance += amount;
    this.transactions.append(" - Deposit ");
    this.transactions.append(centsToString(amount));
    this.transactions.append(": ");
    this.transactions.append(memo);
    this.transactions.append("\n");
    return true; // we can always add funds to a savings account
  }

  @Override
  public boolean transferFunds(Account receivingAccount, int amount) {
    assert amount > 0;
    if (amount > this.balance) {
        return false; // insufficient funds
    }
    if (!receivingAccount.depositFunds(amount, "Transfer from " + this.name)) {
        return false; // could not add funds
    }
    this.balance -= amount;
    this.transactions.append(" - Transfer ");
    this.transactions.append(centsToString(amount));
    this.transactions.append(" to ");
    this.transactions.append(receivingAccount.name());
    this.transactions.append("\n");
    return true;
  }

  /**
   * Called once at the end of each month to return a `String` summarizing the 
   * account's initial balance that month, all transactions made during that 
   * month, and its final balance. As a final transaction for the month, 
   * interest is accrued to the account based on the current `rate`. 
   */
  @Override
  public String transactionReport() {
    this.accrueMonthlyInterest();
    this.transactions.append("Final Balance: ");
    this.transactions.append(centsToString(this.balance));
    this.transactions.append("\n");

    String report = this.transactions.toString();
    this.resetTransactionLog();
    return report;
  }

  /**
   * Adds the monthly interest payment to the account balance.
   */
  private void accrueMonthlyInterest() {
    int interestAmount = (int) (this.balance * this.rate / (12 * 100));
    this.depositFunds(interestAmount, "Monthly interest @" + this.rate + "%");
  }
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/** Models a savings account in our personal finance app. */
public class SavingsAccount implements Account {

  /** The name of this account. */
  private String name;

  /** The current balance, in cents, of this account. */
  private int balance;

  /** The current (nominal) APR of this account. */
  private double rate;

  /** The current month's transaction report. */
  private StringBuilder transactions;

  /**
   * Constructs a savings account with given `name`, initial `balance`, and interest `rate`.
   */
  public SavingsAccount(String name, int balance, double rate) {
    this.name = name;
    this.balance = balance;
    this.rate = rate;
    this.resetTransactionLog();
  }

  /**
   * Reassigns the transaction log to a new `StringBuilder` object that contains this month's
   * initial account balance
   */
  private void resetTransactionLog() {
    this.transactions = new StringBuilder("Initial Balance: ");
    this.transactions.append(centsToString(this.balance));
    this.transactions.append("\n"); // newline
  }

  /**
   * Converts the given number of `cents` into a String in "$X.XX" format.
   */
  private static String centsToString(int cents) {
    int dollars = cents / 100;
    cents = cents % 100;
    return ("$" + dollars + "." + (cents < 10 ? "0" : "") + cents);
  }

  @Override
  public String name() {
    return this.name;
  }

  @Override
  public int balance() {
    return this.balance;
  }

  /**
   * Returns the current (nominal) APR of this account.
   */
  public double interestRate() {
    return this.rate;
  }

  /**
   * Deposits the specified `amount`, in cents, to the balance of the account, 
   * logs this transaction with the given `memo`, and returns `true` to indicate 
   * that this deposit was successful. Requires that `amount > 0`.
   */
  @Override
  public boolean depositFunds(int amount, String memo) {
    assert amount > 0;
    this.balance += amount;
    this.transactions.append(" - Deposit ");
    this.transactions.append(centsToString(amount));
    this.transactions.append(": ");
    this.transactions.append(memo);
    this.transactions.append("\n");
    return true; // we can always add funds to a savings account
  }

  @Override
  public boolean transferFunds(Account receivingAccount, int amount) {
    assert amount > 0;
    if (amount > this.balance) {
        return false; // insufficient funds
    }
    if (!receivingAccount.depositFunds(amount, "Transfer from " + this.name)) {
        return false; // could not add funds
    }
    this.balance -= amount;
    this.transactions.append(" - Transfer ");
    this.transactions.append(centsToString(amount));
    this.transactions.append(" to ");
    this.transactions.append(receivingAccount.name());
    this.transactions.append("\n");
    return true;
  }

  /**
   * Called once at the end of each month to return a `String` summarizing the 
   * account's initial balance that month, all transactions made during that 
   * month, and its final balance. As a final transaction for the month, 
   * interest is accrued to the account based on the current `rate`. 
   */
  @Override
  public String transactionReport() {
    this.accrueMonthlyInterest();
    this.transactions.append("Final Balance: ");
    this.transactions.append(centsToString(this.balance));
    this.transactions.append("\n");

    String report = this.transactions.toString();
    this.resetTransactionLog();
    return report;
  }

  /**
   * Adds the monthly interest payment to the account balance.
   */
  private void accrueMonthlyInterest() {
    int interestAmount = (int) (this.balance * this.rate / (12 * 100));
    this.depositFunds(interestAmount, "Monthly interest @" + this.rate + "%");
  }
}

Interfaces in Client Code

Now that we’ve defined the SavingsAccount and CheckingAccount classes, let’s use them to fill in the details of our client code. To initialize the Account variables checking and savings, we can call the CheckingAccount and SavingsAccount constructors. Then, we can copy the remaining client code from above, placing this all in a main() method in a FinanceSimulation class to complete a simple application.

FinanceSimulation.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class FinanceSimulation {
  public static void main(String[] args) {
    Account checking = new CheckingAccount("Checking", 55000);
    Account savings = new SavingsAccount("Savings", 230000, 3.0);

    checking.depositFunds(4000, "Check from John");
    checking.transferFunds(savings, 10000);
    System.out.println(checking.name());
    System.out.println(checking.transactionReport());
    System.out.println(savings.name());
    System.out.println(savings.transactionReport());
  }
} 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class FinanceSimulation {
  public static void main(String[] args) {
    Account checking = new CheckingAccount("Checking", 55000);
    Account savings = new SavingsAccount("Savings", 230000, 3.0);

    checking.depositFunds(4000, "Check from John");
    checking.transferFunds(savings, 10000);
    System.out.println(checking.name());
    System.out.println(checking.transactionReport());
    System.out.println(savings.name());
    System.out.println(savings.transactionReport());
  }
} 

When we run this application, we get the expected output:


Checking
Initial Balance: $550.00
 - Deposit $40.00: Check from John
 - Transfer $100.00 to Savings
Final Balance: $490.00

Savings
Initial Balance: $2300.00
 - Deposit $100.00: Transfer from Checking
 - Deposit $6.00: Monthly interest @3.0%
Final Balance: $2406.00

From this example, we can see the role of an interface as an intermediary between the implementer code and client code. Here, after the initial construction of the objects referenced by checking and savings, the client code never directly interacts with the CheckingAccount and SavingsAccount classes. Instead, it only calls methods from the Account interface, so the client only needs to be aware of the specifications of this one interface to develop their code. By implementing the Account interface, the CheckingAccount and SavingsAccount authors offered this simpler perspective on their classes to the client. For the rest of today’s lecture, we’ll formalize this notion of a “simpler perspective” by introducing subtype relationships.

Subtype Relationships

Dynamic vs Static Types

How would we visualize the initialization “Account checking = new CheckingAccount("Checking", 55000);” in a memory diagram? The RHS is a new expression that calls the CheckingAccount constructor, so it allocates a CheckingAccount object on the heap. We store the address of this object into a variable checking with type Account.

This is the first example that we have seen of a type “mismatch” that was allowed in the code. We declared a variable checking with one type (Account) that references an object that has a different type (CheckingAccount). To talk about these different types both relating to the variable checking, we’ll introduce some refined terminology.

Definition: Static, Dynamic Types

The static type of a variable is the type included in its declaration statement. This type is known at compile time and restricts which object references can be assigned to that variable.

The dynamic type of an object is determined by the constructor that was initially called during the creation of that object. The dynamic type of an object referenced by a variable is not known at compile time.

We must be very precise with this terminology. Notice that these two notions of type are used to describe different entities in our programs. Variables (and expressions) have static types, whereas objects have dynamic types. The word “static” means unchanging, or inherently present. Since the type of a variable is declared once and persists throughout its lifetime, this is a static property. The fact that it appears explicitly in the variable’s declaration makes the static type accessible to the compiler at compile time. We can contrast this with a dynamic type. Dynamic means changing. While a variable’s (static) type persists throughout its lifetime, it may be reassigned to reference different objects with different dynamic types. Since objects exist only at runtime, the compiler cannot determine dynamic types at compile time (we’ll see an example of why soon).

Subtype Substitution

Does it seem reasonable to reference a CheckingAccount object from an Account variable? By declaring the static type of checking to be Account, we are signaling that checking should be able to store a reference to any Account object. Do CheckingAccounts count as Account objects? We can argue, yes! Anything that I want to do with an Account object makes sense to do with a CheckingAccount, since they accommodate all of the same behaviors; this is enforced by the fact that CheckingAccount implements the Account interface. Because of this, we say that CheckingAccount is a subtype of Account.

Definition: Subtype, Supertype

A reference type S is a subtype of a reference type T (equivalently, T is a supertype of S) if it makes sense to use the type S in any place where the type T was expected. In other words, S is a subtype of T if S is substitutable for T.

We often denote this supertype/subtype relationship using the notation S <: T or T :> S.

Implementing an interface is one way to establish a subtype relationship (the implementing class type is a subtype of the interface type). We’ll talk about a second mechanism, inheritance in our next lecture. Let’s drill down on what is meant by substitutability. There are four primary ways that we interact with a variable of (static) type T in our code.

First, we can access a behavior of a T by invoking a method on a variable with static type T. To make this invocation substitutable, it must be the case that every method signature that exists for supertype T also exists for subtype S. Interfaces enforce this by requiring that the implementing (subtype) class include definitions of all methods declared in the interface.

Next, we can assign an expression with (static) type T to a variable t with static type T. For this to be substitutable with a subtype S, we must also be able to assign an expression with static type S to t.

Subtype Substitution Rule 1: Assignment

If t is a variable with static type T and S <: T, then we should be able to assign any expression with static type S to t.

It is this subtype substitution rule that allows us to assign a reference to a CheckingAccount object to our Account variable.

Next, we can specify that a parameter of a method has static type T, meaning any expression with type T can be passed in as an argument to that method. For this to be substitutable with a subtype S, we must also be able to pass in an expression with type S.

Subtype Substitution Rule 2: Parameters

If f() is a method that accepts a parameter with type T and S <: T, then we should be able to pass in any expression with static type S to f() to serve as this parameter.

Our example code made use of this rule in the line checking.transferFunds(savings, 10000);, which passed savings, a reference to a SavingsAccount object to serve as the receivingAccount parameter with type Account.

Finally, we can declare that T is the return type of a method, which promises that the method will return an expression with type T. For this to be substitutable with a subtype S, we must also be able to return an expression with type S.

Subtype Substitution Rule 3: Return Types

If f() is a method with return type T and S <: T, then f() should be able to return an expression with static type S.

We haven’t seen an example of this yet, but we will soon when we introduce factory methods.

Remark:

Reading these rules and definitions can be a bit challenging because of all of the symbols. A more intuitive notation of subtype/supertype relations is that every object belonging to a subtype should also have the supertype, meaning the supertype describes a superset of objects of the subtype. In still other words, the subtype refines the description of the supertype.

It's also helpful to work through the rules with some familiar "types" from the real world; when we do this, the rules should hopefully seem almost "obvious". Let's think about the subtype relationship Cat <: Animal (it's true that every cat is an animal, though not every animal is a cat). The first substitution rule says, "a variable that can store any animal should be able to store any cat." The second substitution rule says, "if a method can accept any animal as an argument, it should be able to accept any cat as this argument." The third substitution rule says, "if a method promises to return an animal, it will fulfill this obligation by returning a cat." To cement this idea, try swapping "cat" and "animal" in the preceding sentences to confirm that these assertions no longer make sense.

We visualize subtypes using a picture called a “type hierarchy”. In this picture, we write out the names of all of the types, with supertypes appearing generally above their subtypes. Types derived from interfaces are italicized. We draw an arrow from each subtype to its supertype. We use a dashed arrow to indicate that this subtype relationship is established via implementation of an interface. For our Account example, we have the following type hierarchy.

Polymorphism

You might be wondering the benefit of interfaces. In our client code, couldn’t we have just declared checking to have static type CheckingAccount and savings to have static type SavingsAccount? In this case, yes, we could have done this and arrived at the same result. However, this approach can quickly lose tractability. While we’ve considered a case where there were two subtypes of an interface, there could potentially be hundreds or thousands of subtypes (imagine all of the different accounts one can open across many different banks). Let’s identify a couple places where dealing with all these account types separately would be impractical.

In short, subtype substitution improves code flexibility, and interfaces are a method for establishing subtype relationships. The ability for a single piece of code to accommodate many different subtypes is called polymorphism, derived from the Greek roots poly meaning “many” and morph meaning “shape”.

Definition: Polymorphism

We say that a piece of code is polymorphic if it can correctly handle data with potentially many different shapes, meaning that statements within the code are written in a way that accommodates different types of data.

Specifically, interfaces allow for subtype polymorphism. We’ll discuss another type of polymorphism, parametric polymorphism, in a few lectures. The support for polymorphism (e.g., by realizing the subtype substitution rules) is one of the central features of object-oriented languages, and leveraging polymorphism is a great way to become a better programmer. Next, we’ll look at two important concepts for handling types that are important when developing polymorphic code.

Dynamic Dispatch

Observe that the transactionReport() methods in CheckingAccount and SavingsAccount have slightly different behaviors, evident from the refined specifications on SavingsAccount.transactionReport() (shorthand that we will use for the transactionReport() method of the SavingsAccount class). The SavingsAccount calculates the interest accrued during that month and adds this as a final transaction on the report. In the output of our FinanceSimulation application, we see that an interest accrual is printed for the savings account but not the checking account. While this may seem unsurprising, let’s stop to think about what happened behind the scenes. For the first print statement, we invoked the transactionReport() method on the object referenced by checking, a variable with static type Account. For the second print statement, we invoked the transactionReport() method on the object referenced by savings, another variable with static type Account. How was Java able to distinguish these, calling CheckingAccount.transactionReport() in the first case and SavingsAccount.transactionReport() in the second case? The answer is that it uses the dynamic types of the objects referenced by checking and savings. Since checking points to a CheckingAccount object, Java knows to invoke CheckingAccount’s definition of transactionReport(). Similarly, since savings points to a SavingsAccount object, Java knows to invoke SavingsAccount’s definition of transactionReport(). Moreover, all of this happens at runtime since the compiler is unable to determine dynamic types at compile time.

Why is this latter statement true? In this example, it is clear that checking was assigned a reference to a CheckingAccount object and savings was assigned a reference to a SavingsAccount object only a couple lines up. However, in other cases, the situation is not as straightforward. Suppose that we add the following method to our client code that can be used to create new accounts.

FinanceSimulation.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** 
 * Creates and returns a new account of the specified `type` with given `name` 
 * and initial `balance`. Requires that `type.equals("checking")` or `type.equals("savings")`.
 */
public static Account createAccount(String type, String name, int balance) {
  if (type.equals("checking")) {
    return new CheckingAccount(name, balance);
  } else { // type.equals("savings")
    return new SavingsAccount(name, balance, 3.0);
  } 
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** 
 * Creates and returns a new account of the specified `type` with given `name` 
 * and initial `balance`. Requires that `type.equals("checking")` or `type.equals("savings")`.
 */
public static Account createAccount(String type, String name, int balance) {
  if (type.equals("checking")) {
    return new CheckingAccount(name, balance);
  } else { // type.equals("savings")
    return new SavingsAccount(name, balance, 3.0);
  } 
}

We call this a factory method because it is used to construct and return new objects. Notice that this method leverages the third subtype substitution rule since it has a declared return type of Account (the supertype) but actually returns a reference to a CheckingAccount or a SavingsAccount (the subtypes). We can modify our client code to make use of this factory method:

FinanceSimulation.java

1
2
3
4
5
6
7
8
9
Account checking = createAccount("checking", "Checking", 55000);
Account savings = createAccount("savings", "Savings", 2300000);

checking.depositFunds(4000, "Check from John");
checking.transferFunds(savings, 10000);
System.out.println(checking.name());
System.out.println(checking.transactionReport());
System.out.println(savings.name());
System.out.println(savings.transactionReport());
1
2
3
4
5
6
7
8
9
Account checking = createAccount("checking", "Checking", 55000);
Account savings = createAccount("savings", "Savings", 2300000);

checking.depositFunds(4000, "Check from John");
checking.transferFunds(savings, 10000);
System.out.println(checking.name());
System.out.println(checking.transactionReport());
System.out.println(savings.name());
System.out.println(savings.transactionReport());

This results in the same output as our previous client code. In this case, the compiler doesn’t have an easy way to detect the dynamic types, as doing so would require the execution of the createAccount() method; the compiler doesn’t execute code, it only translates it to byte code.

Remark:

If you are still not convinced, you can imagine a program that asks the user to select the new account type (either with keyboard input, clicking a button, etc.), in which case the dynamic type can definitely not be determined until the program is run.

We call this use of the dynamic type to locate a method at runtime dynamic dispatch.

Definition: Dynamic Dispatch

The principle of dynamic dispatch, which is followed by Java, says that an object's dynamic type is used to locate the definition of a method for which it is the target.

We will continue our discussion of the specifics of dynamic dispatch in the next lecture. Dynamic dispatch enables subtype polymorphism, since it allows a common line of code executed on a supertype variable to invoke different behaviors based on which (dynamic) type of object it actually references.

The Compile-Time Reference Rule

Recall that the type of an expression (including a variable) determines which operations/behaviors can be invoked on that expression. The compiler uses static type analysis to enforce this. In polymorphic code, where the static type of a variable and the dynamic type of the object that it references can be different, we need to be careful about which methods we attempt to call on a variable. Consider the following code:

1
2
Account a = new SavingsAccount("Savings",230000,3.0);
System.out.println("Interest Rate: " + a.interestRate() + "%");
1
2
Account a = new SavingsAccount("Savings",230000,3.0);
System.out.println("Interest Rate: " + a.interestRate() + "%");

What should be the result when this code is executed? You might expect that this code will print “Interest Rate: 3.0%” since it calls the interestRate() accessor method in the SavingsAccount class, which will return the 3% interest value that was initialized by the constructor on the first line. In actuality, this code does not compile, and we receive the error message


Cannot resolve method 'interestRate' in 'Account'

Since a was declared with the static type Account, the compiler will only allow our code to invoke methods a that are declared for the Account type. Since interestRate() is declared in the SavingsAccount class but not the Account class, we receive an error at compile time. We call this restriction to methods declared for the static type the compile-time reference rule.

Definition: Compile-Time Reference Rule

The compile-time reference rule states that the methods that we may invoke on a variable are determined by the static type of that variable. Violations of the compile-time reference rule are identified by the compiler and prevent the compilation of the code.

The compile-time reference rule is one of the most important rules in the course, as it underscores object-oriented design. It can also be challenging for students to grasp at first, since it defies our intuition: “we know that a references a SavingsAccount object, which has an interestRate() method, so why can’t we call it?". This statement that a “references a SavingsAccount object” is describing a dynamic type, which is unknown to the compiler. Since the compiler’s job is to ensure type safety, it must use the information it has available (the static type) to decide what method calls to allow. Not every Account variable will reference an account with an interestRate(), so the compiler disallows this call to prevent the case where it fails.

It can be helpful to think about the static type of a variable as describing the perspective or point-of-view that it looks at an object from. Two variables, one with static type Account and one with static type SavingsAccount can both store a reference to (i.e., look at) a SavingsAccount object, but they will “see” different methods. Since the SavingsAccount is a more detailed description of the object than Account (i.e., it is a subtype), the SavingsAccount variable can access behaviors that the Account variable cannot.

Together, dynamic dispatch and the compile-time reference rule describe the following dichotomy of how object behaviors are managed in Java.

The static type of a variable determines which methods may be invoked on an object that it references. The dynamic type of the object referenced by that variable determines which definition of that method actually executes at runtime.

Reference Type Coercion

We first introduced coercion when we were discussing primitive types. Coercion is a mechanism to alter the type of an expression to change which operations can be applied to it. For primitive types, we saw that this coercion was sometimes implicit, as in the type-widening of the second argument in 1.0 / 2 from int to double to allow double division. Other times, it needed to be explicit with a cast, such as in (double) 1 / 2 which coerces the int expression 1 to the double expression 1.0 to prevent integer division. These coercions change the underlying memory representation of their expressions; for example, the cast (int) (1.0 / 2) converts the double value 0.5 to the int value 0, a different number entirely!

Coercions of reference types share some similarities to those of primitive types but also have some differences. The main difference is that reference type coercions do not change the underlying object. The coercion of a reference type expression changes the static type of that expression, which has the effect of viewing the referenced object from a different “perspective”.

Similar to primitive type coercions, sometimes reference type coercions are implicit. Consider the following code.

1
2
CheckingAccount c = new CheckingAccount("Checking",10000);
Account a = c;
1
2
CheckingAccount c = new CheckingAccount("Checking",10000);
Account a = c;

In the assignment statement on the second line, we are assigning c, which is a reference to a CheckingAccount object to a, a variable with static type Account. These types are mismatched, but the first subtype substitution rule says that such an assignment should be allowed (CheckingAccount <: Account). Therefore, Java does an implicit type coercion, which allows a to view the CheckingAccount object referenced by c from the perspective of an Account. We often refer to these implicit coercions as up-casts since they adjust the perspective to a static type that is higher in the type hierarchy.

In other cases, an explicit coercion (i.e., a cast) is required. Consider our code from above.

1
2
Account a = new SavingsAccount("Savings",230000,3.0);
System.out.println("Interest Rate: " + a.interestRate() + "%");
1
2
Account a = new SavingsAccount("Savings",230000,3.0);
System.out.println("Interest Rate: " + a.interestRate() + "%");

This code violated the compile-time reference rule, so did not compile. The static type of a is Account, and this is the perspective from which a views the object that it references. Shifting perspective to view this object as a SavingsAccount and access the interestRate() method is not automatic; there’s a possibility that a could reference an object that is not a SavingsAccount (e.g., it could refer to a CheckingAccount object), which would cause an error at runtime. Let’s add a cast to this code:

1
2
Account a = new SavingsAccount("Savings",230000,3.0);
System.out.println("Interest Rate: " + ((SavingsAccount)a).interestRate() + "%");
1
2
Account a = new SavingsAccount("Savings",230000,3.0);
System.out.println("Interest Rate: " + ((SavingsAccount)a).interestRate() + "%");

Here, the expression (SavingsAccount)a says to the compiler, “trust me, it’s alright to view the object referenced by a from the perspective of a SavingsAccount.” Once the compiler views a from this new perspective (i.e., assigns the static type SavingsAccount to the expression (SavingsAccount)a), the compile-time reference rule is no longer violated, and the code will both compile and execute without error. We often refer to these explicit coercions as down-casts since they adjust the perspective to a static type that is lower in the type hierarchy.

Explicit casts can result in undesired behaviors. Consider the following code.

1
2
Account a = new CheckingAccount("Checking",55000);
System.out.println("Interest Rate: " + ((SavingsAccount)a).interestRate() + "%");
1
2
Account a = new CheckingAccount("Checking",55000);
System.out.println("Interest Rate: " + ((SavingsAccount)a).interestRate() + "%");

Since the compiler has no information about the dynamic type of the object referenced by a, the best it can do is trust that the cast (SavingsAccount)a will work (after all, it will work whenever a truly references a SavingsAccount). This code will compile successfully, but face a problem at runtime, when the “perspective shift” that arises from the cast is determined to be impossible and a ClassCastException is thrown, crashing the program. The compiler was unable to detect this error. These examples expose some subtlety in how casts are handled. It’s best to reason about casts in two separate stages:

  1. Compile Time: The compiler asks, “Is this cast possible?” In other words, is it possible for a variable with the “starting” static type to refer to an object whose dynamic type can be viewed from the perspective of (i.e., is a subtype of) the “ending” static type? If it’s possible, then the compiler will assume the cast works correctly and continue the compilation.

In the preceding example, it was possible for the Account variable a to refer to a SavingsAccount object, so the compilation was successful.

  1. Runtime: When the program executes the cast, it looks at the dynamic type of the object and asks, “Will the cast actually succeed?” In other words, is the dynamic type of the object a subtype of the “ending” static type, such that the object can be viewed from the “perspective” of this “ending” static type? If the cast fails, then a ClassCastException is thrown at runtime, most likely crashing the program.

In the preceding example, the dynamic type CheckingAccount is not a subtype of SavingsAccount, so the cast fails. There are more examples of casts for you to practice working through this two-stage reasoning in the lecture exercises.

Since casting admits the possibility that our code compiles but crashes at runtime, we seek to avoid it whenever possible. Rather, we prefer to use dynamic dispatch to leverage subtype-specific behaviors from within polymorphic code. Soon, we will introduce another tool, dynamic type queries, that can be used to add guardrails around casts and prevent runtime errors.

Main Takeaways:

  • Interfaces are a Java construct that allow us to define a type with a specified set of behaviors without committing to the state representation or implementation of these behaviors. Interfaces cannot be instantiated directly.
  • When a class implements an interface, it provides a definition for each of the interface's methods. This establishes a subtype relationship between the class and the interface, which can be illustrated in a type hierarchy.
  • The subtype substitution rules describe when Java will allow a subtype in place of a declared (static) type.
  • Polymorphic code is written to accommodate objects with different types. Subtype polymorphism is enabled through dynamic dispatch, which determines (at runtime) the "version" of a method to invoke on a target based on its dynamic type.
  • The compile-time reference rule says that the methods that can be invoked on an object are determined by the static type of the target variable referencing that object.
  • Coercion of reference types is done with casting. Up-casting is handled automatically by subtype substitution. Down-casting must be done explicitly and can fail for different reasons either at compile time or runtime. Avoid explicit casts when you can.

Exercises

Exercise 9.1: Check Your Understanding
(a)

Consider this piece of code that uses Account and SavingsAccount from the above lecture notes.

1
2
3
SavingsAccount myAccount = new SavingsAccount("Savings", 10000, 3.0);
Account yourAccount = new Account();
myAccount.transferFunds(yourAccount, 500);
1
2
3
SavingsAccount myAccount = new SavingsAccount("Savings", 10000, 3.0);
Account yourAccount = new Account();
myAccount.transferFunds(yourAccount, 500);
What happens when the following code is run?
Check Answer
(b)
True or False: Subtype substitution rules only apply to direct subtypes. For instance, for subtype substitution rule 1, if R <: S and S <: T, then it is not necessarily the case that you can assign an expression of static type R to a variable of static type T.
Check Answer
(c)

Assume the following subtype relationships.

  • Cat <: Pet
  • Manx <: Cat
  • Birman <: Cat
1
2
3
4
Pet p = new Birman();
Cat c = (Cat)p;
p = new Manx();
Manx m = (Manx)p;
1
2
3
4
Pet p = new Birman();
Cat c = (Cat)p;
p = new Manx();
Manx m = (Manx)p;
After running the above code, what is the static type of variable c?
Check Answer
After running the above code, what is the dynamic type of the object referenced by c?
Check Answer
Exercise 9.2: More Implementations of Account Interface
For each of the following, define a class modeling this account type that implements the Account interface. You may need to define extra methods to support behaviors specific to these accounts.
(a)
A CD account, or Certificate of Deposit, is a type of savings account where you can add funds once at the beginning and matures after a certain amount of time with a set interest rate.
(b)
An investment account has an available balance that can be invested in shares of stocks with current prices which “locks” this part of the balance until they are sold.
Exercise 9.3: Working with Multiple Interfaces
Consider the set of interfaces modeling even more banking accounts.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public interface HighInterestAccount extends Account {
  /**
   * Withdraws money from the account. Requires that withdrawing `amount` will not
   * reduce the balance to below $1000.
   */
  void withdraw(double amount);
}
public interface OverdraftAccount extends Account {
  /**
   * Withdraws money from the account, allowing the balance to go into overdraft, 
   * i.e., below zero.
   */
  void withdraw(double amount);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public interface HighInterestAccount extends Account {
  /**
   * Withdraws money from the account. Requires that withdrawing `amount` will not
   * reduce the balance to below $1000.
   */
  void withdraw(double amount);
}
public interface OverdraftAccount extends Account {
  /**
   * Withdraws money from the account, allowing the balance to go into overdraft, 
   * i.e., below zero.
   */
  void withdraw(double amount);
}
(a)

When an interface extends another interface, the child interface inherits all of the parent interface’s method declarations. This notion of inheritance will be explored more thoroughly in the next lecture.

1
public class HighYieldSavingsAccount implements HighInterestAccount { ... }
1
public class HighYieldSavingsAccount implements HighInterestAccount { ... }
What methods must this class implement to compile?
(b)

A class can also implement several interfaces to combine multiple capabilities.

1
public class HybridAccount implements HighInterestAccount, OverdraftAccount { ... }
1
public class HybridAccount implements HighInterestAccount, OverdraftAccount { ... }
What methods must this class implement to compile?
(c)
Notice that HybridAccount implements interfaces that both have the method signature void withdraw(double amount). Will this cause an issue at a compile time level? If not, might this cause issues from a different perspective?
Exercise 9.4: Subtype Substitutions
Consider the following set of classes and interfaces. Assume that the classes defined have some implementation and a default (no-arguments) constructor.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public interface Character {
  void attack(Character enemy);
}

public interface Healer {
  void heal(Summoner ally);
}

public interface Summoner {
  Healer summon();
}

public class NPC implements Character { ... }
public class Witch implements Summoner { ... }
public class Cleric implements Healer { ... }
public class Druid implements Healer, Summoner { ... }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public interface Character {
  void attack(Character enemy);
}

public interface Healer {
  void heal(Summoner ally);
}

public interface Summoner {
  Healer summon();
}

public class NPC implements Character { ... }
public class Witch implements Summoner { ... }
public class Cleric implements Healer { ... }
public class Druid implements Healer, Summoner { ... }
Determine whether each of the following code snippets (appropriately placed within a main() method of some class) will compile. If you answer "no", explain why not.
(a)
1
2
Druid d = new Witch();
d.attack(new NPC());
1
2
Druid d = new Witch();
d.attack(new NPC());
(b)
1
2
Healer h = new Cleric();
h.attack(new Druid());
1
2
Healer h = new Cleric();
h.attack(new Druid());
(c)
1
2
Druid d = new Druid();
d.heal(new Healer());
1
2
Druid d = new Druid();
d.heal(new Healer());
(d)

Suppose you are implementing the summon method for Summoner.

1
2
3
public Healer summon() {
  return new NPC();
}
1
2
3
public Healer summon() {
  return new NPC();
}
Exercise 9.5: Dynamic Dispatch and Compile-Time Reference Rule
For each of the following problems, identify if there is a compile-time or run-time error. If it fails to compile, edit the code to make it compile. If neither, for each method invocation, write which "version" of the method was executed. For example, "NPC.attack()".
(a)
1
2
3
4
5
6
7
8
Character[] players = new Character[3];
players[0] = new Witch();
players[1] = new Druid();
players[2] = new Cleric();
NPC n = new NPC();
for (int i = 0; i < 3; i++) {
  players[i].attack(n);
}
1
2
3
4
5
6
7
8
Character[] players = new Character[3];
players[0] = new Witch();
players[1] = new Druid();
players[2] = new Cleric();
NPC n = new NPC();
for (int i = 0; i < 3; i++) {
  players[i].attack(n);
}
(b)

Assume that the implementation of summon() in Witch is:

1
2
3
public Healer summon() {
  return new Druid();
}
1
2
3
public Healer summon() {
  return new Druid();
}
1
2
3
Summoner s = new Witch();
s.summon().heal(new NPC());
s.summon().summon();
1
2
3
Summoner s = new Witch();
s.summon().heal(new NPC());
s.summon().summon();
Exercise 9.6: default Methods in Interfaces
Default methods in interfaces allow us to define a concrete implementation that subtypes can choose to override. For instance, consider the following:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/** A 1-dimensional or 2-dimensional shape. */
public interface Shape {
  /** Returns the non-zero area for a 2D shape or 0 for a 1D shape. */
  double area();

  /** Returns the perimeter for a 2D shape or the length for a 1D shape. */
  double perimeter();

  /** Returns whether this shape is 2D. */
  boolean isTwoDimensional();
}

public class Circle implements Shape { ... }

public class Trapezoid implements Shape { ... }

/** A 1-dimensional line with 0 area and non-zero perimeter. */
public class Line implements Shape { ... }

/** A 1-dimensional point with 0 area and 0 perimeter. */
public class Point implements Shape { ... }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/** A 1-dimensional or 2-dimensional shape. */
public interface Shape {
  /** Returns the non-zero area for a 2D shape or 0 for a 1D shape. */
  double area();

  /** Returns the perimeter for a 2D shape or the length for a 1D shape. */
  double perimeter();

  /** Returns whether this shape is 2D. */
  boolean isTwoDimensional();
}

public class Circle implements Shape { ... }

public class Trapezoid implements Shape { ... }

/** A 1-dimensional line with 0 area and non-zero perimeter. */
public class Line implements Shape { ... }

/** A 1-dimensional point with 0 area and 0 perimeter. */
public class Point implements Shape { ... }
(a)

We want to define another method in the Shape interface called isLargerThan.

1
2
3
4
5
6
7
/**
 * Returns if `this` is larger than `other`. A 2-dimensional shape is larger
 * than a 1-dimensional shape. A 2D shape is larger than another 2D shape 
 * if its area is larger. A 1D shape is larger than another 1D shape if 
 * its perimeter is larger.
 */
boolean isLargerThan(Shape other);
1
2
3
4
5
6
7
/**
 * Returns if `this` is larger than `other`. A 2-dimensional shape is larger
 * than a 1-dimensional shape. A 2D shape is larger than another 2D shape 
 * if its area is larger. A 1D shape is larger than another 1D shape if 
 * its perimeter is larger.
 */
boolean isLargerThan(Shape other);
Suppose this method declaration is in the Shape interface. Suppose the four classes defined have implementations for the existing methods in the interface. Edit the four classes to align with this new interface. How many classes did you have to edit?
(b)

Instead, we can choose to use default methods. Consider the updated Shape interface. Note that in the default method, we can call methods defined within the same interface. This allows us to treat interfaces as small toolkits to build more complex methods from the abstract ones.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/** A 1-dimensional or 2-dimensional shape. */
public interface Shape {
  // ...

  /**
  * Returns if `this` is larger than `other`. A 2-dimensional shape is larger
  * than a 1-dimensional shape. A 2D shape is larger than another 2D shape 
  * if its area is larger. A 1D shape is larger than another 1D shape if 
  * its perimeter is larger.
  */
  default boolean isLargerThan(Shape other) {
    return area() > other.area();
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/** A 1-dimensional or 2-dimensional shape. */
public interface Shape {
  // ...

  /**
  * Returns if `this` is larger than `other`. A 2-dimensional shape is larger
  * than a 1-dimensional shape. A 2D shape is larger than another 2D shape 
  * if its area is larger. A 1D shape is larger than another 1D shape if 
  * its perimeter is larger.
  */
  default boolean isLargerThan(Shape other) {
    return area() > other.area();
  }
}
Since we defined isLargerThan() as a default method, all classes that implement Shape will have this implementation by default. Do any methods still need to override isLargerThan() to meet the specifications? How many files in total would you need to edit?
(c)
Consider a large, production-facing codebase where there are several classes that implement Shape. Why would a developer use default methods?
Exercise 9.7: static Methods in Interfaces
Like default methods, static methods in interfaces can provide existing implementations of methods. However, they don't require an instance of the interface to invoke the method. We'll explore this by adding a factory method to Shape.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** A 1-dimensional or 2-dimensional shape. */
public interface Shape {
  // ...

  /**
   * Returns a quadrilateral with the specified side lengths. If `side1
   * = side2`, returns a Square, otherwise a Rectangle. Requires `side1 > 0`
   * and `side2 > 0`.
   */
  static Shape createQuadrilateral(double side1, double side2) { ... }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** A 1-dimensional or 2-dimensional shape. */
public interface Shape {
  // ...

  /**
   * Returns a quadrilateral with the specified side lengths. If `side1
   * = side2`, returns a Square, otherwise a Rectangle. Requires `side1 > 0`
   * and `side2 > 0`.
   */
  static Shape createQuadrilateral(double side1, double side2) { ... }
}
(a)

Implement createQuadrilateral assuming the following constructors in the partial class definitions.

1
2
3
4
5
6
7
public class Square implements Shape {
  public Square(double side) { ... }
}

public class Rectangle implements Shape {
  public Rectangle(double base, double height) { ... }
}
1
2
3
4
5
6
7
public class Square implements Shape {
  public Square(double side) { ... }
}

public class Rectangle implements Shape {
  public Rectangle(double base, double height) { ... }
}
(b)
static methods usually lend themselves to utility or factory helper methods. Why is this the case? Why might a developer want to use static methods in their interface?
Exercise 9.8: Drawing Type Hierarchies
For each of the following, draw the type hierarchy with the information given.
(a)
1
2
3
4
5
6
7
public interface Editable { ... }
public interface Printable { ... }
public interface Shareable { ... }

public class PDF implements Printable, Shareable { ... }
public class Whiteboard implements Editable { ... }
public class WordDoc implements Editable, Printable, Shareable { ... }
1
2
3
4
5
6
7
public interface Editable { ... }
public interface Printable { ... }
public interface Shareable { ... }

public class PDF implements Printable, Shareable { ... }
public class Whiteboard implements Editable { ... }
public class WordDoc implements Editable, Printable, Shareable { ... }
(b)
1
2
3
4
5
6
7
public interface Vehicle { ... }
public interface ElectricVehicle extends Vehicle { ... }
public interface Pedalable { ... }

public class Car implements Vehicle { ... }
public class ElectricBicycle implements ElectricVehicle, Pedalable { ... }
public class Tricycle implements Vehicle, Pedalable { ... }
1
2
3
4
5
6
7
public interface Vehicle { ... }
public interface ElectricVehicle extends Vehicle { ... }
public interface Pedalable { ... }

public class Car implements Vehicle { ... }
public class ElectricBicycle implements ElectricVehicle, Pedalable { ... }
public class Tricycle implements Vehicle, Pedalable { ... }
Exercise 9.9: Memory Diagrams with Static and Dynamic Types
For the following problems, you can represent objects as empty rounded rectangles labeled with their (dynamic) type.
(a)
Draw the memory diagram after running the code in Exercise 9.5.a. Assume the attack() method doesn’t modify any objects.
(b)
Draw the memory diagram after running the code in Exercise 9.1.a.
(c)
Draw the memory diagram after running the code in Exercise 9.1.c.
Exercise 9.10: Casting Castling Pieces
Consider the following type hierarchy diagram.
For the following, state whether there is a compile-time error, run-time error, or no error. Assume the King and Rook classes have default (no-argument) constructors.
(a)
1
2
Piece p = new King();
King k = (King) p;
1
2
Piece p = new King();
King k = (King) p;
(b)
1
2
Piece p = new Rook();
King k = (King) p;
1
2
Piece p = new Rook();
King k = (King) p;
(c)
1
2
King k = new King();
Piece p = (Piece) k;
1
2
King k = new King();
Piece p = (Piece) k;
(d)
1
2
Piece p = new Rook();
Promotable pr = (Promotable) p;
1
2
Piece p = new Rook();
Promotable pr = (Promotable) p;
(e)
1
2
Piece p = new King();
Promotable pr = (Promotable) p;
1
2
Piece p = new King();
Promotable pr = (Promotable) p;
(f)
1
2
King k = new King();
Promotable pr = (Promotable) k;
1
2
King k = new King();
Promotable pr = (Promotable) k;
(g)
1
2
Rook r = new Rook();
King k = (King) r;
1
2
Rook r = new Rook();
King k = (King) r;
(h)
1
2
Rook r = new Rook();
King k = (King)(Piece) r;
1
2
Rook r = new Rook();
King k = (King)(Piece) r;
(i)
1
2
King k = new King();
Promotable pr = (Promotable)(Rook)(Piece) k;
1
2
King k = new King();
Promotable pr = (Promotable)(Rook)(Piece) k;