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

7. Adapters

Edward Sciore1 
(1)
Newton, MA, USA
 

The next two chapters examine a design technique known as wrapping. Wrapping denotes a close relationship between two classes, called the wrapper class and the wrapped class. The sole purpose of the wrapper class is to modify or enhance the functionality of its wrapped class. Wrapping corresponds perfectly to the Open/Closed design rule: If you need a class to behave slightly differently then don’t modify it. Instead, create a new class that wraps it.

This chapter covers the concept of an adapter, which is a wrapper that changes the functionality of the class it wraps. This chapter presents several examples that illustrate the widespread applicability of adapter classes. Of particular interest are the Java library classes that adapt the byte streams of Chapter 3. These classes make it possible to read and write streams of characters, primitive values, or objects simply by adapting the existing classes that implement streams of bytes.

Inheritance for Reuse

Suppose you need to write a class. There is an existing class that has methods similar to the methods you need. You therefore decide to define your class as a subclass of this existing class, so that you can inherit those methods and thereby simplify your code-writing task.

This sounds like a great idea, right? Unfortunately, it isn’t. In fact, it’s a very bad idea. As discussed in Chapter 3, the only good reason to create a subclass is because the subclass–superclass relationship satisfies the Liskov Substitution Principle—that is, if subclass objects can be used in place of a superclass objects. The possibility of inheriting code plays no part in the decision.

Nevertheless, it is hard to resist the temptation to create a subclass just to inherit code. The history of object-oriented software is littered with such classes. This design technique even has a name: inheritance for reuse.

In the early days of object-oriented programming, inheritance for reuse was touted as one of the big advantages of object orientation. Now we understand that this idea is completely wrong. The Java library class Stack provides a good example of the problems that arise.

Stack extends the Java library class Vector, which implements List. The advantage to extending Vector is that the stack methods empty, push, and pop become very easy to implement. Listing 7-1 gives a simplified version of the source code for Stack.
public class Stack<E> extends Vector<E> {
   public boolean empty() {
      return size() == 0;
   }
   public void push(E item) {
      add(item);
   }
   public E pop() {
      return remove(size()-1);
   }
}
Listing 7-1

Simplified Code for the Stack Class

This code takes advantage of the inherited methods size, add, and remove from Vector. The designers of Stack were no doubt delighted that they could use the Vector methods for “free,” without having to write any code for them. This is a perfect example of inheritance for reuse.

However, this design has significant problems. The decision to extend Vector violates the Liskov Substitution Principle because a stack in no way behaves like a list. For example, a stack only lets you look at its top element whereas a list lets you look at (and modify) every element.

Practically speaking, the problem is that clients can use a stack in non–stack-like ways. For a simple example, the last statement of the following code modifies the bottom of the stack:
   Stack<String> s = new Stack<>();
   s.push("abc");
   s.push("xyz");
   s.set(0,"glorp");

In other words, the Stack class is not sufficiently encapsulated. A client can take advantage of the fact that it is implemented in terms of Vector.

The Stack class was part of the first Java release. Since then the Java development community has admitted that this design was a mistake. In fact, the current documentation for the class recommends that it not be used.

Wrappers

