© Edward Sciore 2019
Edward ScioreJava Program Designhttps://doi.org/10.1007/978-1-4842-4143-1_3

3. Class Hierarchies

Edward Sciore1 
(1)
Newton, MA, USA
 

Chapter 2 examined how interfaces can extend other interfaces, creating a hierarchy of types. One of the characteristics of an object-oriented language is that classes can extend other classes, creating a class hierarchy. This chapter investigates class hierarchies and the ways they can be used effectively.

Subclasses

Java allows one class to extend another. If class A extends class B, then A is said to be a subclass of B and B is a superclass of A. Subclass A inherits all public variables and methods of its superclass B, as well as all of B’s code for these methods.

The most common example of subclassing in Java is the built-in class Object. By definition, every class in Java is a subclass of Object. That is, the following two class definitions are equivalent:
   class Bank { ... }
   class Bank extends Object { ... }
Consequently, the methods defined by Object are inherited by every object. Of these methods, two commonly-used ones are equals and toString. The equals method returns true if the two references being compared are to the same object. (That is, the method is equivalent to the “==” operation.) The toString method returns a string describing the object’s class and its location in memory. Listing 3-1 demonstrates these methods.
Object x = new Object();
Object y = new Object();
Object z = x;
boolean b1 = x.equals(y); // b1 is false
boolean b2 = x.equals(z); // b2 is true
System.out.println(x.toString());
// prints something like "java.lang.Object@42a57993"
Listing 3-1

Demonstrating the Default Equals Method

A class can choose to override an inherited method. Often the code provided by a superclass is too generic and a subclass may be able to override the method with more appropriate code. The toString method is typically overridden. For example, the Bank, SavingsAccount, and CheckingAccount classes in the version 6 banking demo override toString.

It is also typical to override the equals method. A class that overrides the equals method typically compares the states of the two objects to determine whether they denote the same real-world thing. For example, consider the class SavingsAccount. Assuming that savings accounts have distinct account numbers, two SavingsAccount objects should be equal if their account numbers are the same. However, consider the following code.
   SavingsAccount s1 = new SavingsAccount(123);
   SavingsAccount s2 = new SavingsAccount(123);
   boolean b = s1.equals(s2); // returns false
Since s1 and s2 refer to different objects, comparing them using the default equals method will return false. If you want the equals method to return true in this case then SavingsAccount needs to override it. See Listing 3-2.
boolean equals(Object obj) {
   if (! obj instanceof SavingsAccount)
      return false;
   SavingsAccount sa = (SavingsAccount) obj;
   return getAcctNum() == sa.getAcctNum();
}
Listing 3-2

The Version 6 Equals Method of SavingsAccount

This code is probably trickier than you expected. The reason is that the argument to the default equals method has the type Object, which means that any class that overrides equals must also declare its argument to be of type Object. That is, the equals method for SavingsAccount must handle the possibility of a client comparing a SavingsAccount object to an object from some other class. The code of Listing 3-2 surmounts this problem by using instanceof and typecasting, as in Chapter 2. If the argument is not a savings account then the method immediately returns false. Otherwise, it casts the argument to the type SavingsAccount and compares their account numbers.

A method defined in class Object never needs to be declared in an interface. For example, consider the following code.
   BankAccount ba = new SavingsAccount(123);
   String s = ba.toString();

This code is legal regardless of whether or not the BankAccount interface declares the toString method , because every implementing class will inherit toString from Object if it has not been otherwise overridden. However, there is still a value in having the interface declare toString—it requires each of its implementing classes to override the method explicitly.

To represent a class–superclass relationship in a class diagram, use a solid-head arrow with a solid line; this is the same arrow that is used for an interface–superinterface relationship. For example, Figure 3-1 shows the part of the class diagram related to the version 6 bank account classes, revised to include the Object class. In general, class diagrams usually omit Object because its presence is implied and adding it tends to make the diagram unnecessarily complex.
../images/470600_1_En_3_Chapter/470600_1_En_3_Fig1_HTML.jpg
Figure 3-1

Adding Object to a class diagram

Chapter 2 introduced the Liskov Substitution Principle as it relates to interfaces. The principle also applies to classes. It states that if class A extends class B then an A-object can be used anywhere that a B-object is expected. In other words, if A extends B then A IS-A B.

As an example, suppose that you want to modify the banking demo to have the new bank account type “interest checking.” An interest checking account is exactly like a regular checking account except that it gives periodic interest. Call this class InterestChecking.

Should InterestChecking extend CheckingAccount? When I described interest checking I said that it “is exactly like” regular checking. This suggests an IS-A relationship, but let’s be sure. Suppose that the bank wants a report listing all checking accounts. Should the report include the interest checking accounts? If the answer is “yes” then there is an IS-A relationship and InterestChecking should extend CheckingAccount. If the answer is “no” then it shouldn’t.

