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
10. Inheritance

10. Inheritance

In the previous lecture, we introduced subtypes, a powerful feature of object-oriented programming that allows us to model hierarchical relationships between types. We leverage this through subtype polymorphism, where we can use dynamic dispatch to invoke different behaviors on objects of different types using a single piece of code.

In today’s lecture, we’ll look at a more structured way to establish a subtype relationship, inheritance. While interfaces ensure the availability of certain method signatures within the subtype class, defining a subclass with inheritance allows for the sharing of method definitions and fields, which can help improve the organization of our code. In today’s lecture, we’ll introduce new syntax and augment our object diagrams and understanding of runtime dispatch to accommodate subclass relationships. We’ll also discuss some additional factors to consider when designing new classes.

Subclass Relationships

We used an example of a personal finance app to help motivate interfaces and polymorphism. We will continue with this example in today’s lecture. As a reminder, we created the following classes for CheckingAccounts and SavingsAccounts. We have made a small enhancement to CheckingAccounts that will help illustrate concepts from today’s lecture. Now, a CheckingAccount has a monthly maintenance fee (ACCOUNT_FEE) that must be paid if the account balance ever drops below a certain amount (MINIMUM_BALANCE) during that month. The declaration of public static final variables with names in MACRO_CASE is how we model constants in Java. We added a new boolean field chargeFee to the CheckingAccount class that tracks whether the fee should be applied at the end of the current month and modified the logic in some of the methods to incorporate this feature.

The full starting source code for both classes is provided below as a reference. Take a look through this source code, and particularly take note of the commonalities between both classes, which have been highlighted.

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 + "%");
  }
}

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
 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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/** Models a checking account in our personal finance app. */
public class CheckingAccount implements Account {
  /**
  * The balance must remain above this amount, in cents, to prevent 
  * the monthly account fee.
  */
  public static final int MINIMUM_BALANCE = 10000;

  /** The monthly account fee. */
  public static final int ACCOUNT_FEE = 500;

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

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

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

  /** Whether the account fee should be charged this month. */
  private boolean chargeFee;

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

  /**
   * Reassigns the transaction log to a new `StringBuilder` object that 
   * contains this month's initial account balance, and resets `chargeFee`.
   */
  private void resetTransactionLog() {
    this.chargeFee = this.balance < MINIMUM_BALANCE;
    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.
   */
  @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
  }

  /**
   * 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 with the 
   * given `memo`. Otherwise, no changes are made to either account and no 
   * transaction is logged. Updates `chargeFee` if `balance` drops below 
   * `MINIMUM_BALANCE`. Requires that `amount > 0`.
   */
  @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.chargeFee |= this.balance < MINIMUM_BALANCE;
    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, a
   * maintenance fee is applied to the account if the balance was ever below
   * `MINIMUM_BALANCE` during the month.
   */
  @Override
  public String transactionReport() {
    this.processMonthlyFee();
    this.transactions.append("Final Balance: ");
    this.transactions.append(centsToString(this.balance));
    this.transactions.append("\n");

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

  /**
   * Debits the monthly account fee if the balance fell below the 
   * minimum during the month.
   */
  private void processMonthlyFee() {
    if (this.chargeFee) {
      this.balance -= ACCOUNT_FEE;
      this.transactions.append(" - Withdraw ");
      this.transactions.append(centsToString(ACCOUNT_FEE));
      this.transactions.append(": Account Maintenance Fee");
      this.transactions.append("\n");
    }
  }
}
  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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/** Models a checking account in our personal finance app. */
public class CheckingAccount implements Account {
  /**
  * The balance must remain above this amount, in cents, to prevent 
  * the monthly account fee.
  */
  public static final int MINIMUM_BALANCE = 10000;

  /** The monthly account fee. */
  public static final int ACCOUNT_FEE = 500;

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

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

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

  /** Whether the account fee should be charged this month. */
  private boolean chargeFee;

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

  /**
   * Reassigns the transaction log to a new `StringBuilder` object that 
   * contains this month's initial account balance, and resets `chargeFee`.
   */
  private void resetTransactionLog() {
    this.chargeFee = this.balance < MINIMUM_BALANCE;
    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.
   */
  @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
  }

  /**
   * 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 with the 
   * given `memo`. Otherwise, no changes are made to either account and no 
   * transaction is logged. Updates `chargeFee` if `balance` drops below 
   * `MINIMUM_BALANCE`. Requires that `amount > 0`.
   */
  @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.chargeFee |= this.balance < MINIMUM_BALANCE;
    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, a
   * maintenance fee is applied to the account if the balance was ever below
   * `MINIMUM_BALANCE` during the month.
   */
  @Override
  public String transactionReport() {
    this.processMonthlyFee();
    this.transactions.append("Final Balance: ");
    this.transactions.append(centsToString(this.balance));
    this.transactions.append("\n");

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

  /**
   * Debits the monthly account fee if the balance fell below the 
   * minimum during the month.
   */
  private void processMonthlyFee() {
    if (this.chargeFee) {
      this.balance -= ACCOUNT_FEE;
      this.transactions.append(" - Withdraw ");
      this.transactions.append(centsToString(ACCOUNT_FEE));
      this.transactions.append(": Account Maintenance Fee");
      this.transactions.append("\n");
    }
  }
}

We see that a lot of the code is repeated. A lot of repetition is a “code smell”, an undesirable characteristic that suggests that we can improve our design. Having large blocks of repeated code increases the maintenance burden of our code, as changes to one of the copies will likely need to be made in the other copies as well. If we fail to make these modifications in one of the copies, it may cause inconsistent behaviors across the implementations, which increases the likelihood of bugs. We’d like a way to extract this common code into a single location and then access it from both the CheckingAccount and SavingsAccount classes. Inheritance provides this ability.

Definition: Inheritance, Subclass

Inheritance is an object-oriented programming tool that is used to establish a subclass relationship, a specialization of a subtype relationship, between two classes. A subclass S of a class T is a refinement of T that introduces new behaviors (and possibly new fields) within S while retaining the fields and methods from T.

Rather than modeling Account as an interface, we will model it as a class.

Account.java

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

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

}

Within the scope of this class, we can declare the fields and provide complete definitions of methods that are common to both account types. Then, we’ll adjust the CheckingAccount and SavingsAccount classes to remove this common code and instead inherit it from the Account class. We establish an inheritance (i.e., subclass) relationship using the extends keyword in Java.

SavingsAccount.java

1
2
3
4
/** Models a savings account in our personal finance app. */
public class SavingsAccount extends Account { 

}
1
2
3
4
/** Models a savings account in our personal finance app. */
public class SavingsAccount extends Account { 

}

In the following sections, we’ll walk through the process of writing code that involves inheritance, including new syntax and design considerations.

Defining the Superclass

Let’s start by defining the superclass (or base class, or parent class), Account. This class should model the state and behaviors that are common to all account types. Looking at the highlighted code in our earlier class definitions, we identify the common fields name, balance, and transactions. Almost every method, including all of the methods from our previous Account interface, the constructors, and private helper methods resetTransactionLog() and centsToString(), are either identical or contain a large proportion of shared behavior. Let’s extract this common code into the Account class. We’ll omit the visibility modifiers in this initial version, as this will be the next topic of discussion.

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
 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
 /** The base class for all account types in our personal finance app. */
public class Account {
  /** The name of this account. */
  String name;

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