The good news is that it is possible to write Stack so that it makes use of Vector without being a subclass of it. Listing 7-2 illustrates the technique: Stack holds a variable of type Vector , and uses this variable to implement its empty, push, and pop methods. Since the variable is private to Stack, it is inaccessible from the other classes, which ensures that Stack variables are not able to call Vector methods.
public class Stack<E> {
   private Vector<E> v = new Vector<>();
   public boolean empty() {
      return v.size() == 0;
   }
   public void push(E item) {
      v.add(item);
   }
   public E pop() {
      return v.remove(v.size()-1);
}
Listing 7-2

Using a Wrapper to Implement Stack

This implementation technique is called wrapping. Wrapping is a specific use of a dependency relationship in which a class C implements its methods via a dependency to a class D, calling D’s methods to do most (or all) of the work. The class D is called the backing store of the wrapper C. For example in Listing 7-2, Stack wraps Vector because it has a dependency to Vector, and implements its methods by calling the Vector methods.

Wrapping is a remarkably useful technique. If a design involves inheritance for reuse then it can always be redesigned to use wrapping instead. And experience has shown that the wrapper-based design is always better.

The Adapter Pattern

Wrapper classes are often used as adapters. The term “adapter” has an analogy with real life adapters. For example, suppose you want to plug a three-pronged vacuum cleaner into a two-prong electric outlet. One way to resolve this impasse is to purchase a new vacuum cleaner that works with a two-prong outlet. Another way is to rewire the outlet so that it accepts three prongs. Both of these options are expensive and impractical.

A third and far better option is to buy an adapter that has two prongs on one end and accepts three prongs on the other end. The device plugs into one end of the adapter and the adapter’s other end plugs into the outlet. The adapter manages the transfer of electricity between its two ends.

An analogous situation exists with software. Suppose that your code needs a class having a particular interface but the best available class implements a slightly different interface. What should you do? You have the same three options.

Your first two options are to modify your code so that it uses the existing interface, or to modify the code for the existing class so that it implements the desired interface. As with the electrical adapter scenario, these options can be expensive and impractical. Moreover, they may not even be possible. For example, you are not allowed to modify classes in the Java library.

A simpler and better solution is to write an adapter class. The adapter class wraps the existing class and uses it to implement the desired interface. This solution is known as the adapter pattern. Its class diagram appears in Figure 7-1.
../images/470600_1_En_7_Chapter/470600_1_En_7_Fig1_HTML.jpg
Figure 7-1

The adapter pattern

The Stack class in Listing 7-2 is an example of the adapter pattern. The existing class is Vector. The desired interface consists of the methods {push, pop, empty}. The adapter class is Stack. Figure 7-2 shows their relationship. In this case there is no formal Java interface, so the diagram uses “StackAPI” to denote the desired methods.
../images/470600_1_En_7_Chapter/470600_1_En_7_Fig2_HTML.jpg
Figure 7-2

Stack as an adapter class

Another example of the adapter pattern arises in the Java library class Arrays. Recall from Chapter 5 that Arrays has a static factory method asList that returns a list containing the contents of a given array. The following code illustrates its use:
   String[] a = {"a", "b", "c", "d"};
   List<String> L = Arrays.asList(a);

One way to implement this method is to create a new ArrayList object, add the array elements to it, and return it. This idea is not very good because copying the elements of a large array is inefficient.

A much better idea is to use the adapter pattern. The object returned by the asList method will belong to an adapter class that wraps the array and implements the List methods. The code in Listing 7-3 implements the Arrays class using this idea. The adapter class is called ArrayAsList .
public class Arrays {
   ...
   public static <T> List<T> asList(T[] vals) {
      return new ArrayAsList<T>(vals);
   }
}
class ArrayAsList<T> extends AbstractList<T> {
   private T[] data;
   public ArrayAsList(T[] data) {
      this.data = data;
   }
   public int size() {
      return data.length;
   }
   public T get(int i) {
      return data[i];
   }
   public T set(int i, T val) {
      T oldval = data[i];
      data[i] = val;
      return oldval;
   }
}
Listing 7-3

A Simplified Implementation of the Arrays Class

The asList method simply creates and returns an object from its ArrayAsList adapter class. ArrayAsList extends the Java library class AbstractList, implementing the methods size, get, and set. Note that the code does not copy the array, but instead accesses the array elements on demand. The design is elegant and efficient. Figure 7-3 gives the class diagram showing how ArrayAsList fits into the adapter pattern.
../images/470600_1_En_7_Chapter/470600_1_En_7_Fig3_HTML.png
Figure 7-3

The adapter class ArrayAsList

Another example of an adapter is the class RandomIterator from Chapter 6, whose code appeared in Listing 6-3. The class wrapped an object of type Random and used it to implement the Iterator interface.

Text Streams

Chapter 3 introduced the abstract class InputStream, whose subclasses are able to read bytes from files, pipes, and other input sources; it also introduced OutputStream, which has analogous subclasses for writing bytes. The sequences of bytes managed by these classes are called byte streams (and are totally unrelated to the collection streams of Chapter 6). This section is concerned with streams of characters, which are called text streams.

The character stream hierarchies are headed by the abstract classes Reader and Writer. These classes closely parallel InputStream and OutputStream. Their methods are similar, the major difference being that the read and write methods for Reader and Writer manipulate characters instead of bytes. Their subclasses are also analogous. For example, the classes FileReader and PipedReader parallel FileInputStream and PipedInputStream.

As an example, Listing 7-4 gives the code for a program that reads the text file “mobydick.txt” and writes its first 500 characters to the file “shortmoby.txt.”
public class FilePrefix {
   public static void main(String[] args) throws IOException {
      try (Reader r = new FileReader("mobydick.txt");
           Writer w = new FileWriter("shortmoby.txt")) {
         for (int i=0; i<500; i++) {
            int x = r.read();
            if (x < 0)
               break;
            char c = (char) x;
            w.write(c);
         }
      }
   }
}
Listing 7-4

Reading and Writing a Text File

From a design perspective, the most interesting question about text streams is how the various Reader and Writer classes are implemented. It turns out that adapters play a big part, as discussed in the following subsections.

The Adapter OutputStreamWriter

Suppose you are asked to implement the FileWriter class; what would you do? Your code will need to address two issues. First, it will need to manage the file—opening it, writing values to it, and closing it. Second, your code will need to translate each output character to one or more bytes, because files understand bytes and not characters. You can handle the first issue by looking at the code for FileOutputStream . It has already dealt with that issue, so you can copy the relevant code. You can handle the second issue by using the class CharEncoder from the Java library, in a way that will be discussed soon. So everything seems under control. But before you proceed further you should stop and reread this paragraph. Is the proposed design a good one?

The answer is “no.” Copying code from FileOutputStream violates the Don’t Repeat Yourself design rule, and is a terrible idea. A much better design is to use an adapter class to leverage the existing implementation. In other words, FileWriter should be a class that adapts FileOutputStream and implements Writer .

In fact, you can do even better. Note that FileWriter’s only real responsibility is to encode each character into bytes and write those bytes to its wrapped output stream. This code is applicable to any output stream, not just FileOutputStream. In other words, instead of writing an adapter class for FileOutputStream, a more general solution is to write an adapter class for OutputStream.

The Java library provides exactly such an adapter class, called OutputStreamWriter. This class wraps an object that has existing functionality (namely OutputStream, which has the ability to write bytes) and uses it to implement the desired interface (namely Writer, which gives you the ability to write chars).

The usefulness of the OutputStreamWriter adapter is that it can convert any output stream into a writer. In particular, you can use it to write FileWriter. The following two statements are equivalent:
  Writer fw = new FileWriter(f);
  Writer fw = new OutputStreamWriter(new FileOutputStream(f));
In other words, FileWriter is a convenience class. It can be implemented as a subclass of OutputStreamWriter whose constructor creates the wrapped FileOutputStream object. Its code looks something like Listing 7-5.
public class FileWriter extends OutputStreamWriter {
   public FileWriter(String f) throws IOException {
      super(new FileOutputStream(f));
   }
}
Listing 7-5

The Code for the FileWriter Class

The class diagram in Figure 7-4 shows the relationship between the Writer, OutputStream, and FileWriter classes, and the key role played by the adapter class OutputStreamWriter.
../images/470600_1_En_7_Chapter/470600_1_En_7_Fig4_HTML.png
Figure 7-4

OutputStreamWriter as an adapter

Now that the purpose of OutputStreamWriter is clear, it is time to consider its implementation. A simplified version of the code appears in Listing 7-6. The class extends the abstract class Writer and therefore needs to implement three abstract methods: close, flush, and the 3-arg write method. OutputStreamWriter implements these three methods using its wrapped OutputStream object. The implementation of close and flush simply call the corresponding OutputStream methods. The write method encodes the specified characters, places the encoded bytes into a byte buffer, and writes each byte to the wrapped output stream.
public class OutputStreamWriter extends Writer {
   private CharsetEncoder encoder;
   private OutputStream os;
   public OutputStreamWriter(OutputStream os,
                   String charsetname) throws IOException {
      this.os = os;
      encoder = Charset.forName(charsetname).newEncoder();
   }
   public OutputStreamWriter(OutputStream os)
                            throws IOException {
      this(os, Charset.defaultCharset().name());
   }
   public void write(char cbuf[], int offset, int len)
                                     throws IOException {
      CharBuffer cb = CharBuffer.wrap(cbuf, offset, len);
      ByteBuffer bb = encoder.encode(cb);
      for (int i=0; i<bb.limit(); i++)
         os.write(bb.get(i));
   }
   public void close() throws IOException {
      os.close();
   }
   public void flush() throws IOException {
      os.flush();
   }
}
Listing 7-6

A Simplified OutputStreamWriter Class

The complexity of this class arises from the fact that there are many ways to encode characters. For example, Java uses 16-bit Unicode for its in-memory character encoding, which requires two bytes to store most characters. (Some characters require four bytes, which complicates matters considerably for Java but fortunately is irrelevant to this discussion.) However, 16-bit Unicode is not necessarily the best way to encode files. Many text editors use an encoding such as ASCII, which assumes a smaller character set that requires only one byte per character. A Java program that reads and writes files needs to be able to interact with multiple character encodings.

The Java library has the class Charset, whose objects denote character encodings. The class supports several standard encodings, each of which has a name. For example, the encodings for ASCII, 8-bit Unicode, and 16-bit Unicode are named “US-ASCII,” “UTF-8,” and “UTF-16.” Its forName method is a static factory method that returns the Charset object corresponding to the specified name.The OutputStreamWriter class has two constructors. The first constructor has an argument that specifies the name of the desired charset. The second constructor uses a predetermined default charset.

The Charset method newEncoder returns a CharsetEncoder object. CharsetEncoder has the method encode, which performs the encoding. The argument to encode is a CharBuffer object. A CharBuffer is similar to a ByteBuffer except that it uses an underlying array of chars instead of bytes. The encode method encodes those characters and places their encoded bytes into a ByteBuffer object, whose bytes can then be written to the output stream.

The Adapter InputStreamReader

The correspondence between the Reader and InputStream classes is analogous to that between Writer and OutputStream . In particular, the Java library contains the adapter class InputStreamReader that wraps InputStream and extends Reader. FileReader is a convenience class that extends InputStreamReader. The FileReader code is analogous to FileWriter and appears in Listing 7-7.
public class FileReader extends InputStreamReader {
   public FileReader(String f) throws IOException {
      super(new FileInputStream(f));
   }
}
Listing 7-7

The FileReader Class

The code for the InputStreamReader adapter class appears in Listing 7-8. It is more complex than OutputStreamWriter because decoding bytes is trickier than encoding chars. The problem is that the characters in some encodings need not encode to the same number of bytes, which means that you cannot know how many bytes to read to decode the next character. The InputStreamReader class solves this problem by buffering the input stream. It reads ahead some number of bytes and stores those bytes in a ByteBuffer object . The read method gets its input from that buffer.

The read method performs the conversion from bytes to chars by calling the method decode of class CharDecoder . Two of the arguments it provides to decode are the input byte buffer and the output char buffer. The decode method reads bytes from the byte buffer and places the decoded characters into the char buffer. It stops either when the char buffer is full or the byte buffer is empty. The situation when the char buffer is full is called overflow. In this case the read method can stop, retaining the remaining input bytes for the next call to read. The situation when the byte buffer is empty is called underflow. In this case the read method must refill the byte buffer and call decode again, so that it can fill the remainder of the char buffer.
public class InputStreamReader extends Reader {
   public static int BUFF_SIZE = 10;
   private ByteBuffer bb = ByteBuffer.allocate(BUFF_SIZE);
   private InputStream is;
   private CharsetDecoder decoder;
   private boolean noMoreInput;
   public InputStreamReader(InputStream is,
                    String charsetname) throws IOException {
      this.is = is;
      decoder = Charset.forName(charsetname).newDecoder();
      bb.position(bb.limit()); //indicates an empty buffer
      noMoreInput = fillByteBuffer();
   }
   public InputStreamReader(InputStream is)
                                        throws IOException {
      this(is, Charset.defaultCharset().name());
   }
   public int read(char cbuf[], int offset, int len)
                                    throws IOException {
      int howmany = len;
      while (true) {
         CharBuffer cb = CharBuffer.wrap(cbuf, offset, len);
         CoderResult result = decoder.decode(bb, cb,
                                             noMoreInput);
         if (result == CoderResult.OVERFLOW)
            return howmany;
         else if (result == CoderResult.UNDERFLOW
                         && noMoreInput)
            return (cb.position() > 0) ? cb.position() : -1;
         else if (result == CoderResult.UNDERFLOW) {
            // Get more bytes and repeat the loop
            // to fill the remainder of the char buffer.
            noMoreInput = fillByteBuffer();
            offset = cb.position();
            len = howmany - cb.position();
         }
         else
            result.throwException();
      }
   }
   public void close() throws IOException {
      is.close();
   }
   private boolean fillByteBuffer() throws IOException {
      bb.compact(); //move leftover bytes to the front
      int pos = bb.position();
      int amtToRead = bb.capacity() - pos;
      int result = is.read(bb.array(), pos, amtToRead);
      int amtActuallyRead = (result < 0) ? 0 : result;
      int newlimit = pos + amtActuallyRead;
      bb.limit(newlimit);
      bb.position(0); //indicates a full buffer
      return (amtActuallyRead < amtToRead);
   }
}
Listing 7-8

A Simplified InputStreamReader Class

The Adapter StringReader

The class StringReader is another example of a text stream adapter from the Java library. The job of the class is to create a reader from a string. Each call to its read method returns the next character(s) from the string. A simplified version of its code appears in Listing 7-9.
public class StringReader extends Reader {
   private String s;
   private int pos = 0;
   public StringReader(String s) throws IOException {
      this.s = s;
   }
   public int read(char[] cbuf, int off, int len)
                                 throws IOException {
      if (pos >= s.length())
         return -1;   // end of stream
      int count=0;
      while (count<len && pos<s.length()) {
         cbuf[off+count] = s.charAt(pos);
         pos++; count++;
      }
      return count;
   }
   public void close() {
      // strings don't need to be closed
   }
}
Listing 7-9

A Simplified StringReader Class

Unlike InputStreamReader, the code for StringReader is short and straightforward. The specified string acts as the input buffer. The read method puts a character from the string into the next slot of cbuf, stopping when len chars have been written (“overflow”) or the string is exhausted (“underflow”). In either case, the method returns the number of chars written.

The StringReader class conforms to the adapter pattern, as illustrated by the class diagram of Figure 7-5.
../images/470600_1_En_7_Chapter/470600_1_En_7_Fig5_HTML.jpg
Figure 7-5

StringReader as an adapter

Object Streams

The classes InputStream and OutputStream let you read and write byte streams, and Reader and Writer let you read and write character streams. The Java library provides two additional level of read/write. The interfaces DataInput and DataOutput let you read and write primitive values, and the interfaces ObjectInput and ObjectOutput let you read and write objects. Listing 7-10 gives the declaration of these four interfaces.
public interface DataInput {
   int readInt() throws IOException;
   double readDouble() throws IOException;
   ... // a method for each primitive type
}
public interface DataOutput {
   void writeInt(int i) throws IOException;
   void writeDouble(double d) throws IOException;
   ... // a method for each primitive type
}
public interface ObjectInput extends DataInput {
   Object readObject() throws IOException;
}
public interface ObjectOutput extends DataInput {
   void writeObject(Object obj) throws IOException;
}
Listing 7-10

The DataInput, DataOutput, ObjectInput, and ObjectOutput Interfaces

The Java library classes ObjectInputStream and ObjectOutputStream implement ObjectInput and ObjectOutput, and consequently also DataInput and DataOutput. These two classes are thus able to manage streams that contain a mixture of objects and primitive values.

Listing 7-11 gives code for the class ObjectStreamTest, which demonstrates the use of these classes. The program shows two ways to write a list of strings to an object stream and read them back.
public class ObjectStreamTest {
   public static void main(String[] args) throws Exception {
      List<String>  L = Arrays.asList("a", "b", "c");
      // Write the list to a file, in two ways.
      try (OutputStream os  = new FileOutputStream("sav.dat");
           ObjectOutput oos = new ObjectOutputStream(os)) {
         oos.writeObject(L);    // Write the list.
         oos.writeInt(L.size()); // Write the list size,
         for (String s : L)      // and then its elements .
            oos.writeObject(s);
      }
      // Read the lists from the file.
      try (InputStream is  = new FileInputStream("sav.dat");
           ObjectInput ois = new ObjectInputStream(is)) {
         List<String>  L1 = (List<String>) ois.readObject();
         List<String>  L2 = new ArrayList<>();
         int size = ois.readInt();      // Read the list size.
         for (int i=0; i<size; i++) {   // Read the elements.
            String s = (String) ois.readObject();
            L2.add(s);
         }
         // L, L1, and L2 are equivalent.
         System.out.println(L + ", " + L1 + ", " + L2);
      }
   }
}
Listing 7-11

The ObjectStreamTest Class

Reading an object stream is different from reading a byte or text stream. An object stream can contain an arbitrary sequence of objects and primitive values, and the client needs to know exactly what to expect when reading it. For example, in ObjectStreamTest the code to read the stream must know that the file “sav.dat” contains the following: a list of strings, an int, and as many individual strings as the value of the preceding int.

Consequently, a client should never need to read past the end of an object stream. This is very different from byte steams, where the client typically reads bytes until the end-of-stream sentinel value -1 is returned.

As the object returned by readObject is of type Object, the client must cast this object to the appropriate type.

The ObjectStreamTest demo illustrates two ways to write a list to the stream: you can write the entire list as a single object, or you can write the elements individually. Writing the entire list is clearly preferable because it avoids the need to loop through the list elements. The code becomes easier to write and easier to read.

The classes ObjectInputStream and ObjectOutputStream are adapter classes. Figure 7-6 shows the class diagram that illustrates how ObjectInputStream conforms to the adapter pattern. The class diagram for ObjectOutputStream is analogous.
../images/470600_1_En_7_Chapter/470600_1_En_7_Fig6_HTML.jpg
Figure 7-6

ObjectInputStream as an adapter

The class ObjectInputStream is implemented similarly to the adapter class InputStreamReader. An ObjectInputStream object holds a reference to the InputStream object it wraps, and uses the methods of that object to implement its methods. The implementation of ObjectOutputStream is similar.

The implementation of the DataInput and DataOutput methods is straightforward. For example, the writeInt method extracts the four bytes from the given int value and writes them to the byte stream. The readInt method reads four bytes from the byte stream and converts them to an int.

The implementation of readObject and writeObject is much more difficult. The writeObject method needs to encode enough information about the object to enable it to be reconstructed. This information includes metadata about the object’s class and the values of its state variables. If the object references another object z then the information about z must also be written to the byte stream. And if the object has multiple references to z (either directly or indirectly) then the method must ensure that z is written only once to the stream. This process of encoding an object as a sequence of bytes is called object serialization .

In general, the algorithm to serialize an object may need to write several objects to the byte stream. The writeObject method first creates a graph of all the objects reachable from the given object; it then systematically traverses the graph, writing the byte representation of each object to the stream. The readObject method performs the reverse operation. Further details of the writeObject and readObject algorithms are well beyond the scope of this book.

Saving State in the Banking Demo

Object streams are a particularly good way to save the state of a program. Recall that the banking demo currently saves the bank’s account information to a file. The class that manages the reading and writing to the file is SavedBankInfo, whose code appeared in Listing 3-14. That class wrote the account information byte by byte; the coding was difficult and tedious.

The use of object streams can dramatically simplify the code for SavedBankInfo . Instead of writing (and reading) each byte of each value of each account to the file, it is now possible to write the entire account map with just a single call to writeObject. Listing 7-12 gives the new code, which is in version 16 of the banking demo.
public class SavedBankInfo {
   private String fname;
   private Map<Integer,BankAccount> accounts;
   private int nextaccount;
   public SavedBankInfo(String fname) {
      this.fname = fname;
      File f = new File(fname);
      if (!f.exists()) {
         accounts = new HashMap<Integer,BankAccount>();
         nextaccount = 0;
      }
      else {
         try (InputStream is = new FileInputStream(fname);
              ObjectInput ois = new ObjectInputStream(is)) {
            accounts =
                 (Map<Integer,BankAccount>) ois.readObject();
            nextaccount = ois.readInt();
         }
         catch (Exception 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);
            ObjectOutput oos = new ObjectOutputStream(os)) {
         oos.writeObject(map);
         oos.writeInt(nextaccount);
      }
      catch (IOException ex) {
         throw new RuntimeException("file write exception");
      }
   }
}
Listing 7-12

The Version 16 SavedBankInfo Class

The saved file will contain two objects: the account map and the next account number. The constructor reads the map and account number from the saved file if they exist; otherwise it creates an empty map and sets the account number to 0. The method saveMap writes the specified map and number to the saved file, overwriting any previous file contents.

The writeObject method has one additional requirement: The object that it writes and all objects referenced by that object must be serializable. That is, they must implement the interface Serializable. Most classes from the Java library are serializable. If you want your classes to be serializable, you must declare them to implement Serializable.

The Serializable interface is unusual in that it has no methods. The point of the interface is to act as an “ok to write” flag. The issue is that a class may contain sensitive information in its private fields (such as passwords, salaries, or bank balances). Serializing that object will make that private information visible to anyone having access to the file, which could have unintended consequences. Thus the programmer is required to “sign off” on the serialization.

Returning to the version 16 banking demo, note that the argument to the writeObject method is the account map. The HashMap and Integer classes are already serializable. The only other components of the map are the bank account classes. You can make them be serializable by having BankAccount implement Serializable, as shown in Listing 7-13.
public interface BankAccount
       extends Comparable<BankAccount>, Serializable {
   ...
}
Listing 7-13

The Version 16 BankAccount Interface

In addition, any object referenced by a bank account must also be serializable. The AbstractBankAccount class has a reference to OwnerStrategy objects, and so you should also declare the OwnerStrategy interface to be serializable. Currently the only thing that implements OwnerStrategy is the enum Owners. Enums are serializable in Java by default, so technically OwnerStrategy does not need to be explicitly serializable. But it is good practice to declare it anyway, in case you modify the implementation of OwnerStrategy in the future.

Adapters for the Banking Demo

Another use of adapters is to combine information from different classes into a single list, even though the classes might have no common interface. The idea is to create an adapter for each class such that the adapters implement the same interface. The resulting adapted objects can then be placed into a list.

As an example, consider the following scenario related to the banking demo. Suppose that the FBI is investigating money laundering, and wants to see information about foreign-owned accounts having a balance over $10,000. Moreover, the FBI is interested in loans as well as bank accounts, where the “balance” of a loan is considered to be its remaining principal. The FBI wants this information to be stored as a list of FBIAcctInfo objects, where FBIAcctInfo is the interface shown in Listing 7-14.
interface FBIAcctInfo {
   int balance();     // in dollars
   boolean isForeign();
   String acctType(); // "deposit" or "loan"
}
Listing 7-14

The FBIAcctInfo Interface

For the purposes of this example, version 16 of the banking demo needs to have a class Loan, which contains some rudimentary information about the bank’s loans. Listing 7-15 gives its code. The class has methods to return the current status of the loan—its balance, remaining payments, and the monthly payment amount—as well as a method to make the next payment.
public class Loan {
   private double balance, monthlyrate, monthlypmt;
   private int pmtsleft;
   private boolean isdomestic;
   public Loan(double amt, double yearlyrate,
               int numyears, boolean isdomestic) {
      this.balance = amt;
      pmtsleft = numyears * 12;
      this.isdomestic = isdomestic;
      monthlyrate = yearlyrate / 12.0;
      monthlypmt = (amt*monthlyrate) /
                   (1-Math.pow(1+monthlyrate, -pmtsleft));
   }
   public double remainingPrincipal() {
      return balance;
   }
   public int paymentsLeft() {
      return pmtsleft;
   }
   public boolean isDomestic() {
      return isdomestic;
   }
   public double monthlyPayment() {
      return monthlypmt;
   }
   public void makePayment() {
      balance = balance + (balance*monthlyrate) - monthlypmt;
      pmtsleft--;
   }
}
Listing 7-15

The Loan Class

To handle the FBI request, the bank needs to combine the bank account and the loan data under the FBIAcctInfo interface . The problem, of course, is that neither BankAccount nor Loan objects implement FBIAcctInfo. BankAccount has an isForeign method, but the corresponding Loan method is isDomestic. In addition, FBIAcctInfo wants its balance method to return a value in dollars, but BankAccount and Loan use different names for the corresponding method and store values in pennies. And neither class has a method corresponding to the acctType method.

The way to address this issue is to create adapter classes for BankAccount and Loan that implement FBIAcctInfo. You can then wrap the BankAccount and Loan objects with these adapters, and place the resulting FBIAcctInfo objects in a list for the FBI to analyze.

The adapter for BankAccount is called BankAccountAdapter and the adapter for Loan is called LoanAdapter . Their code appears in listings 7-16 and 7-17.
public class BankAccountAdapter implements FBIAcctInfo {
   private BankAccount ba;
   public BankAccountAdapter(BankAccount ba) {
      this.ba = ba;
   }
   public int balance() {
      return ba.getBalance() / 100;
   }
   public boolean isForeign() {
      return ba.isForeign();
   }
   public String acctType() {
      return "deposit";
   }
}
Listing 7-16

The BankAccountAdapter Class

public class LoanAdapter implements FBIAcctInfo {
   private Loan loan;
   public LoanAdapter(Loan loan) {
      this.loan = loan;
   }
   public int balance() {
      return (int) (loan.principalRemaining() / 100);
   }
   public boolean isForeign() {
      return !loan.isDomestic();
   }
   public String acctType() {
      return "loan";
   }
}
Listing 7-17

The LoanAdapter Class

Figure 7-7 shows the class diagram corresponding to these adapters. This figure also shows a class FBIClient , which creates an adapted FBIAcctInfo object for each account and loan and stores them in a list. Then it can process the list as needed; for simplicity, this code just counts the affected accounts. The code appears in Listing 7-18.
../images/470600_1_En_7_Chapter/470600_1_En_7_Fig7_HTML.jpg
Figure 7-7

The class diagam of the FBI scenario

public class FBIClient {
   public static void main(String[] args) {
      Bank b = getBank();
      // put account info into a single list
      List<FBIAcctInfo> L = new ArrayList<>();
      for (BankAccount ba : b)
         L.add(new BankAccountAdapter(ba));
      for (Loan ln : b.loans())
         L.add(new LoanAdapter(ln));
      // then process the list
      int count = 0;
      for (FBIAcctInfo a : L)
         if (a.isForeign() && a.balance() > 1000.0)
            count++;
      System.out.println("The count is " + count);
   }
   private static Bank getBank() {
      SavedBankInfo info = new SavedBankInfo("bank16.info");
      Map<Integer,BankAccount> accounts = info.getAccounts();
      int nextacct = info.nextAcctNum();
      return new Bank(accounts, nextacct);
   }
}
Listing 7-18

The FBIClient Class

Summary

A class is called a wrapper class if it has a dependency to a class D and implements its methods by calling D’s methods to do most (or all) of the work. One common use of a wrapper class is as an adapter. An adapter class uses its wrapped class to help it implement a similar interface. For example, the ArrayAsList class of Figure 7-3 implements a list by wrapping an array. The relationship between the adapter class, the class it wraps, and the interface it implements, is called the adapter pattern.

The byte stream classes in the Java library illustrate the value of adapters. The classes InputStream, OutputStream, and their subclasses provide ways to read and write bytes between various kinds of inputs and outputs. But byte-level operations are often too low-level to be practical, so Java has classes that support higher-level operations. The classes Reader, Writer, and their subclasses read and write characters, and the interfaces ObjectInput and ObjectOutput read and write objects and primitive values. The best way to implement these higher level operations is to use adapters.

In particular, the class InputStreamReader wraps any InputStream object and enables it to be read as a sequence of characters. Similarly, the adapter class ObjectInputStream enables any InputStream object to be read as a sequence of objects and values. These adapters only need to know how to encode a character (in the case of InputStreamReader) or an object (in the case of ObjectInputStream) as a sequence of bytes. Each adapter class can then let its wrapped InputStream object perform the rest of the work. The adapter classes OutputStreamWriter and ObjectOutputStream work similarly for their wrapped ObjectStream object.

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

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