Suppose that InterestChecking should indeed be a subclass of CheckingAccount. An interest checking account differs from a regular checking account in two ways: its toString method prints “Interest checking,” and its addInterest method gives interest. Consequently, the code for InterestChecking will override toString and addInterest, and inherit the code for the remaining methods from its superclass. A possible implementation of the class appears in Listing 3-3.
public class InterestChecking extends CheckingAccount {
   private double rate = 0.01;
   public InterestChecking(int acctnum) {
      super(acctnum);
   }
   public String toString() {
      return "Interest checking account " + getAcctNum()
            + ": balance=" + getBalance() + ", is "
            + (isForeign() ? "foreign" : "domestic");
   }
   public void addInterest() {
      int newbalance = (int) (getBalance() * rate);
      deposit(newbalance);
   }
}
Listing 3-3

A Proposed InterestChecking Class

Note that the constructor calls the method super. The super method is a call to the superclass’s constructor and is used primarily when the subclass needs the superclass to handle its constructor’s arguments. If a subclass’s constructor calls super then Java requires that the call must be the first statement of the constructor.

The private variables of a class are not visible to any other class, including its subclasses. This forces the subclass code to access its inherited state by calling the superclass’s public methods. For example, consider again the proposed InterestChecking code of Listing 3-3. The toString method would like to access the variables acctnum, balance, and isforeign from its superclass. However, these variables are private, which forces toString to call the superclass methods getAcctNum, getBalance, and isForeign to get the same information. Similarly, the addInterest method has to call getBalance and deposit instead of simply updating the variable balance.

It is good practice to encapsulate a class from its subclasses as much as possible. But sometimes (as in the case of the addInterest code) the result is awkward. Consequently, Java provides the modifier protected as an alternative to public or private. A protected variable is accessible to its descendent classes in the hierarchy but not to any other classes. For example, if CheckingAccount declares the variable balance to be protected then the addInterest method of InterestChecking can be written as follows:
   public void addInterest() {
      balance += (int) (balance * RATE);
   }

Abstract Classes

Consider again version 6 of the banking demo. The CheckingAccount and SavingsAccount classes currently have several identical methods. If these methods need not remain identical in the future then the classes are designed properly. However, suppose that the bank’s policy is that deposits always behave the same regardless of the account type. Then the two deposit methods will always remain identical; in other words, they contain duplicate code.

The existence of duplicate code in a program is problematic because this duplication will need to be maintained as the program changes. For example, if there is a bug fix to the deposit method of CheckingAccount then you will need to remember to make the same bug fix to SavingsAccount. This situation leads to the following design rule, called Don’t Repeat Yourself (or “DRY”) :

The “Don’t Repeat Yourself” Rule

A piece of code should exist in exactly one place.

The DRY rule is related to the Most Qualified Class rule, which implies that a piece of code should only exist in the class that is most qualified to perform it. If two classes seem equally qualified to perform the code then there is probably a flaw in the design – most likely, the design is missing a class that can serve as the most qualified one. In Java, a common way to provide this missing class is to use an abstract class.

Version 6 of the banking demo illustrates a common cause of duplicate code: two related classes implementing the same interface. A solution is to create a superclass of CheckingAccount and SavingsAccount and move the duplicate methods to it, together with the state variables they use. Call this superclass AbstractBankAccount. The classes CheckingAccount and SavingsAccount will each hold their own class-specific code and will inherit their remaining code from AbstractBankAccount. This design is version 7 of the banking demo. The code for AbstractBankAccount appears in Listing 3-4. This class contains
  • the state variables acctnum, balance, and isforeign. These variables have the protected modifier so that the subclasses can access them freely.

  • a constructor that initializes acctnum. This constructor is protected so that it can only be called by its subclasses (via their super method).

  • code for the common methods getAcctNum, getBalance, deposit, compareTo, and equals.

public abstract class AbstractBankAccount
                      implements BankAccount {
   protected int acctnum;
   protected int balance = 0;
   protected boolean isforeign = false;
   protected AbstractBankAccount(int acctnum) {
      this.acctnum = acctnum;
   }
   public int getAcctNum() {
      return acctnum;
   }
   public int getBalance() {
      return balance;
   }
   public boolean isForeign() {
      return isforeign;
   }
   public void setForeign(boolean b) {
      isforeign = b;
   }
   public void deposit(int amt) {
      balance += amt;
   }
   public int compareTo(BankAccount ba) {
      int bal1 = getBalance();
      int bal2 = ba.getBalance();
      if (bal1 == bal2)
         return getAcctNum() - ba.getAcctNum();
      else
         return bal1 - bal2;
   }
   public boolean equals(Object obj) {
      if (! (obj instanceof BankAccount))
         return false;
      BankAccount ba = (BankAccount) obj;
      return getAcctNum() == ba.getAcctNum();
   }
   public abstract boolean hasEnoughCollateral(int loanamt);
   public abstract String toString();
   public abstract void addInterest();
}
Listing 3-4

The Version 7 AbstractBankAccount Class

Note the declarations for the methods hasEnoughCollateral, toString, and addInterest. These methods are declared to be abstract and have no associated code. The issue is that AbstractBankAccount implements BankAccount, so those methods need to be in its API; however, the class has no useful implementation of the methods because the code is provided by its subclasses. By declaring those methods to be abstract, the class asserts that its subclasses will provide code for them.

