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.
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.
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
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.
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 Simplified Implementation of the Arrays Class
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.
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 Code for the FileWriter Class
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 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.
A Simplified InputStreamReader Class
The Adapter StringReader
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.
Object Streams
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.
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 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 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.
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.
The FBIAcctInfo Interface
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 BankAccountAdapter Class
The LoanAdapter Class
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.