0% found this document useful (0 votes)
6 views15 pages

Design Java0796

This document discusses the design aspects of the Java standard I/O library, particularly focusing on the java.io package and its use of the decorator design pattern for stream processing. It emphasizes the modularity and flexibility of the I/O system, allowing programmers to extend functionality and handle various data types. The article also covers techniques for error handling and stream manipulation, providing insights into effective program design in Java.

Uploaded by

balaji.pvb
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
6 views15 pages

Design Java0796

This document discusses the design aspects of the Java standard I/O library, particularly focusing on the java.io package and its use of the decorator design pattern for stream processing. It emphasizes the modularity and flexibility of the I/O system, allowing programmers to extend functionality and handle various data types. The article also covers techniques for error handling and stream manipulation, providing insights into effective program design in Java.

Uploaded by

balaji.pvb
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Design Aspects of the Standard I/O Library

Design with Java:

QUOIN
1208 Massachusetts Avenue,
Suite 3
Cambridge, Massachusetts
02138

tel: 617.492.6461
fax: 617.492.6461
email: info@[Link]
web: [Link]
Design with Java

Design Aspects of the Standard I/O Library

by David M. Papurt and Jean Pierre LeJacq

printed in the Journal of Object-Oriented Programming, September 1996

Introduction

This is the first column in a series discussing design in the Java programming language that
will appear regularly here in JOOP. Each column will focus on one aspect of design; the
topics will be taken from a variety of areas: design elements of the standard Java library;
patterns for solving particular design problems adapted to the peculiarities of Java; and
fundamental Java language design issues.

Providing a variety of compile-time, load-time, and run-time error checks, Java is a


relatively safe programming language. Nontheless, experience shows that without
satisfactory attention paid to program design, brittle, non-extendible, and error ridden
programs likely emerge from a large programming effort, no matter what language is used.
This series of columns contain descriptions of techniques that enable program designers
and programmers to avoid design errors when writing programs in Java. The idea is to
provide designers with sophisticated methods that maximize flexibility, minimize
maintenance and debugging efforts, and enable extensions.

This first column on Design with Java covers design aspects of the Input/Output (I/O)
package [Link] included in the Java standard library; the structure and design philosophy
of the package are described, and techniques for extending processing and for handling
new data types are demonstrated.

Package Structure

The notion of a stream characterizes I/O in Java. A stream is an ordered sequence of data
items either read from some source or written to some destination possibly with processing
along the way. The Java I/O package abstracts the various and normally encountered
sources, destinations, and processing algorithms: large portions of Java programs can
operate on InputStream and OutputStream objects independently of whether the ultimate
data source or destination is, for example, a disk file, an in-core array, another thread, or a
file across the network and independently of whether the data is buffered, translated, or
otherwise processed.

The means of this abstraction is based on the patterns of design formed by the classes in the
[Link] package. Figure 1 shows the pattern of input classes; this configuration of classes is
sometimes called decorator or wrapper [1], and includes classes representing the ultimate
data source and classes that perform processing of stream data items. The crucial feature of
the decorator pattern is the reference source from subclass FilterInputStream to root class
InputStream. The arrangement of output classes is similar. The figure uses Object
Modeling Technique (OMT) notation [2] as enhanced for description of Design Patterns [1]
1
.

Figure 1: Java I/O input classes pattern.

This design is highly run-time configurable; the decorator pattern generates constellations
of objects that look like singly-linked lists and that mix and match ultimate data sources and
processing. Figure 2 contains three sample object constellations 2 : figure 2a contains an
object constellation that represents an ultimate data source of an in-core array without any
additional processing; figure 2b represents a separate thread as ultimate data source via a
PipedInputStream object with additional single byte pushback buffer processing; figure 2c
adds buffering and typed data interpretation to an unbuffered byte stream from a disk file.
Other example processing might uppercase a byte stream from across the network, or might
count the number of lines in a stream originating from a disk file.

Figures 2a-c: Sample input class object constellations.

In each case, an object of a class representing an ultimate data source terminates the list.
The constructor for each subclass of FilterInputStream (i.e., the processors or decorators)
requires an InputStream as argument to initialize reference source: either an ultimate data
source object or another processing class object. An arbitrary number of processing class
objects can decorate (i.e., can appear in front of) the ultimate data source. Java code for
generating the three sample streams in figure 2 appears below.
The program passes streamA, streamB, and streamC to methods that read directly from the
head decorator.

Filter Operation