A class that contains an abstract method is called an abstract class and must have the abstract keyword in its header. An abstract class cannot be instantiated directly. Instead, it is necessary to instantiate one of its subclasses so that its abstract methods will have some code. For example:
   BankAccount xx = new AbstractBankAccount(123); // illegal
   BankAccount ba = new SavingsAccount(123);      // legal
Listing 3-5 gives the version 7 code for SavingsAccount ; the code for CheckingAccount is similar. This code is basically the same as the version 6 code except that it only contains implementations of the three abstract methods of AbstractBankAccount; the other methods of BankAccount can be omitted because they are inherited from AbstractBankAccount. The implementations of the abstract methods are able to reference the variables balance, acctnum, and isforeign because they are protected in AbstractBankAccount.
public class SavingsAccount extends AbstractBankAccount {
   private double rate = 0.01;
   public SavingsAccount(int acctnum) {
      super(acctnum);
   }
   public boolean hasEnoughCollateral(int loanamt) {
      return balance >= loanamt / 2;
   }
   public String toString() {
      return "Savings account " + acctnum + ": balance="
                  + balance + ", is "
                  + (isforeign ? "foreign" : "domestic");
   }
   public void addInterest() {
      balance += (int) (balance * rate);
   }
}
Listing 3-5

The Version 7 SavingsAccount Class

The version 7 code for InterestChecking is similar to the code in Listing 3-3 except that its methods refer to the protected variables of AbstractBankAccount; its code is therefore not shown.

The version 7 BankClient and Bank classes have minor revisions to handle the creation of InterestChecking objects. Listing 3-6 gives the relevant portion of the newAccount method in BankClient. Listing 3-7 gives the revised method for newAccount in Bank. The changes are in bold.
private void newAccount() {
   System.out.print("Enter account type(1=savings,
                     2=checking, 3=interest checking): ");
   int type = scanner.nextInt();
   boolean isforeign = requestForeign();
   current = bank.newAccount(type, isforeign);
   System.out.println("Your new account number is "
                     + current);
}
Listing 3-6

The Version 7 newAccount Method of BankClient

public int newAccount(int type, boolean isforeign) {
   int acctnum = nextacct++;
   BankAccount ba;
   if (type == 1)
      ba = new SavingsAccount(acctnum);
   else if (type == 2)
      ba = new CheckingAccount(acctnum);
   else
      ba = new InterestChecking(acctnum);
   ba.setForeign(isforeign);
   accounts.put(acctnum, ba);
   return acctnum;
}
Listing 3-7

The Version 7 newAccount Method of Bank

The class diagram for the version 7 bank account classes appears in Figure 3-2. From it you can deduce that AbstractBankAccount implements all the methods in BankAccount except hasEnoughCollateral, toString, and addInterest; that CheckingAccount and SavingsAccount implement those three methods; and that InterestChecking overrides toString and addInterest. Note that the rectangle for AbstractBankAccount italicizes the class name and the abstract methods, to denote that they are abstract.
../images/470600_1_En_3_Chapter/470600_1_En_3_Fig2_HTML.jpg
Figure 3-2

The version 7 bank account classes

An abstract class defines a category of related classes. For example, the class AbstractBankAccount defines the category “bank accounts,” whose descendent classes—savings accounts, checking accounts, and interest checking accounts—are members of this category.

On the other hand, a non-abstract superclass such as CheckingAccount plays both roles: it defines the category “checking accounts” (of which InterestChecking is a member) and it also denotes a particular member of that category (namely, the “regular checking accounts”). This dual usage of CheckingAccount makes the class less easy to understand and complicates the design.

A way to resolve this issue is to split CheckingAccount into two pieces: an abstract class that defines the category of checking accounts and a subclass that denotes the regular checking accounts. Version 8 of the banking demo makes this change: the abstract class is CheckingAccount and the subclass is RegularChecking.

CheckingAccount implements the method hasEnoughCollateral , which is common to all checking accounts. Its abstract methods are toString and addInterest, which are implemented by the subclasses RegularChecking and InterestChecking. Figure 3-3 shows the version 8 class diagram. Note how the two abstract classes form a taxonomy that categorizes the three bank account classes.
../images/470600_1_En_3_Chapter/470600_1_En_3_Fig3_HTML.jpg
Figure 3-3

The version 8 bank account classes

The revised code for CheckingAccount appears in Listing 3-8. The methods toString and addInterest are abstract because its subclasses are responsible for calculating interest and knowing their account type. Its constructor is protected because it should be called only by subclasses.
public abstract class CheckingAccount
                      extends AbstractBankAccount {
   protected CheckingAccount(int acctnum) {
      super(acctnum);
   }
   public boolean hasEnoughCollateral(int loanamt) {
      return balance >= 2 * loanamt / 3;
   }
   public abstract String toString();
   public abstract void addInterest();
}
Listing 3-8

The Version 8 CheckingAccount Class