  /** The current transaction report. */
  StringBuilder transactions;

  /**
   * Constructs an account with given `name` and initial `balance`.
   */
  Account(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.
   */
  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.
   */
  static String centsToString(int cents) {
    int dollars = cents / 100;
    cents = cents % 100;
    return ("$" + dollars + "." + (cents < 10 ? "0" : "") + cents);
  }

  /**
   * Return the name associated to this account.
   */
  String name() {
    return this.name;
  }

  /**
   * Returns the total balance, in cents, of the account.
   */
  int balance() {
    return this.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 (always true in the provided implementation), 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) {
    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;
  }

  /**
   * 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 with the 
   * given `memo`. Otherwise, no changes are made to either account and no 
   * transaction is logged. Requires that `amount > 0`.
   */
  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.
   */
  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
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
 /** The base class for all account types in our personal finance app. */
public class Account {
  /** The name of this account. */
  String name;

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

  /** The current transaction report. */
  StringBuilder transactions;

  /**
   * Constructs an account with given `name` and initial `balance`.
   */
  Account(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.
   */
  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.
   */
  static String centsToString(int cents) {
    int dollars = cents / 100;
    cents = cents % 100;
    return ("$" + dollars + "." + (cents < 10 ? "0" : "") + cents);
  }

  /**
   * Return the name associated to this account.
   */
  String name() {
    return this.name;
  }

  /**
   * Returns the total balance, in cents, of the account.
   */
  int balance() {
    return this.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 (always true in the provided implementation), 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) {
    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;
  }

  /**
   * 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 with the 
   * given `memo`. Otherwise, no changes are made to either account and no 
   * transaction is logged. Requires that `amount > 0`.
   */
  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.
   */
  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;
  }
}

protected Visibility

Previously, we have defined two types of visibility (beyond Java’s default visibility):

When we consider the possibility of inheritance, a third type of visibility is desirable. We would like a visibility that allows common private (to the client) code to be extracted into the superclass while remaining visible to its subclasses. In other words, we want a visibility modifier that only provides access within a class and its subclasses. These are the semantics of the protected modifier.

Definition: protected

The protected visibility modifier grants access to a field or method within the class where this member is defined and within all of its subclasses. In general, protected members are not visible to the client.

Remark:

There is a small caveat to the above definition; protected members are visible within any class in the same package where the field or method was defined. So far in the course, we haven't focused on packages, so we'll ignore this possibility in our subsequent discussion.

The addition of protected visibility creates a second “interface” that we must manage when we define a class, the specialization interface.

Definition: Specialization Interface

The specialization interface of a class includes all fields and methods of the class that are visible to implementers of its subclasses but not visible to other external classes (i.e., all protected fields and methods).

When we design a well-encapsulated class, we must carefully consider the visibility of each field and method in order to maintain both the client and specialization interfaces. In the end, it is the base class’ responsibility for managing its class invariant; it cannot delegate this to its subclasses or guarantee that they will respect it, so members should only be marked as protected when it will not compromise the class invariant and when it provides some additional utility to the subclasses. Let’s go through the members of the Account class now to decide their appropriate visibilities.

The name(), balance(), depositFunds(), transferFunds(), and transactionReport() methods make up the client interface of Accounts; these are the methods that can be accessed by our personal finance app, so they must be marked as public. To encapsulate our code, no other fields or methods should have public visibility.

The balance and report fields have a class invariant of consistency (documented in the method specifications). Whenever the client changes the value of balance, this is reflected through a transaction in report. If we gave a subclass direct access to these fields, they could modify their values in a way that was inconsistent with this invariant. Therefore, we’ll mark them as private to prevent this possibility. Note that the subclass can still access the value of balance through the public balance() accessor method; private visibility prevents write access to the variable. We’ll also mark name as private to maintain control over it in the Account class.

The resetTransactionLog() is a helper method that called from the constructor and from transactionReport(). A subclass should never call this method directly, as doing this could violate the class invariant; some transactions could disappear before being sent to the client. Thus, we’ll mark this method as private. However, it is important that this method is always called from transactionReport(), We’ll modify the specifications to ensure this.

The centsToString() method is a static helper method. It does not modify any fields, so it is safe to provide this as a convenience to any subclasses. We’ll mark it as protected.

We’ll revisit the visibility of the constructor later in the lecture.

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
 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
 /** The base class for all account types in our personal finance app. */
public class Account {
  /** The name of this account. */
  private String name;

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

  /** The current transaction report. */
  private StringBuilder transactions;

  /**
   * Constructs an account with given `name` and initial `balance`.
   */
  Account(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.
   */
  protected static String centsToString(int cents) {
    int dollars = cents / 100;
    cents = cents % 100;
    return ("$" + dollars + "." + (cents < 10 ? "0" : "") + cents);
  }

  /**
   * Return the name associated to this account.
   */
  public String name() {
    return this.name;
  }

  /**
   * Returns the total balance, in cents, of the account.
   */
  public int balance() {
    return this.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 (always true in the provided implementation), it 
   * is logged with the given `memo`. Otherwise, no changes are made to this 
   * account and no transaction is logged Requires that `amount > 0`.
   */
  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;
  }

  /**
   * 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 with the 
   * given `memo`. Otherwise, no changes are made to either account and no 
   * transaction is logged. Requires that `amount > 0`.
   */
  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. This base implementation must be called from 
   * all `Account` subclasses that override this method.
   */
  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
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
 /** The base class for all account types in our personal finance app. */
public class Account {
  /** The name of this account. */
  private String name;

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

  /** The current transaction report. */
  private StringBuilder transactions;

  /**
   * Constructs an account with given `name` and initial `balance`.
   */
  Account(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.
   */
  protected static String centsToString(int cents) {
    int dollars = cents / 100;
    cents = cents % 100;
    return ("$" + dollars + "." + (cents < 10 ? "0" : "") + cents);
  }

  /**
   * Return the name associated to this account.
   */
  public String name() {
    return this.name;
  }

  /**
   * Returns the total balance, in cents, of the account.
   */
  public int balance() {
    return this.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 (always true in the provided implementation), it 
   * is logged with the given `memo`. Otherwise, no changes are made to this 
   * account and no transaction is logged Requires that `amount > 0`.
   */
  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;
  }

  /**
   * 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 with the 
   * given `memo`. Otherwise, no changes are made to either account and no 
   * transaction is logged. Requires that `amount > 0`.
   */
  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. This base implementation must be called from 
   * all `Account` subclasses that override this method.
   */
  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;
  }
}

At this point, we have completed our initial implementation of the Account superclass and can shift our focus to its subclasses.

Defining Subclasses

SavingsAccount Subclass

Let’s start by defining the SavingsAccount subclass. Since SavingsAccount will extend Account, it inherits its fields and methods. We do not need to (nor can we) recreate the name or balance fields since these already exist within the Account class. We also do not need to take any action for name() or balance() methods since these already exist within the Account class and have the desired behavior. A client can invoke name() on a SavingsAccount object, and this will dispatch to Account.name(). As we design this subclass, we should ask ourselves “in what ways is a SavingsAccount different from an Account?” From the specifications, the primary difference is the support for account interest. Our previous SavingsAccount implementation had a rate field, and this field is not present in Account. We will need to add this to our new SavingsAccount implementation. We can visualize this new field in a slightly expanded object diagram that accounts for inheritance.

SavingsAccount objects have four fields, name, balance, transactions, and rate. In this diagram, we use a dashed line to separate the rate field (which was declared in the SavingsAccount class) from the other three fields (which were inherited from the Account class).

Our previous SavingsAccount implementation also had a public accessor method interestRate() and a private helper method accrueMonthlyInterest() that we will need to include. Note that we’ll need to make a small modification to the definition of accrueMonthlyInterest(). The old definition accessed this.balance, which was a field within the SavingsAccount class. Now that balance is a private field of the Account superclass, we cannot access it directly from this method. Instead, we’ll need to use the public accessor this.balance().

SavingsAccount.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/** Models a savings account in our personal finance app. */
public class SavingsAccount extends Account {
  /** The current (nominal) APR of this account. */
  private double rate;

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

  /**
   * 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
/** Models a savings account in our personal finance app. */
public class SavingsAccount extends Account {
  /** The current (nominal) APR of this account. */
  private double rate;

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

  /**
   * 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 + "%");
  }
}

We are not yet finished with the SavingsAccount implementation. We need a way to modify the implementation of transactionReport() to add the interest payment. We’ll also need to provide a new SavingsAccount constructor that will accept an interest rate parameter from the client.

Method Overrides and super

When we implement an interface, we use the @Override syntax to mark that a method was declared in the interface. In other words, we are overriding the method’s signature by providing a definition of this method that is specific to the class we are defining. We can do the same thing in a subclass.

Definition: Override

When we override a method from a superclass, we declare a method with the same signature within the subclass.

As we will soon discuss, this subclass version of the method will take precedence over the superclass definition; calling this method on a subclass object (i.e., an object whose dynamic type is the subclass) from within the client code will lead to the subclass version of the method being executed. Thus, overriding provides us a mechanism to modify the superclass behaviors.

In our example, the SavingsAccount class must override the transactionReport() method to add a call to accrueMonthlyInterest(). However, after this, we want the method to proceed exactly as in the superclass. We can do this using the keyword super followed by the method invocation. Writing super.transactionReport() tells Java “now, invoke the transactionReport() method that was defined in my superclass on me.” We achieve our desired behavior with the definition:

SavingsAccount.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
 * 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();
  return super.transactionReport();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
 * 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();
  return super.transactionReport();
}

For the SavingsAccount constructor, we’ll need to initialize the rate field directly (as this was defined in the SavingsAccount class). However, the remaining fields must be initialized by the Account constructor. We again access this using the super keyword, this time without a method name. Java requires that the first line in any constructor is a call to a superclass constructor, so we arrive at the constructor definition:

SavingsAccount.java

1
2
3
4
5
6
7
8
/**
 * Constructs a savings account with given `name`, initial `balance`, and 
 * interest `rate`.
 */
public SavingsAccount(String name, int balance, double rate) {
    super(name, balance);
    this.rate = rate;
}
1
2
3
4
5
6
7
8
/**
 * Constructs a savings account with given `name`, initial `balance`, and 
 * interest `rate`.
 */
public SavingsAccount(String name, int balance, double rate) {
    super(name, balance);
    this.rate = rate;
}
Remark:

When you do not include an explicit call to the superclass constructor, Java will automatically add an implicit call to super(), the default, no-arguments constructor of the superclass. This will suffice when there is such a default constructor (e.g., when no constructor is explicitly defined in the superclass). However, if the superclass does not have a default constructor, you are required to write an explicit superclass constructor call, meaning you are required to write a constructor for your subclass.

Altogether, our new SavingsAccount definition that leverages inheritance is much shorter than our implementation that implemented an interface. Most of the work has been delegated to the Account superclass. Within the SavingsAccount class, we only need to handle interest, the unique feature of savings accounts.

CheckingAccount Subclass

Now, let’s use similar ideas to define the CheckingAccount subclass. In this case, the feature that is unique to a CheckingAccount is the potential monthly fee, and we can provide support for this feature by doing the following.

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
/** Models a checking account in our personal finance app. */
public class CheckingAccount extends Account {
  /** 
   * The balance must remain above this amount, in cents, to prevent the 
   * monthly account fee. 
   */
  public static final int MINIMUM_BALANCE = 10000;

  /** The monthly account fee. */
  public static final int ACCOUNT_FEE = 500;

  /**
   * Whether the account fee should be charged this month. Will be `false` 
   * unless `balance()` ever was below MINIMUM_BALANCE since the previous 
   * call to `transactionReport()`
   */
  private boolean chargeFee;

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

  /**
   * 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 with the 
   * given `memo`. Otherwise, no changes are made to either account and no 
   * transaction is logged. Updates `chargeFee` if `balance` drops below 
   * `MINIMUM_BALANCE`. Requires that `amount > 0`.
   */
  @Override
  public boolean transferFunds(Account receivingAccount, int amount) {
    boolean success = super.transferFunds(receivingAccount, amount);
    if (success && this.balance() < MINIMUM_BALANCE) {
      this.chargeFee = true;
    }
    return success;
  }

  /**
   * 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, a 
   * maintenance fee is applied to the account if the balance was ever below 
   * `MINIMUM_BALANCE` during the month.
   */
  @Override
  public String transactionReport() {
    this.processMonthlyFee();
    this.chargeFee = this.balance() < MINIMUM_BALANCE; // reset `chargeFee` for the new month
    return super.transactionReport();
  }

  /**
   * Debits the monthly account fee if the balance fell below the minimum 
   * during the month.
   */
  private void processMonthlyFee() {
    if (this.chargeFee) {
      this.balance -= ACCOUNT_FEE;
      this.transactions.append(" - Withdraw ");
      this.transactions.append(centsToString(ACCOUNT_FEE));
      this.transactions.append(": Account Maintenance Fee");
      this.transactions.append("\n");
    }
  }
}
 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
/** Models a checking account in our personal finance app. */
public class CheckingAccount extends Account {
  /** 
   * The balance must remain above this amount, in cents, to prevent the 
   * monthly account fee. 
   */
  public static final int MINIMUM_BALANCE = 10000;

  /** The monthly account fee. */
  public static final int ACCOUNT_FEE = 500;

  /**
   * Whether the account fee should be charged this month. Will be `false` 
   * unless `balance()` ever was below MINIMUM_BALANCE since the previous 
   * call to `transactionReport()`
   */
  private boolean chargeFee;

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

  /**
   * 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 with the 
   * given `memo`. Otherwise, no changes are made to either account and no 
   * transaction is logged. Updates `chargeFee` if `balance` drops below 
   * `MINIMUM_BALANCE`. Requires that `amount > 0`.
   */
  @Override
  public boolean transferFunds(Account receivingAccount, int amount) {
    boolean success = super.transferFunds(receivingAccount, amount);
    if (success && this.balance() < MINIMUM_BALANCE) {
      this.chargeFee = true;
    }
    return success;
  }

  /**
   * 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, a 
   * maintenance fee is applied to the account if the balance was ever below 
   * `MINIMUM_BALANCE` during the month.
   */
  @Override
  public String transactionReport() {
    this.processMonthlyFee();
    this.chargeFee = this.balance() < MINIMUM_BALANCE; // reset `chargeFee` for the new month
    return super.transactionReport();
  }

  /**
   * Debits the monthly account fee if the balance fell below the minimum 
   * during the month.
   */
  private void processMonthlyFee() {
    if (this.chargeFee) {
      this.balance -= ACCOUNT_FEE;
      this.transactions.append(" - Withdraw ");
      this.transactions.append(centsToString(ACCOUNT_FEE));
      this.transactions.append(": Account Maintenance Fee");
      this.transactions.append("\n");
    }
  }
}

There is an issue with the code as we have written it. Take some time to try to identify this issue before reading on.

Within the processMonthlyFee() helper method, we are currently decrementing this.balance -= ACCOUNT_FEE. However, balance is a private member of the Account superclass, so we don’t have visibility to this field. Similarly, on the following lines, we try to call append() on this.transaction, which is also a private member of Account. Recall that we disallowed direct access to these fields to protect the Account class invariant. Therefore, the modification to these fields will need to happen within the Account class.

We can add a method withdrawFunds() method to Account that parallels depositFunds() to handle the withdrawal of the account fee. We don’t want to change the client interface of Account (although an argument can be made that this method would allow us to support a useful feature to our application), and we need to access this method from CheckingAccount, so it must be marked as protected. The definition is given below.

Account.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/**
 * Attempts to withdraw the specified `amount`, in cents, from the balance of 
 * the account, and returns whether this transaction was successful. If this 
 * transaction is successful (always true in the provided implementation), it 
 * is logged with the given `memo`. Otherwise, no changes are made to this 
 * account and no transaction is logged. Requires that `amount > 0`.
 */
protected boolean withdrawFunds(int amount, String memo) {
    assert amount > 0;
    this.balance -= amount;
    this.transactions.append(" - Withdraw ");
    this.transactions.append(centsToString(amount));
    this.transactions.append(": ");
    this.transactions.append(memo);
    this.transactions.append("\n");
    return true;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/**
 * Attempts to withdraw the specified `amount`, in cents, from the balance of 
 * the account, and returns whether this transaction was successful. If this 
 * transaction is successful (always true in the provided implementation), it 
 * is logged with the given `memo`. Otherwise, no changes are made to this 
 * account and no transaction is logged. Requires that `amount > 0`.
 */
protected boolean withdrawFunds(int amount, String memo) {
    assert amount > 0;
    this.balance -= amount;
    this.transactions.append(" - Withdraw ");
    this.transactions.append(centsToString(amount));
    this.transactions.append(": ");
    this.transactions.append(memo);
    this.transactions.append("\n");
    return true;
}

We can utilize this method to update our processMonthlyFee() helper method in the CheckingAccount class as follows:

CheckingAccount.java

1
2
3
4
5
6
7
8
9
/**
 * Debits the monthly account fee if the balance fell below the minimum during 
 * the month.
 */
private void processMonthlyFee() {
    if (this.chargeFee) {
        this.withdrawFunds(ACCOUNT_FEE, "Account Maintenance Fee");
    }
}
1
2
3
4
5
6
7
8
9
/**
 * Debits the monthly account fee if the balance fell below the minimum during 
 * the month.
 */
private void processMonthlyFee() {
    if (this.chargeFee) {
        this.withdrawFunds(ACCOUNT_FEE, "Account Maintenance Fee");
    }
}

This restructuring, while primarily aimed at addressing a visibility error in our original implementation, also provides a much clearer division of responsibilities between the CheckingAccount and Account classes; CheckingAccount tracks the account balance to determine whether the fee is charged, and Account is responsible for actually charging and reporting the fee. This demonstrates some of the subtleties involved in designing classes that leverage inheritance. Thinking carefully about the visibility of the fields and methods helps us to end up with a well-encapsulated design with a clear separation of roles. Often, this requires us to rethink and refactor our code as we are developing it.

As a final bit of refactoring for now, we can rewrite our Account.transferFunds() method to make use of the new withdrawFunds() method and cut down on code duplication.

Account.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
 * 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 with the given 
 * `memo`. Otherwise, no changes are made to either account and no transaction 
 * is logged. Requires that `amount > 0`.
 */
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.withdrawFunds(amount, "Transfer to " + receivingAccount.name);
  return true;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
 * 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 with the given 
 * `memo`. Otherwise, no changes are made to either account and no transaction 
 * is logged. Requires that `amount > 0`.
 */
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.withdrawFunds(amount, "Transfer to " + receivingAccount.name);
  return true;
}

Type Hierarchies

We can model subclass relationships in type hierarchies in a similar manner to subtype relationships that arise from implementing an interface. We use a solid arrows rather than dashed arrow to model inheritance from a (concrete) superclass. In addition, “Account” is no longer italicized in the diagram since it is now a class instead of an interface. The new hierarchy for our account classes is

Java is an object-oriented language with single inheritance, meaning almost every class extends exactly one superclass.

Definition: Single Inheritance

In an object-oriented programming language with single inheritance, such as Java, essentially every class has exactly one superclass.

This can allow us to create very deep type hierarchies, when a class extends another class, which itself extends a class, etc.

If we continue to follow these superclass relationships upward in the type hierarchy, we will eventually reach the top “super-est of all classes”, the Object class. The Object class defines a small set of behaviors that are common to all reference types. We will explore these in more detail as they become relevant in the course. Object is the only exception to the single inheritance rule; as the top of the hierarchy, Object does not have a superclass. When a class does not explicitly extend another class, its implicit superclass is Object.

Single inheritance has some great benefits. As we will soon discuss, it helps to provide an unambiguous procedure for reasoning about method dispatches (which “version” of a method is actually executed when a particular call is made) at runtime. This contrasts with multiple inheritance, supported by some earlier programming languages such as C++, which introduces some subtle complications (e.g., the diamond problem in type hierarchies).

Inheritance vs. Interfaces

As we have seen, inheritance offers us a convenient way to cut down on repeated code by extracting common behaviors into a superclass. For this reason, inheritance is often a popular choice for beginning developers. However, it is often not the ideal design choice because of Java’s single inheritance limitation. We are only allowed to select one class as our superclass, meaning we can only inherit code from one class (and its superclasses). This means that when we choose to model a subtype with inheritance, we “tie our hands” and prevent the inheritance of code from any other class. We should only do this when the benefits that arise from code reuse outweigh the rigidity of this choice. Interfaces are more flexible in this regard, and more often than not will be our choice for modeling subtype relationships in CS 2110. There is no limit on the number of interfaces that a class may implement.

Remember, interfaces simply model the availability of particular methods, so conflicts do not arise if a class implements multiple. Instead, additional interfaces just increase the responsibilities on the class implementer to include all of the required method definitions. In this way, interfaces provide a more flexible way to enable polymorphism; one can implement an interface to leverage polymorphism without sacrificing the ability to select a superclass.

Abstract Classes

There is a third construct in Java that combines features of classes and interfaces, an abstract class. To declare an abstract class, we modify the class declaration with the abstract keyword, as in

1
public abstract class Account { ... }
1
public abstract class Account { ... }

The word abstract hints at modeling the idea or concept of a type without actually instantiating it. This intuition helps to explain the two primary features of abstract classes.

  1. A client cannot construct objects whose dynamic type is an abstract class. If we make our Account class abstract, then clients can no longer access the Account() constructor to directly create Account objects. Instead, they would need to construct an object of an Account subtype (such as a SavingsAccount or a CheckingAccount) which can then call an Account superconstructor. In this way, abstract classes are similar to interfaces, which also cannot be instantiated.

  2. Within an abstract class, one can declare abstract methods. Similar to an interface, abstract methods only declare a method signature, but delegate the responsibility of providing the method definition to their subclasses. A non-abstract (i.e., concrete) class cannot have abstract methods. If a subclass of an abstract class does not provide a definition for an inherited abstract method, it must also be declared as abstract.

Abstract classes offer a way to delegate some responsibility to the subclass implementer while still offering a mechanism to extract common code into a superclass. They are good for modeling general categories of types that are not yet specific enough to be realized but are still specific enough to have some common fields and behaviors. In this way, our Account class is a good candidate to be abstract. Every bank account has a type, whether this is a checking account, a savings account or something else, that gives it some unique characteristics. It does not make sense to instantiate a general “account” object without it belonging to one of these subtypes. We’ll see some advantages of this approach in the next section.

As classes, abstract classes are still bound by the single inheritance rule. A class still may only choose one superclass, even if this class is abstract. For this reason, an interface may be preferable if there are not enough shared fields or shared method definitions to warrant establishing an inheritance relationship.

Refactoring our Accounts Example

Let us mark our Account class as abstract. When we do this, a natural choice of visibility modifier for the Account() constructor is protected. This way, the constructor is still accessible to its subclasses (which will need to invoke it) but not to any other classes.

Remark:

Java permits abstract classes to have public constructors, even though being abstract precludes outside classes from calling these constructors (making them function as if they were protected). I feel it's clearer to mark these constructors as protected to better reflect their semantics.

Next, we might consider whether it makes sense to mark any methods as abstract. As written, all of the Account methods provide reasonable implementations of their behaviors that are leveraged in the Account subclasses. This suggests that making these methods abstract would not be beneficial. However, we can note one “code smell” in our current implementation. The transactionReport() method is overridden in both subclasses and contains an awkward specification that it must be called from within the subclasses. This places a burden on the subclass implementer and can violate the Account invariant if not handled correctly. Can we refactor things to make this better?

The reason for the transactionReport() overrides is to process some end-of-month transactions that are unique to each account type (interest for savings accounts, fees for checking accounts). Let’s instead define a closeOutMonth() method in the Account class that is called from the Account.transactionReport() where these actions can take place. We’ll mark this method as abstract, since there are no “default” end-of-month actions that all accounts have. Instead, we want to delegate responsibility for modeling end-of-month actions to the subclass implementer. We’ll give this method protected visibility so that it is not exposed in the client interface but is present in the specialization interface.

Account.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 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.
 */
public final String transactionReport() {
  this.closeOutMonth();
  this.transactions.append("Final Balance: ");
  this.transactions.append(centsToString(this.balance));
  this.transactions.append("\n");

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

/**
 * Performs any necessary end-of-month tasks for this account immediately 
 * before the transaction report is finalized and returned to the client.
 */
protected abstract void closeOutMonth();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 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.
 */
public final String transactionReport() {
  this.closeOutMonth();
  this.transactions.append("Final Balance: ");
  this.transactions.append(centsToString(this.balance));
  this.transactions.append("\n");

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

/**
 * Performs any necessary end-of-month tasks for this account immediately 
 * before the transaction report is finalized and returned to the client.
 */
protected abstract void closeOutMonth();

Notice here that we also added the final keyword to the transactionReport() method. This prevents this method from being overridden in any Account subclasses, allowing the compiler (and dynamic dispatch, as we will discuss shortly) to automatically enforce our cumbersome “this base implementation must be called” constraint.

Now, we can remove the transactionReport() overrides in the SavingsAccount and CheckingAccount classes and refactor the code from these and their private helper methods to closeOutMonth().

SavingsAccount.java

1
2
3
4
5
6
7
8
/**
 * Adds the monthly interest payment to the account balance.
 */
@Override
protected void closeOutMonth() {
  int interestAmount = (int) (this.balance() * this.rate / (12 * 100));
  this.depositFunds(interestAmount, "Monthly interest @" + this.rate + "%");
}
1
2
3
4
5
6
7
8
/**
 * Adds the monthly interest payment to the account balance.
 */
@Override
protected void closeOutMonth() {
  int interestAmount = (int) (this.balance() * this.rate / (12 * 100));
  this.depositFunds(interestAmount, "Monthly interest @" + this.rate + "%");
}

CheckingAccount.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
 * Debits the monthly account fee if the balance fell below the minimum during the month.
 */
 @Override
protected void closeOutMonth() {
  if (this.chargeFee) {
    this.withdrawFunds(ACCOUNT_FEE, "Account Maintenance Fee");
  }
  this.chargeFee = this.balance() < MINIMUM_BALANCE; // reset `chargeFee` for the new month
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
 * Debits the monthly account fee if the balance fell below the minimum during the month.
 */
 @Override
protected void closeOutMonth() {
  if (this.chargeFee) {
    this.withdrawFunds(ACCOUNT_FEE, "Account Maintenance Fee");
  }
  this.chargeFee = this.balance() < MINIMUM_BALANCE; // reset `chargeFee` for the new month
}

Our final implementation of our account classes is provided as part of the lecture source code. You are highly encouraged to download, play around with, and expand this code to understand its design decisions. Some avenues for explanation are suggested in the lecture exercises.

Execution with Inheritance

To wrap up this lecture, let’s revisit some of the topics from last lecture, the compile-time reference rule and dynamic dispatch, to see how they interact with inheritance.

The compile-time reference rule works exactly as before, just with our modified understanding of a class’ members. Recall that we may only access fields and invoke methods on a variable that are declared for its static type. In an inheritance relationship, the subclass inherits fields and methods of its superclass. This means that even if a method is not declared within a class, it still may belong to that class’ type. For example, the client code,

1
2
CheckingAccount c = new CheckingAccount("Checking",55000);
System.out.print(c.name());
1
2
CheckingAccount c = new CheckingAccount("Checking",55000);
System.out.print(c.name());

compiles despite the fact that the method name() is not declared within the CheckingAccount class. Since the name() method is inherited from the Account superclass, the CheckingAccount type still supports this behavior. Practically, this means that the compiler must traverse upward through the inheritance hierarchy to check for (both the availability and visibility of) a requested field or method.

As we saw earlier, a subclass can override a method (which is not marked as final) that is declared/defined in its superclass (or some other class even higher in its inheritance hierarchy). This allows the subclass to refine the behavior of the method to support more specific behaviors and/or maintain its own enhanced class invariant, as long as it still satisfies the specifications of the original method. To ensure that objects maintain all of their invariants, the overridden method definitions have precedence over their superclass definitions. This means that at runtime, every method dispatch must begin at the dynamic type of the target and proceed upward in the type hierarchy only until a suitable method signature is found. We sometimes refer to these refined semantics of dynamic dispatch as the bottom-up rule.

Definition: Bottom-Up Rule

The bottom-up rule says that when a method is invoked on a target, Java will search for that method's definition beginning with the dynamic type of the target and then proceeding upward in the type hierarchy until the correct method signature is found.

The one time that this dispatch does not begin at the dynamic type is when a method is invoked with the super keyword. In this case, Java begins the method dispatch in the superclass of the class where the method was invoked, and then continues to proceed up the type hierarchy.

Let’s practice these semantics by tracing through the method dispatches during the execution of some client code in our final refactoring of the code utilizing an abstract Account class. As a reference, we include snippets of the relevant code from each of our three account classes.

Account.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class Account {
    public Account(String name, int balance) { ... }
    private void resetTransactionLog() { ... }
    public boolean depositFunds(int amount, String memo) { ... }
    protected boolean withdrawFunds(int amount, String memo) { ... }

    public boolean transferFunds(Account receivingAccount, int amount) {
        if (!receivingAccount.depositFunds(...)) { ... }
        this.withdrawFunds(...);
        // ...
    }

    public final String transactionReport() {
        this.closeOutMonth();
        // ...
        this.resetTransactionLog();
        // ...
    }

    protected abstract void closeOutMonth();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class Account {
    public Account(String name, int balance) { ... }
    private void resetTransactionLog() { ... }
    public boolean depositFunds(int amount, String memo) { ... }
    protected boolean withdrawFunds(int amount, String memo) { ... }

    public boolean transferFunds(Account receivingAccount, int amount) {
        if (!receivingAccount.depositFunds(...)) { ... }
        this.withdrawFunds(...);
        // ...
    }

    public final String transactionReport() {
        this.closeOutMonth();
        // ...
        this.resetTransactionLog();
        // ...
    }

    protected abstract void closeOutMonth();
}

SavingsAccount.java

1
2
3
4
5
6
public class SavingsAccount extends Account {
    public SavingsAccount(String name, int balance, double rate) {
        super(name, balance);
        // ...
    }
}
1
2
3
4
5
6
public class SavingsAccount extends Account {
    public SavingsAccount(String name, int balance, double rate) {
        super(name, balance);
        // ...
    }
}

CheckingAccount.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class CheckingAccount extends Account {
    public CheckingAccount(String name, int balance) {
        super(name, balance);
        // ...
    }
    
    @Override
    public boolean transferFunds(Account receivingAccount, int amount) {
        boolean success = super.transferFunds(...);
        // ...
    }

    @Override
    protected void closeOutMonth() {
        if (this.chargeFee) { this.withdrawFunds(...); }
        // ...
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class CheckingAccount extends Account {
    public CheckingAccount(String name, int balance) {
        super(name, balance);
        // ...
    }
    
    @Override
    public boolean transferFunds(Account receivingAccount, int amount) {
        boolean success = super.transferFunds(...);
        // ...
    }

    @Override
    protected void closeOutMonth() {
        if (this.chargeFee) { this.withdrawFunds(...); }
        // ...
    }
}

previous

next

This animation demonstrates many of the intricacies that arise when we leverage dynamic dispatch and inheritance in our code. It is important that you are comfortable tracing through executions similar to the one above, as we will make extensive use of these object-oriented language features when we start to design data structures in upcoming lectures. The lecture exercises provide more opportunities to practice these concepts.

Main Takeaways:

  • Inheritance provides a way to reduce code duplication by extracting common fields and behaviors into a superclass that are inherited by its subclasses. Inheritance relationships are established using the extends keyword in Java.
  • The protected visibility modifier makes a field or method visible to subclasses but invisible to external client code. The protected members of a class make up its specialization interface while its public members make up its client interface. Good class design requires careful consideration of what members belong to each of these.
  • A subclass can override methods of its superclass to refine their behaviors. The super keyword provides access to overridden superclass implementations from within the class.
  • A type hierarchy summarizes the subtype relationships between different classes and interfaces. Java has single inheritance, so each class (besides Object) has exactly one superclass, but may implement multiple interfaces. Single inheritance makes subclassing a very rigid mechanism, so it should be used sparingly.
  • abstract classes cannot be instantiated directly, but can declare abstract methods which delegate their definition to a (concrete) subclass.
  • The bottom-up rule says that the dispatch of every method call begins at the target's dynamic type and proceeds upward in the type hierarchy until a suitable method definition is found.

Exercises

Exercise 10.1: Check Your Understanding
(a)

Consider the following family of classes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Parent {
  public int foo(int x) { /* Parent impl */ }
  public int bar(int x) {
    return foo(2 * x + 1);
  }
}

public class Derived extends Parent {
  @Override
  public int foo(int x) { /* Derived impl */ }
}

public class Child extends Parent {
  @Override
  public int bar(int x) {
    return super.foo(x - 1);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Parent {
  public int foo(int x) { /* Parent impl */ }
  public int bar(int x) {
    return foo(2 * x + 1);
  }
}

public class Derived extends Parent {
  @Override
  public int foo(int x) { /* Derived impl */ }
}

public class Child extends Parent {
  @Override
  public int bar(int x) {
    return super.foo(x - 1);
  }
}
1
2
Parent p = new Derived();
int ans = p.bar(42);
1
2
Parent p = new Derived();
int ans = p.bar(42);
Which blocks of code are executed when the above client code is run?
Check Answer
(b)

Consider the classes below. We show only the parts of the classes that mention the name() method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class A {
  public String name() {
    return "A";
  }
} 

public class B extends A { ... } // class B does not override name()

public class C extends B {
  @Override
  public String name() {
    return "C" + EXP;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class A {
  public String name() {
    return "A";
  }
} 

public class B extends A { ... } // class B does not override name()

public class C extends B {
  @Override
  public String name() {
    return "C" + EXP;
  }
}
EXP should be a call that executes the body of name() as implemented in class A. Is this possible? If so, how should EXP be written?
Check Answer
(c)

Consider the two classes shown below. Assume that elided implementations comply with their specifications.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class A {
  private String title;

  /** Constructs an instance with title `s`. */
  public A(String s) { ... }
}

public class B extends A {
  private String name;

  public B(String n, String s) {
    name = n;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class A {
  private String title;

  /** Constructs an instance with title `s`. */
  public A(String s) { ... }
}

public class B extends A {
  private String name;

  public B(String n, String s) {
    name = n;
  }
}
Choose the answer that correctly states what should be done to make the constructor for class B correct.
Check Answer
(d)
1
2
3
4
5
6
7
8
9
public class Pet {
  public String name;
  protected double taillength;
  private String sound;
  protected void setSound(String sound) {
    this.sound = sound;
  }
  private boolean checkInvariant() { ... }
}
1
2
3
4
5
6
7
8
9
public class Pet {
  public String name;
  protected double taillength;
  private String sound;
  protected void setSound(String sound) {
    this.sound = sound;
  }
  private boolean checkInvariant() { ... }
}
Given the following class definition, which of the following members are part of the specialization interface but not the client interface?
Check Answer
Which of the following members are part of the client interface?
Check Answer
Exercise 10.2: Extending Accounts
Refactor your CD and InvestmentAccount subtype classes from Exercise 9.2 so that they still have the correct behavior now that Account is a class (as opposed to an interface). Remove as much redundancy or other "code smells" as you can.
Exercise 10.3: Concrete Class vs. Abstract Class vs. Interfaces
In Java, there are several ways that we can define subtype relationships: extending concrete or abstract classes and implementing interfaces.
(a)
What are the key differences between concrete and abstract classes? Why would a developer use one over the other?
(b)
Both abstract classes and interfaces can declare methods that subtypes must implement. With default methods (see Exercise 9.6 in Lecture 9) allowing interfaces to provide default implementations, why would a developer use an abstract class over an interface?
(c)
Suppose you want to define a supertype Herbivore to mark classes with graze() behavior and a supertype Nocturnal to mark classes with useNightVision(). You anticipate defining a Quokka class that is both nocturnal and an herbivore. Should Herbivore and Nocturnal be abstract classes or interfaces?
Exercise 10.4: private abstract Methods
Java forbids you from defining abstract methods as private. Try defining one in IntelliJ. Explain why this is a reasonable restriction.
Exercise 10.5: Overriding Functions Methods
We want to define an abstract class to represent math functions.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/** Represents a mathematical function. */
public abstract class MathFunction {
  // ... some fields, including `name`

  /** Constructs a function with name `n`. */
  public MathFunction(String n) {
    name = n;
  }

  /** 
   * Evaluates the function at a given `x` value. Requires `x` is in the 
   * domain of this function.
   */
  public abstract double evaluate(double x);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/** Represents a mathematical function. */
public abstract class MathFunction {
  // ... some fields, including `name`

  /** Constructs a function with name `n`. */
  public MathFunction(String n) {
    name = n;
  }

  /** 
   * Evaluates the function at a given `x` value. Requires `x` is in the 
   * domain of this function.
   */
  public abstract double evaluate(double x);
}
For each of the following subclasses, override evaluate and refine the specifications.
(a)

Given the slope and y-intercept, \( m, b \in \mathbb{R} \), a linear function is defined as

\[ f(x) = mx + b. \]
1
public class LinearFunction extends MathFunction { ... }
1
public class LinearFunction extends MathFunction { ... }
(b)

Given a degree \( n \in \mathbb{N} \) and coefficients \( a_0, a_1, \ldots, a_n \in \mathbb{R} \), a polynomial function is defined as

\[ f(x) = a_nx^n + a_{n-1}x^{n-1} + \ldots + a_1x+a_0=\sum_{i=0}^na_ix^i. \]
1
public class PolynomialFunction extends MathFunction { ... }
1
public class PolynomialFunction extends MathFunction { ... }
(c)

Given base \( b \in \mathbb{R} \) with \( b > 0 \) and \( b\neq 1 \), a logarithmic function is defined as

\[ f(x) = \log_b(x). \]

Java’s Math class has no method to compute the logarithm of a number with an arbitrary base. However, you might find the following fact useful, where \( a > 0 \) and \( a \neq 1 \).

\[ \log_b(x)= \frac{\log_a(x)}{\log_a(b)} \]
1
public class LogarithmicFunction extends MathFunction { ... }
1
public class LogarithmicFunction extends MathFunction { ... }
Exercise 10.6: Type Hierarchy
Analyze the following set of interfaces and classes.
1
2
3
4
5
6
7
public interface Payable {}
public interface Refundable {}
public class Cash implements Payable {}
public class CreditCard implements Payable, Refundable {}
public interface Online {}
public class DigitalWallet extends CreditCard implements Refundable, Online {}
public class SuperWallet extends DigitalWallet {}
1
2
3
4
5
6
7
public interface Payable {}
public interface Refundable {}
public class Cash implements Payable {}
public class CreditCard implements Payable, Refundable {}
public interface Online {}
public class DigitalWallet extends CreditCard implements Refundable, Online {}
public class SuperWallet extends DigitalWallet {}
(a)
Draw a type hierarchy diagram of the above interfaces and classes. Also, include the Object class that should have a supertype relationship of any class.
(b)
We want to complete the following statement: SuperWallet <: X. Which class or interfaces in the diagram can we substitute in for X to make this statement true?
(c)
Is subtyping transitive? That is, if R <: S and S <: T, then R <: T.
Exercise 10.7: Dynamic Dispatch and Compile-Time Reference Rule (Reprise)
Suppose we define the following chess-inspired classes:
 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
public abstract class Piece {
  private int row;
  private int col;
  public boolean legalMove() {
    System.out.println("Piece's legalMove()");
    // ...
  }
  public void moveTo() {
    System.out.println("Piece's moveTo()");
    legalMove();
  }
}

public class Pawn extends Piece {
  @Override
  public boolean legalMove() {
    System.out.println("Pawn's legalMove()");
    // ...
  }
  @Override
  public void moveTo() {
    System.out.println("Pawn's moveTo()");
    super.moveTo();
  }
}

public class PromotedPawn extends Pawn {
  public void promote() {
    System.out.println("Pawn promoted");
  }
}
 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
public abstract class Piece {
  private int row;
  private int col;
  public boolean legalMove() {
    System.out.println("Piece's legalMove()");
    // ...
  }
  public void moveTo() {
    System.out.println("Piece's moveTo()");
    legalMove();
  }
}

public class Pawn extends Piece {
  @Override
  public boolean legalMove() {
    System.out.println("Pawn's legalMove()");
    // ...
  }
  @Override
  public void moveTo() {
    System.out.println("Pawn's moveTo()");
    super.moveTo();
  }
}

public class PromotedPawn extends Pawn {
  public void promote() {
    System.out.println("Pawn promoted");
  }
}
For each of the following, identify if there is a compile-time, run-time, or no error. If you believe that there is an error, explain why.
(a)
1
2
3
4
5
6
public class Pawn extends Piece {
  // ... 
  public void advance() {
    super.col++;
  }
}
1
2
3
4
5
6
public class Pawn extends Piece {
  // ... 
  public void advance() {
    super.col++;
  }
}
(b)
1
2
Piece p = new Pawn();
((PromotedPawn) p).promote();
1
2
Piece p = new Pawn();
((PromotedPawn) p).promote();
(c)
1
2
Piece p = new Pawn();
p.moveTo();
1
2
Piece p = new Pawn();
p.moveTo();