An abstract class, InputStream declares the basic operations that all input stream classes
provide: three versions of read() read single bytes and byte arrays, blocking until input is
available; skip() skips over bytes in the stream; available() returns the number of bytes
available without blocking; and close() closes the stream and releases resources held by the
stream. A simplified class declaration appears below.

The single byte read() provides the single byte in the first 8 bits of its 32 bit return value.
All three versions of read() indicate the end of the data stream with a return value of -1.

FilterInputStream implements all of the above operations; these implementations simply


forward any request to the object referenced by source, so a direct FilterInputStream object
would be transparent 3 . For example, FilterInputStream's implementation of the single
byte read() operation along with its constructor implementation appear below.
A processing class inherits and normally overrides some of FilterInputStream's methods.
The normal structure of an overriding processing class method implementation includes the
following:

* A call to the overridden version of the method (which forwards the request to
source), and

* Additional processing that corresponds to the semantics of the processing class.

Consider class PushbackInputStream's implementation of the single byte read() method


along with its constructor and push back method unread() 4
.
The single byte read() above first checks the push back buffer, and returns the buffer
contents if it contains a valid byte; otherwise, the method simply returns the byte read()
from the InputStream object referenced by source. Other methods of class
PushbackInputStream also manipulate the push back buffer: the array versions of read()
likewise check the push back buffer, and possibly place buffer in the first position in the
byte array; skip() may ask source to skip one less byte than originally requested; and
available() may add one to the number of bytes that source has available.

Other [Link] processing classes add their own processing. BufferedInputStream, for
example, provides transparent data buffering. This class contains a multiple byte array
buffer so that nearly all read() requests can be filled directly from this buffer; it makes
requests to source for more bytes only when the buffer is empty. This is most useful when
the ultimate data source is a disk file, and a single read of a large block of bytes is
significantly more efficient than multiple reads of small blocks.

Extending Processing

By mimicking the [Link] processing classes, programmers can extend stream processing in
a straightforward way. Consider class UpperCaseInputStream below that subclasses
FilterInputStream:

This class operates on data streams assumed to comprise text, and insures that all alphabetic
characters are upper case when they reach the code manipulaing the stream.

UpperCaseInputStream is particulary simple, however more complex processing classes


are possible: class GrepInputStream could perform a grep-like search for patterns in text
streams, forwarding only those lines that match the pattern and consuming all others;
StatsInputStream could could accumulate statistics on, for example, the numbers of words,
lines, and punctuation, whitespace, and other characters in a text stream; stream
compression and decompression or stream encryption and unencryption processing classes
could be written in pairs, subclassing FilterOutputStream as well as FilterInputStream.
mark() and reset()

Base class InputStream defines three functions for marking a position in a stream so that
after some reading has taken place, the stream can be reset, and the just read bytes reread
another time.

When markSupported() returns true, then the stream implements mark() and reset(); reset()
will return the stream to the marked postion so long as no more than readlimit bytes have
been read() since the mark(). Methods mark() and reset() are useful in a context where the
type of the data in the stream is unknown when the stream is opened (for example a stream
might contain simple text vs. HTML text), and some amount of the stream must be
consumed in order to determine its type. Once the type of the data in the stream is
determined, the stream can be reset() and processed appropriately.

The idea of mark() and reset() is valuable, but the problem with the standard library
solution is that all input stream classes inherit this capability whether it is sensible or not. A
more modular and sensible solution removes the three methods above from class
InputStream (or disregards them) and implements the idea as a processing class.
Indeed, class MarkAndResetInputStream is semantically like [Link] class
PushbackInputStream with a larger push back buffer capacity.

Handling New Data Types

For the most part, this article has presented streams whose data items were limited to type
byte (or bytes interpreted as characters). However, a stream is an ordered sequence of data
items that can have arbitrary type; a data item read from or written to a stream is not
restricted to byte, but can have, in principle, any built-in, library, or user-defined type.
This section describes how to accommodate different types of data items in Java streams.

One approach represents all values, no matter their type, in text; the [Link] processing class
PrintStream realizes this approach for output streams.
Each method in class PrintStream translates its argument into a sequence of bytes (i.e.,
characters), and passes those bytes towards the ultimate data destination. The methods that
take an Object call toString() on their argument to perform the translation to text.

A second approach to accommodating different types in Java streams passes the binary
representation of the value (its raw bytes) towards the ultimate data destination or from the
ultimate data source. For example, consider [Link] processing class DataInputStream.