The code for RegularChecking appears in Listing 3-9; the code for InterestChecking is similar. The other classes in the version 8 demo are essentially unchanged from version 7. For example, the only change to Bank is its newAccount method, where it needs to create a RegularChecking object instead of a CheckingAccount object.
public class RegularChecking extends CheckingAccount {
   public RegularChecking(int acctnum) {
      super(acctnum);
   }
   public String toString() {
      return "Regular checking account " + acctnum
                + ": balance=" + balance + ", is "
                + (isforeign ? "foreign" : "domestic");
   }
   public void addInterest() {
      // do nothing
   }
}
Listing 3-9

The Version 8 RegularChecking Class

Abstract classes are by far the most common use of subclassing. The Java library contains numerous examples of subclass–superclass relationships, but in nearly all of them the superclass is abstract. The InterestChecking example illustrates why this is so: A design that involves a non-abstract superclass can often be improved by turning it into an abstract class.

Writing Java Collection Classes

Chapter 2 introduced the Java collection library, its interfaces, and the classes that implement these interfaces. These classes are general purpose and appropriate for most situations. However, a program may have a specific need for a custom collection class. The problem is that the collection interfaces have lots of methods, which complicates the task of writing custom classes. Moreover, many of the methods have straightforward implementations that would be the same for each implementing class. The result is duplicated code, violating the DRY rule.

The Java collection library contains abstract classes to remedy this problem. Most of the collection interfaces have a corresponding abstract class, whose name is “Abstract” followed by the interface name. That is, the class corresponding to List<E> is named AbstractList<E>, and so on. Each abstract class leaves a few of its interface methods abstract and implements the remaining methods in terms of the abstract ones.

For example, the abstract methods of AbstractList<E> are size and get. If you want to create your own class that implements List<E> then it suffices to extend AbstractList<E> and implement these two methods. (You also need to implement the method set if you want the list to be modifiable.)

As an example, suppose that you want to create a class RangeList that implements List<Integer>. A RangeList object will denote a collection that contains the n integers from 0 to n-1, for a value n specified in the constructor. Listing 3-10 gives the code for a program RangeListTest, which uses a RangeList object to print the numbers from 0 to 19:
public class RangeListTest {
   public static void main(String[] args) {
      List<Integer> L = new RangeList(20);
      for (int x : L)
         System.out.print(x + " ");
      System.out.println();
   }
}
Listing 3-10

The RangeListTest Class

The code for RangeList appears in Listing 3-11. Note how a RangeList object acts as if it actually contains a list of values, even though it doesn’t. In particular, its get method acts as if each slot i of the list contains the value i. This technique is remarkable and significant. The point is that if an object declares itself to be a list and behaves like a list, then it is a list. There is no requirement that it actually contain the list’s elements.
public class RangeList extends AbstractList<Integer> {
   private int limit;
   public RangeList(int limit) {
      this.limit = limit;
   }
   public int size() {
      return limit;
   }
   public Integer get(int n) {
      return n;
   }
}
Listing 3-11

The RangeList Class

Byte Streams

The Java library contains the abstract class InputStream, which denotes the category of data sources that can be read as a sequence of bytes. This class has several subclasses. Here are three examples:
  • The class FileInputStream reads bytes from a specified file.

  • The class PipedInputStream reads bytes from a pipe. Pipes are what enable different processes to communicate. For example, internet sockets are implemented using pipes.

  • The class ByteArrayInputStream reads bytes from an array. This class enables a program to access the contents of a byte array as if it were a file.

Similarly, the abstract class OutputStream denotes objects to which you can write a sequence of bytes. The Java library has OutputStream classes that mirror the InputStream classes. In particular, FileOutputStream writes to a specified file, PipedOutputStream writes to a pipe, and ByteArrayOutputStream writes to an array. The class diagrams for these classes appear in Figure 3-4.
../images/470600_1_En_3_Chapter/470600_1_En_3_Fig4_HTML.jpg
Figure 3-4

Class diagrams for InputStream and OutputStream

The public variable System.in belongs to an unspecified class that extends InputStream and by default reads bytes from the console. For example, the class BankProgram in the banking demo contains the following statement:
   Scanner sc = new Scanner(System.in);
This statement can be written equivalently as follows:
   InputStream is = System.in;
   Scanner sc = new Scanner(is);
One of the great values of the abstract classes InputStream and OutputStream is their support of polymorphism. Client classes that use InputStream and OutputStream need not depend on the specific input or output source they use. The class Scanner is a good example. The argument to the Scanner’s constructor can be any input stream whatsoever. For example, to create a scanner that reads from the file “testfile” you can write:
   InputStream is = new FileInputStream("testfile");
   Scanner sc = new Scanner(is);
The demo class EncryptDecrypt illustrates a typical use of byte streams. The code for this class appears in Listing 3-12. Its encrypt method takes three arguments: the name of the source and output files, and an encryption offset. It reads each byte from the source, adds the offset to it, and writes the modified byte value to the output. The main method calls encrypt twice. The first time, it encrypts the bytes of the file “data.txt” and writes them to the file “encrypted.txt”; the second time, it encrypts the bytes of “encrypted.txt” and writes them to “decrypted.txt.” Since the second encryption offset is the negative of the first, the bytes in “decrypted.txt” will be a byte-by-byte copy of “data.txt.”
public class EncryptDecrypt {
   public static void main(String[] args) throws IOException {
      int offset = 26;  // any value will do
      encrypt("data.txt", "encrypted.txt", offset);
      encrypt("encrypted.txt", "decrypted.txt", -offset);
   }
   private static void encrypt(String source, String output,
                             int offset) throws IOException {
      try ( InputStream  is = new FileInputStream(source);
            OutputStream os = new FileOutputStream(output) ) {
         int x;
         while ((x = is.read()) >= 0) {
            byte b = (byte) x;
            b += offset;
            os.write(b);
         }
      }
   }
}
Listing 3-12

The EncryptDecrypt Class

Note that this “decryption by double encryption” algorithm works properly regardless of the encryption offset. The reason has to do with the properties of byte arithmetic. When an arithmetic operation causes a byte value to go outside of its range, the overflow is discarded; the result is that addition and subtraction become cyclic. For example, 255 is the largest byte value, so 255+1 = 0. Similarly, 0 is the smallest byte value, so 0-1 = 255.

The encrypt method illustrates the use of the read and write methods. The write method is straightforward; it writes a byte to the output stream. The read method is more intricate. It returns an integer whose value is either the next byte in the input stream (a value between 0 and 255) or a -1 if the stream has no more bytes. Client code typically calls read in a loop, stopping when the returned value is negative. When the returned value is not negative, the client should cast the integer value to a byte before using it.

Unbeknown to clients, input and output streams often request resources from the operating system on their behalf. Consequently, InputStream and OutputStream have the method close, whose purpose is to return those resources to the operating system. A client can call close explicitly, or can instruct Java to autoclose the stream. The encrypt method illustrates the autoclose feature. The streams are opened as “parameters” to the try clause and will be automatically closed when the try clause completes.

Most stream methods throw IO exceptions. The reason is that input and output streams are often managed by the operating system and therefore are subject to circumstances beyond the control of the program. The stream methods need to be able to communicate unexpected situations (such as a missing file or unavailable network) so that their client has a chance to handle them. For simplicity, the two EncryptDecrypt methods do not handle exceptions and instead throw them back up the call chain.

In addition to the zero-argument read method used in Listing 3-12, InputStream has two methods that read multiple bytes at a time:
  • A one-argument read method, where the argument is a byte array. The method reads enough bytes to fill the array.

  • A three-argument read method, where the arguments are a byte array, the offset in the array where the first byte should be stored, and the number of bytes to read.

The value returned by these methods is the number of bytes that were read, or -1 if no bytes could be read.

As a simple example, consider the following statements:
   byte[] a = new byte[16];
   InputStream is = new FileInputStream("fname");
   int howmany = is.read(a);
   if (howmany == a.length)
       howmany = is.read(a, 0, 4);

The third statement tries to read 16 bytes into array a; the variable howmany contains the actual number of bytes read (or -1 if no bytes were read). If that value is less than 16 then the stream must have run out of bytes, and the code takes no further action. If the value is 16 then the next statement tries to read four more bytes, storing them in slots 0-3 of the array. Again, the variable howmany will contain the number of bytes actually read.

The class OutputStream has analogous write methods. The primary difference between the write and read methods is that the write methods return void.

For a concrete example that uses the multibyte read and write methods, consider the banking demo. Suppose that you want the bank’s account information to be written in a file so that its state can be restored upon each execution of BankProgram.

The revised BankProgram code appears in Listing 3-13. The code makes use of a class SavedBankInfo that behaves as follows. Its constructor reads account information from the specified file and constructs the account map. Its method getAccounts returns the account map, which will be empty if the file does not exist. Its method nextAcctNum returns the number for the next new account, which will be 0 if the file does not exist. Its method saveMap writes the current account information to the file, overwriting any previous information.
public class BankProgram {
   public static void main(String[] args) {
      SavedBankInfo info = new SavedBankInfo("bank.info");
      Map<Integer,BankAccount> accounts = info.getAccounts();
      int nextacct = info.nextAcctNum();
      Bank bank = new Bank(accounts, nextacct);
      Scanner scanner = new Scanner(System.in);
      BankClient client = new BankClient(scanner, bank);
      client.run();
      info.saveMap(accounts, bank.nextAcctNum());
   }
}
Listing 3-13

The Version 8 BankProgram Class

The code for SavedBankInfo appears in Listing 3-14. The variables accounts and nextaccount are initialized for a bank having no accounts. The constructor is responsible for reading the specified file; if the file exists, it calls the local method readMap to use the saved account information to initialize nextaccount and populate the map. The method saveMap opens an output stream for the file and calls writeMap to write the account information to that stream.
public class SavedBankInfo {
   private String fname;
   private Map<Integer,BankAccount> accounts
                        = new HashMap<Integer,BankAccount>();
   private int nextaccount = 0;
   private ByteBuffer bb = ByteBuffer.allocate(16);
   public SavedBankInfo(String fname) {
      this.fname = fname;
      if (!new File(fname).exists())
         return;
      try (InputStream is = new FileInputStream(fname)) {
         readMap(is);
      }
      catch (IOException ex) {
         throw new RuntimeException("file read exception");
      }
   }
   public Map<Integer,BankAccount> getAccounts() {
      return accounts;
   }
   public int nextAcctNum() {
      return nextaccount;
   }
   public void saveMap(Map<Integer,BankAccount> map,
                       int nextaccount) {
      try (OutputStream os = new FileOutputStream(fname)) {
         writeMap(os, map, nextaccount);
      }
      catch (IOException ex) {
         throw new RuntimeException("file write exception");
      }
   }
   ... // definitions for readMap and writeMap
}
Listing 3-14