The methods do no translation of the representation beyond collecting and packaging


individual bytes from the source into higher level type values.

As it stands, only a fixed set of fundamental types can be handled directly by this approach
- only those types with corresponding methods in DataInputStream (and DataOutputStream
for output). An indirect approach that builds input and output methods on top of the
DataInputStream and DataOutputStream methods accommodates other, non-fundamental
types. For example, class DateIO below contains static methods read() and write() for the
I/O of [Link] class Date 5 .

A class X that includes a Date object would implement its static read(DataInputStream,X)
and static write(DataOutputStream,X) methods in part via calls to [Link]() and
[Link](). More generally, composite types would invariably implement their I/O
methods in terms of their components' I/O methods 6 .

References To Decorated Objects

Typically, stream creating code keeps a reference only to the head decorator, and passes
this reference to other methods that read from or write to the head. However, in some cases
the stream creating code may hold onto and manipulate a reference to an internal object
along the decorator list other than the head. This manipulation should almost never include
actual reads or writes, but instead should be limited to higher level, control operations.
Consider the highly decorated input stream below.
The program might mark() the beginning of the stream via the internal object mris, and then
consume some data from the DataInputStream dis in the process of determining the type of
data in the stream.

Once the type of data in the stream is known, the stream must be reset(), and then
processed appropriately from its beginning.

As a matter of good style, all read() operations are performed only to the head decorator in
the composite stream object; reading from an internal object like mris breaks the
uncomplicated illusion of a single object accessed via a single reference, and makes the
code more difficult to understand.

Summary

The [Link] stream package is highly modular. It uses the decorator design pattern of
classes to permit run-time configuration of singly linked lists that mix and match ultimate
data source and destination stream objects and processing stream objects. By mimicking the
[Link] processing classes, programmers can extend stream processing in a straightforward
and elegant way; nearly any stream processing can be embedded in a modular processing
class. A stream can be adapted to read() or write() data items of arbitrary type; a data item
read from or written to a stream is not restricted to type byte, but can have, in principle, any
built-in, library, or user-defined type.

Footnotes

1
In this notation, a rectangle represents a class, the class name appearing inside the
rectangle. The notation for inheritance is a triangle; the superclass connects by a line to the
triangle apex, and each subclass connects by individual lines to a line attached to the
triangle base. The notation for reference containment is a diamond adjacent the containing
class anchoring an arrow pointing at the referenced class.

2
A rounded corner rectangle represents an object; the object's class appears in parentheses.
An arrow anchored by a solid circle represents a reference instance.

3
PushbackInputStream implements a single-byte push back buffer useful for implementing
lexical analyzers [3]. Having read a byte terminating but not included in the current token,
the lexical analyzer can push this byte back onto the stream with unread () so that it is the
first byte read when processing the next token.

5
The new class DateIO is created to contain these methods rather than modifying the
standard library class Date, because standard library classes should never be modified
except by the vendor.

6
Though syntactically distinct, this approach is semantically similar to the approach taken
by C++ programmers when overloading operator>>() for input and operator<<() for
output.

Acknowledgments

The authors thank Ron LaBonte of SunU Training for his support.

References
[1] Gamma, E., R. Helm, R. Johnson, and J. Vlissides, Design Patterns: Elements of
Reusable Object-Oriented Software, Addison-Wesley, Reading, MA, 1995.

[2] Rumbaugh, J., M. Blaha, W. Premerlani, F. Eddy, and W. Lorensen, Object-Oriented


Modeling and Design, Prentice-Hall, Englewood Cliffs, NJ, 1991.

[3] Aho, A. V., R. Sethi, and J. D. Ullman, Compilers: Principles, Techniques, and
Tools, Addison-Wesley, Reading, MA, 1986.

Biographies

David Papurt is founder of David Papurt Associates, Cambridge, MA, a firm specializing
in Java, C++, and object-oriented analysis and design, and is Director of Technology at
Terasoft Technology Corp., Milford, MA. He is also author of the highly acclaimed text
Inside the Object Model: The Sensible Use of C++ (SIGS Books, 1995). He may be
contacted at 75310.1621@[Link].

Jean Pierre LeJacq is a founder of Quoin Inc., Cambridge, MA, a company that provides
consulting and training services for distributed computing. He may be contacted at
jplejacq@[Link].

------------------------------------------------------------------------

article presented with permission of David Papurt Associates


© Copyright QUOIN, 1997

You might also like