The Version 8 SavedBankInfo Class

SavedBankInfo has a variable of type ByteBuffer. The ByteBuffer class defines methods for converting between values and bytes. A ByteBuffer object has an underlying byte array. Its method putInt stores the 4-byte representation of an integer into the array at the specified offset; its method getInt converts the 4 bytes at the specified offset into an integer. SavedBankInfo creates a single 16-byte ByteBuffer object, whose underlying array will be used for all reading and writing to the file.

The code for the writeMap and readMap methods appears in Listing 3-15. These methods determine the overall structure of the data file. First, writeMap writes an integer denoting the next account number; then it writes the values for each account. The readMap method reads these values back. It first reads an integer and saves it in the global variable nextaccount. Then it reads the account information, saving each account in the map.
private void writeMap(OutputStream os,
                      Map<Integer,BankAccount> map,
                      int nextacct) throws IOException {
   writeInt(os, nextacct);
   for (BankAccount ba : map.values())
      writeAccount(os, ba);
}
private void readMap(InputStream is) throws IOException {
   nextaccount = readInt(is);
   BankAccount ba = readAccount(is);
   while (ba != null) {
      accounts.put(ba.getAcctNum(), ba);
      ba = readAccount(is);
   }
}
Listing 3-15

The Methods writeMap and readMap

The code for writeInt and readInt appears in Listing 3-16. The writeInt method stores an integer in the first four bytes of the byte buffer’s underlying array, and then uses the 3-argument write method to write those bytes to the output stream. The readInt method uses the 3-argument read method to read four bytes into the beginning of the ByteBuffer array, and then converts those bytes to an integer.
private void writeInt(OutputStream os, int n)
                                    throws IOException {
   bb.putInt(0, n);
   os.write(bb.array(), 0, 4);
}
private int readInt(InputStream is) throws IOException {
   is.read(bb.array(), 0, 4);
   return bb.getInt(0);
}
Listing 3-16

The writeInt and readInt Methods

The code for writeAccount and readAccount appears in Listing 3-17. The writeAccount method extracts the four crucial values from a bank account (its account number, type, balance, and isforeign flag), converts them to four integers, places them into the byte buffer, and then writes the entire underlying byte array to the output stream. The readAccount method reads 16 bytes into the underlying byte array and converts it into four integers. It then uses these integers to create a new account and configure it properly. The method indicates end of stream by returning a null value.
private void writeAccount(OutputStream os, BankAccount ba)
                                       throws IOException {
   int type = (ba instanceof SavingsAccount)  ? 1
            : (ba instanceof RegularChecking) ? 2 : 3;
   bb.putInt(0, ba.getAcctNum());
   bb.putInt(4, type);
   bb.putInt(8, ba.getBalance());
   bb.putInt(12, ba.isForeign() ? 1 : 2);
   os.write(bb.array());
}
private BankAccount readAccount(InputStream is)
                                       throws IOException {
   int n = is.read(bb.array());
   if (n < 0)
      return null;
   int num       = bb.getInt(0);
   int type      = bb.getInt(4);
   int balance   = bb.getInt(8);
   int isforeign = bb.getInt(12);
   BankAccount ba;
   if (type == 1)
      ba = new SavingsAccount(num);
   else if (type == 2)
      ba = new RegularChecking(num);
   else
      ba = new InterestChecking(num);
   ba.deposit(balance);
   ba.setForeign(isforeign == 1);
   return ba;
}
Listing 3-17

The writeAccount and readAccount Methods

As you can see, this way of preserving account information is very low level. Saving the information involves converting each account to a specific sequence of bytes, and restoring it requires reversing the process. As a result, the coding is difficult and somewhat painful. Chapter 7 will introduce the concept of an object stream, which enables clients to read and write objects directly and let the underlying code perform the tedious translation to bytes.

Now that you have seen how to use byte streams, it is time to examine how they are implemented. I will consider input streams only. Output streams are implemented analogously.

InputStream is an abstract class. It has one abstract method, namely the zero-argument read method, and provides default implementations of the other methods. A simplified version of the InputStream code appears in Listing 3-18.
public abstract class InputStream {
   public abstract int read() throws IOException;
   public void close() { }
   public int read(byte[] buf, int offset, int len)
                                        throws IOException {
      for (int i=0; i<len; i++) {
         int x = read();
         if (x < 0)
            return (i==0) ? -1 : i;
         buf[offset+i] = (byte) x;
      }
      return len;
   }
   public int read(byte[] buf) throws IOException {
      read(buf, 0, buf.length);
   }
   ...
}
Listing 3-18

A Simplified InputStream Class

The default implementations of the three non-abstract methods are straightforward. The close method does nothing. The three-argument read method fills the specified portion of the array by making repeated calls to the zero-argument read method. And the one-argument read method is just a special case of the three-argument method.

Each subclass of InputStream needs to implement the zero-argument read method and can optionally override the default implementation of other methods. For example, if a subclass acquires resources (such as the file descriptor acquired by FileInputStream) then it should override the close method to release those resources.

A subclass may choose to override the three-argument read method for efficiency. For example, classes such as FileInputStream and PipedInputStream obtain their bytes via operating system calls. Since calls to the operating system are time consuming, the classes will be more efficient when they minimize the number of these calls. Consequently, they override the default three-argument read method by a method that makes a single multibyte call to the operating system.

The code for ByteArrayInputStream provides an example of an InputStream subclass. A simple implementation appears in Listing 3-19.
public class ByteArrayInputStream extends InputStream {
   private byte[] a;
   private int pos = 0;
   public ByteArrayInputStream(byte[] a) {
      this.a = a;
   }
   public int read() throws IOException {
      if (pos >= a.length)
         return -1;
      else {
         pos++;
         return a[pos-1];
      }
   }
}
Listing 3-19

A Simplified ByteArrayInputStream Class

The way that the InputStream methods act as defaults for their subclasses is akin to the way that the abstract collection classes help their subclasses implement the collection interfaces. The difference is that the collection library makes a difference between an abstract class (such as AbstractList) and its corresponding interface (such as List). The abstract classes InputStream and OutputStream have no corresponding interface. In effect, they act as their own interface.

The Template Pattern

The abstract collection classes and the byte stream classes illustrate a particular way to use an abstract class: The abstract class implements some of the methods of its API, and declares the other methods to be abstract. Each of its subclasses will then implement these abstract public methods (and possibly override some of the other methods).

Here is a slightly more general way to design an abstract class. The abstract class will implement all the methods of its API, but not necessarily completely. The partially-implemented methods call “helper” methods, which are protected (that is, they are not visible from outside the class hierarchy) and abstract (that is, they are implemented by subclasses).

This technique is called the template pattern . The idea is that each partial implementation of an API method provides a “template” of how that method should work. The helper methods enable each subclass to appropriately customize the API methods.

In the literature, the abstract helper methods are sometimes called “hooks.” The abstract class provides the hooks, and each subclass provides the methods that can be hung on the hooks.

The version 8 BankAccount class hierarchy can be improved by using the template pattern. The problem with the version 8 code is that it still violates the DRY rule. Consider the code for method hasEnoughCollateral in the classes SavingsAccount (Listing 3-5) and CheckingAccount (Listing 3-8). These two methods are almost identical. They both multiply the account balance by a factor and compare that value to the loan amount. Their only difference is that they multiply by different factors. How can we remove this duplication?

The solution is to move the multiplication and comparison up to the AbstractBankAccount class and create an abstract helper method that returns the factor to multiply by. This solution is implemented in the version 9 code. The code for the hasEnoughCollateral method in AbstractBankAccount changes to the following:
   public boolean hasEnoughCollateral(int loanamt) {
      double ratio = collateralRatio();
      return balance >= loanamt * ratio;
   }
   protected abstract double collateralRatio();
That is, the hasEnoughCollateral method is no longer abstract. Instead, it is a template that calls the abstract helper method collateralRatio, whose code is implemented by the subclasses. For example, here is the version 9 code for the collateralRatio method in SavingsAccount.
   protected double collateralRatio() {
      return 1.0 / 2.0;
   }

The abstract methods addInterest and toString also contain duplicate code. Instead of having each subclass implement these methods in their entirety, it is better to create a template for them in AbstractBankAccount. Each template method can call abstract helper methods, which the subclasses can then implement. In particular, the addInterest method calls the abstract method interestRate and the toString method calls the abstract method accountType.

Figure 3-5 displays the class diagram for the version 9 banking demo. From it you can deduce that:
  • AbstractBankAccount implements all of the methods in BankAccount, but itself has the abstract methods collateralRatio, accountType, and interestRate.

  • SavingsAccount implements all three of these methods.

  • CheckingAccount implements collateralRatio only, and leaves the other two methods for its subclasses.

  • RegularChecking and InterestChecking implement accountType and interestRate.

../images/470600_1_En_3_Chapter/470600_1_En_3_Fig5_HTML.jpg
Figure 3-5

The version 9 class diagram

The following listings show the revised classes for version 9. The code for AbstractBankAccount appears in Listing 3-20; the code for SavingsAccount appears in Listing 3-21; the code for CheckingAccount appears in Listing 3-22; and the code for RegularChecking appears in Listing 3-23. The code for InterestChecking is similar to that for RegularChecking, and is omitted. Note that because of the template pattern, these classes are remarkably compact. There is no repeated code whatsoever!
public abstract class AbstractBankAccount
                      implements BankAccount {
   protected int acctnum;
   protected int balance;
   ...
   public boolean hasEnoughCollateral(int loanamt) {
      double ratio = collateralRatio();
      return balance >= loanamt * ratio;
   }
   public String toString() {
      String accttype = accountType();
      return accttype + " account " + acctnum
             + ": balance=" + balance + ", is "
             + (isforeign ? "foreign" : "domestic");
   }
   public void addInterest() {
      balance += (int) (balance * interestRate());
   }
   protected abstract double collateralRatio();
   protected abstract String accountType();
   protected abstract double interestRate();
}
Listing 3-20

The Version 9 AbstractBankAccount Class

public class SavingsAccount extends BankAccount {
   public SavingsAccount(int acctnum) {
      super(acctnum);
   }
   public double collateralRatio() {
      return 1.0 / 2.0;
   }
   public String accountType() {
      return "Savings";
   }
   public double interestRate() {
      return 0.01;
   }
}
Listing 3-21

The Version 9 SavingsAccount Class

public abstract class CheckingAccount extends BankAccount {
   public CheckingAccount(int acctnum) {
      super(acctnum);
   }
   public double collateralRatio() {
      return 2.0 / 3.0;
   }
   protected abstract String accountType();
   protected abstract double interestRate();
}
Listing 3-22

The Version 9 CheckingAccount Class

public class RegularChecking extends CheckingAccount {
   public RegularChecking(int acctnum) {
      super(acctnum);
   }
   protected String accountType() {
      return "Regular Checking";
   }
   protected double interestRate() {
      return 0.0;
   }
}
Listing 3-23

The Version 9 RegularChecking Class

For another illustration of the template pattern, consider the Java library class Thread. The purpose of this class is to allow a program to execute code in a new thread. It works as follows:
  • Thread has two methods: start and run.

  • The start method asks the operating system to create a new thread. It then executes the object’s run method from that thread.

  • The run method is abstract and is implemented by a subclass.

  • A client program defines a class X that extends Thread and implements the run method. The client then creates a new X-object and calls its start method.

The class ReadLine in Listing 3-24 is an example of a Thread subclass. Its run method does very little. The call to sc.nextLine blocks until the user presses the Return key. When that occurs, the run method stores the input line in variable s, sets its variable done to true, and exits. Note that the method does nothing with the input line. The only purpose of the input is to set the variable done to true when the user presses Return.
class ReadLine extends Thread {
   private boolean done = false;
   public void run() {
      Scanner sc = new Scanner(System.in);
      String s = sc.nextLine();
      sc.close();
      done = true;
   }
   public boolean isDone() {
      return done;
   }
}
Listing 3-24

The ReadLine Class

Listing 3-25 gives the code for the class ThreadTest . That class creates a ReadLine object and calls its start method, causing its run method to execute from a new thread. The class then proceeds (from the original thread) to print integers in increasing order until the isDone method of ReadLine returns true. In other words, the program prints integers until the user presses the Return key. The new thread makes it possible for the user to interactively decide when to stop the printing.
public class ThreadTest {
   public static void main(String[] args) {
      ReadLine r = new ReadLine();
      r.start();
      int i = 0;
      while(!r.isDone()) {
         System.out.println(i);
         i++;
      }
   }
}
Listing 3-25

The ThreadTest Class

Note how the Thread class uses the template pattern. Its start method is part of the public API and acts as the template for thread execution. Its responsibility is to create and execute a new thread, but it doesn’t know what code to execute. The run method is the helper method. Each Thread subclass customizes the template by specifying the code for run.

One common mistake when using threads is to have the client call the thread’s run method instead of its start method . After all, the Thread subclass contains the method run and the start method is hidden from sight. Moreover, calling run is legal; doing so has the effect of running the thread code, but not in a new thread. (Try executing Listing 3-25 after changing the statement r.start() to r.run(). What happens?) However, once you understand that threading uses the template pattern, the reason for calling the start method becomes clear and the design of the Thread class finally makes sense.

Summary

Classes in an object-oriented language can form subclass-superclass relationships. The creation of these relationships should be guided by the Liskov Substitution Principle: Class X should be a subclass of class Y if X-objects can be used wherever Y-objects are needed. A subclass inherits the code of its superclass.

One reason for creating superclass–subclass relationships is to satisfy the DRY rule, which states that a piece of code should exist in exactly one place. If two classes contain common code then that common code can be placed in a common superclass of the two classes. The classes can then inherit this code from their superclass.

If the two subclasses are different implementations of the same interface then their common superclass should also implement that interface. In this case the superclass becomes an abstract class and the interface methods that it does not implement are declared as abstract. An abstract class cannot be instantiated, and instead acts as a category for its implementing classes. The categorization produced by a hierarchy of abstract classes is called a taxonomy.

There are two ways for an abstract class to implement its interface. The first way is exemplified by the Java abstract collection classes. The abstract class declares a few of the interface methods to be abstract and then implements the remaining methods in terms of the abstract ones. Each subclass only needs to implement the abstract methods, but can override any of the other methods if desired.

The second way is exemplified by the Java Thread class. The abstract class implements all of the interface methods, calling abstract “helper” methods when needed. Each subclass implements these helper methods. This technique is called the template pattern. The abstract class provides a “template” of how each interface method should work, with each subclass providing the subclass-specific details.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.209.131