![]() |
James Thornton |
| Internet Business Consultant | Call Toll Free: 1 (800) 409-2501 |
| About James | My MySpace | Internet Marketing | Enron Loophole | Lock Bumping | Contact Me |
|---|
Creating a good
input/output (IO) system is one of the more difficult tasks for the language
designer.
This is evidenced by the number of
different approaches. The challenge seems to be in covering all eventualities.
Not only are there different kinds of IO that you want to communicate with
(files, the console, network connections), but you need to talk to them in a
wide variety of ways (sequential, random-access, binary, character, by lines, by
words, etc.).
The Java library designers attacked
the problem by creating lots of classes. In fact, there are so many classes for
Java’s IO system that it can be intimidating at first (ironically, the
Java IO design actually prevents an explosion of classes). There has also been a
significant change in the IO library between Java
1.0 and Java 1.1. Instead of
simply replacing the old library with a new one, the designers at Sun extended
the old library and added the new one alongside it. As a result you can
sometimes end up mixing the old and new libraries and creating even more
intimidating code.
This chapter will help you
understand the variety of IO classes in the standard Java library and how to use
them. The first portion of the chapter will introduce the “old” Java
1.0 IO stream library, since there is a significant
amount of existing code that uses that library. The remainder of the chapter
will introduce the new features in the Java 1.1 IO library. Note that when you
compile some of the code in the first part of the chapter with a Java 1.1
compiler you can get a “deprecated feature”
warning message at compile time. The code still works; the compiler is just
suggesting that you use certain new features that are described in the latter
part of this chapter. It is valuable, however, to see the difference between the
old and new way of doing things and that’s why it was left in – to
increase your understanding (and to allow you to read code written for Java
1.0).
The Java library classes for IO are
divided by input and output, as
you can see by looking at the online Java class hierarchy with your Web browser.
By inheritance, all classes derived from
InputStream have basic
methods called read( )
for reading a single byte or array of bytes. Likewise, all classes derived
from OutputStream have
basic methods called
write( ) for writing a
single byte or array of bytes. However, you won’t generally use these
methods; they exist so more sophisticated classes can use them as they provide a
more useful interface. Thus, you’ll rarely create your stream object by
using a single class, but instead will layer multiple objects together to
provide your desired functionality. The fact that you create more than one
object to create a single resulting stream is the primary reason that
Java’s stream library is confusing.
It’s helpful to categorize
the classes by their functionality. The library designers started by deciding
that all classes that had anything to do with input would be inherited from
InputStream and all classes that were associated with output would be
inherited from OutputStream.
InputStream’s job is
to represent classes that produce input from different sources. These sources
can be (and each has an associated subclass of InputStream):
In addition,
the FilterInputStream is also a type of InputStream, to provide a
base class for "decorator" classes that attach attributes or useful interfaces
to input streams. This is discussed later.
|
Class |
Function |
Constructor
Arguments |
|---|---|---|
|
How to use it |
||
|
ByteArray-InputStream |
Allows a buffer in memory to be
used as an InputStream. |
The buffer from which to extract
the bytes. |
|
As a source of data. Connect it to
a FilterInputStream object to provide a useful
interface. |
||
|
StringBuffer-InputStream |
Converts a String into an
InputStream. |
A String. The underlying
implementation actually uses a StringBuffer. |
|
As a source of data. Connect it to
a FilterInputStream object to provide a useful
interface. |
||
|
File-InputStream |
For reading information from a
file. |
A String representing the
file name, or a File or FileDescriptor object. |
|
As a source of data. Connect it to
a FilterInputStream object to provide a useful
interface. |
This category includes the classes
that decide where your output will go: an array of bytes (no String,
however; presumably you can create one using the array of bytes), a file, or a
“pipe.”
In addition, the
FilterOutputStream provides a base class for "decorator" classes that
attach attributes or useful interfaces to output streams. This is discussed
later.
The use of layered objects to
dynamically and transparently add responsibilities to individual objects is
referred to as the
decorator pattern.
(Patterns[44]
are the subject of Chapter 16.) The decorator pattern specifies that all objects
that wrap around your initial object have the same interface, to make the use of
the decorators transparent – you send the same message to an object
whether it’s been decorated or not. This is the reason for the existence
of the “filter” classes in the Java IO library: the abstract
“filter” class is the base class for all the decorators. (A
decorator must have the same interface as the object it decorates, but the
decorator can also extend the interface, which occurs in several of the
“filter” classes).
Decorators are often used when
subclassing requires a large number of subclasses to support every possible
combination needed – so many that subclassing becomes impractical. The
Java IO library requires many different combinations of features which is why
the decorator pattern is a good approach. There is a drawback to the decorator
pattern, however. Decorators give you much more flexibility while you’re
writing a program (since you can easily mix and match attributes), but they add
complexity to your code. The reason that the Java IO library is awkward to use
is that you must create many classes – the “core” IO type plus
all the decorators – in order to get the single IO object that you want.
The classes that provide the
decorator interface to control a particular InputStream or
OutputStream are the FilterInputStream and
FilterOutputStream – which don’t have very intuitive names.
They are derived, respectively, from InputStream and OutputStream,
and they are abstract classes, in theory to provide a common interface for all
the different ways you want to talk to a stream. In fact,
FilterInputStream and FilterOutputStream simply mimic their base
classes, which is the key requirement of the
decorator.
The FilterInputStream
classes accomplish two significantly different things. DataInputStream
allows you to read different types of primitive data as well as String
objects. (All the methods start with “read,” such as
readByte( ), readFloat( ), etc.) This, along with its
companion DataOutputStream, allows you to move primitive data from one
place to another via a stream. These “places” are determined by the
classes in Table 10-1. If you’re reading data in blocks and parsing it
yourself, you won’t need DataInputStream, but in most other cases
you will want to use it to automatically format the data you
read.
The remaining classes modify the
way an InputStream behaves internally: whether it’s buffered or
unbuffered, if it keeps track of the lines it’s reading (allowing you to
ask for line numbers or set the line number), and whether you can push back a
single character. The last two classes look a lot like support for building a
compiler (that is, they were added to support the construction of the Java
compiler), so you probably won’t use them in general programming.
You’ll probably need to
buffer your input almost every time, regardless of the IO device you’re
connecting to, so it would have made more sense for the IO library to make a
special case for unbuffered input rather than buffered input.
|
Class |
Function |
Constructor
Arguments |
|
How to use it |
||
|
Data-InputStream |
Used in concert with
DataOutputStream, so you can read primitives (int, char, long, etc.) from
a stream in a portable fashion. |
InputStream |
|
Contains a full interface to allow
you to read primitive types. |
The complement to
DataInputStream is DataOutputStream, which formats each of the
primitive types and String objects onto a stream in such a way that any
DataInputStream, on any machine, can read them. All the methods start
with “write,” such as writeByte( ),
writeFloat( ), etc.
If you want to do true formatted
output, for example, to the console, use a PrintStream. This is the
endpoint that allows you to print all of the primitive data types and
String objects in a viewable format as opposed to
DataOutputStream, whose goal is to put them on a stream in a way that
DataInputStream can portably reconstruct them. The System.out
static object is a PrintStream.
The two important methods in
PrintStream are print( ) and println( ), which
are overloaded to print out all the various types. The difference between
print( ) and println( ) is that the latter adds a
newline when it’s done.
BufferedOutputStream is a
modifier and tells the stream to use buffering so you don’t get a physical
write every time you write to the stream. You’ll probably always want to
use this with files, and possibly console IO.
RandomAccessFile is used for
files containing records of known size so that you can move from one record to
another using
seek( ), then read
or change the records. The records don’t have to be the same size; you
just have to be able to determine how big they are and where they are placed in
the file.
At first it’s a little bit
hard to believe that RandomAccessFile is not part of the
InputStream or OutputStream hierarchy. It has no association with
those hierarchies other than that it happens to implement the
DataInput and
DataOutput interfaces
(which are also implemented by DataInputStream and
DataOutputStream). It doesn’t even use any of the functionality of
the existing InputStream or OutputStream classes –
it’s a completely separate class, written from scratch, with all of its
own (mostly native) methods. The reason for this may be that
RandomAccessFile has essentially different behavior than the other IO
types, since you can move forward and backward within a file. In any event, it
stands alone, as a direct descendant of Object.
Essentially, a
RandomAccessFile works like a DataInputStream pasted together with
a DataOutputStream and the methods getFilePointer( ) to find
out where you are in the file, seek( ) to move to a new point in the
file, and length( ) to determine the maximum size of the file. In
addition, the constructors require a second argument (identical to
fopen( ) in C) indicating whether you are just randomly reading
(“r”) or reading and writing (“rw”).
There’s no support for write-only files, which could suggest that
RandomAccessFile might have worked well if it were inherited from
DataInputStream.
What’s even more frustrating
is that you could easily imagine wanting to seek within other types of streams,
such as a ByteArrayInputStream, but the seeking methods are available
only in RandomAccessFile, which works for files only.
BufferedInputStream does allow you to
mark( ) a position
(whose value is held in a single internal variable) and
reset( ) to that
position, but this is limited and not too
useful.
The
File class has a
deceiving name – you might think it refers to a file, but it
doesn’t. It can represent either the name of a particular file or
the names of a set of files in a directory. If it’s a set of files,
you can ask for the set with the
list( ) method, and
this returns an array of String. It makes sense to return an array rather
than one of the flexible collection classes because the number of elements is
fixed, and if you want a different directory listing you just create a different
File object. In fact, “FilePath” would have been a better
name. This section shows a complete example of the use of this class, including
the associated
FilenameFilter
interface.
Suppose you’d like to see a
directory listing. The File object can be listed in two ways. If you call
list( ) with no arguments, you’ll get the full list that the
File object contains. However, if you want a restricted list, for
example, all of the files with an extension of .java, then you use a
“directory filter,” which is a class that tells how to select the
File objects for display.
Here’s the code for the
example:
//: DirList.java
// Displays directory listing
package c10;
import java.io.*;
public class DirList {
public static void main(String[] args) {
try {
File path = new File(".");
String[] list;
if(args.length == 0)
list = path.list();
else
list = path.list(new DirFilter(args[0]));
for(int i = 0; i < list.length; i++)
System.out.println(list[i]);
} catch(Exception e) {
e.printStackTrace();
}
}
}
class DirFilter implements FilenameFilter {
String afn;
DirFilter(String afn) { this.afn = afn; }
public boolean accept(File dir, String name) {
// Strip path information:
String f = new File(name).getName();
return f.indexOf(afn) != -1;
}
} ///:~
The DirFilter class
“implements” the interface FilenameFilter. (Interfaces
were covered in Chapter 7.) It’s useful to see how simple the
FilenameFilter interface is:
public interface FilenameFilter {
boolean accept(File dir, String name);
}
It says that all that this type of
object does is provide a method called accept( ). The whole reason
behind the creation of this class is to provide the accept( ) method
to the list( ) method so that list( ) can call
back accept( ) to determine which file names should be included
in the list. Thus, this technique is often referred to as a
callback or sometimes a
functor (that is, DirFilter is a functor
because its only job is to hold a method). Because list( ) takes a
FilenameFilter object as its argument, it means that you can pass an
object of any class that implements FilenameFilter to choose (even at
run-time) how the list( ) method will behave. The purpose of a
callback is to provide flexibility in the behavior of code.
DirFilter shows that just
because an interface contains only a set of methods, you’re not
restricted to writing only those methods. (You must at least provide definitions
for all the methods in an interface, however.) In this case, the
DirFilter constructor is also created.
The accept( ) method
must accept a File object representing the directory that a particular
file is found in, and a String containing the name of that file. You
might choose to use or ignore either of these arguments, but you will probably
at least use the file name. Remember that the list( ) method is
calling accept( ) for each of the file names in the directory object
to see which one should be included – this is indicated by the
boolean result returned by accept( ).
To make sure that what you’re
working with is only the name and contains no path information, all you have to
do is take the String object and create a File object out of it,
then call getName( ) which strips away all the path information (in
a platform-independent way). Then accept( ) uses the
String class
indexOf( ) method to see if the search string afn appears
anywhere in the name of the file. If afn is found within the string, the
return value is the starting index of afn, but if it’s not found
the return value is -1. Keep in mind that this is a simple string search and
does not have regular expression “wildcard” matching such as
“fo?.b?r*” which is much more difficult to
implement.
The list( ) method
returns an array. You can query this array for its length and then move through
it selecting the array elements. This ability to easily pass an array in and out
of a method is a tremendous improvement over the behavior of C and
C++.
This example is ideal for rewriting
using an
anonymous
inner class (described in Chapter 7). As a first cut, a method filter( )
is created that returns a handle to a
FilenameFilter:
//: DirList2.java
// Uses Java 1.1 anonymous inner classes
import java.io.*;
public class DirList2 {
public static FilenameFilter
filter(final String afn) {
// Creation of anonymous inner class:
return new FilenameFilter() {
String fn = afn;
public boolean accept(File dir, String n) {
// Strip path information:
String f = new File(n).getName();
return f.indexOf(fn) != -1;
}
}; // End of anonymous inner class
}
public static void main(String[] args) {
try {
File path = new File(".");
String[] list;
if(args.length == 0)
list = path.list();
else
list = path.list(filter(args[0]));
for(int i = 0; i < list.length; i++)
System.out.println(list[i]);
} catch(Exception e) {
e.printStackTrace();
}
}
} ///:~
Note that the argument to
filter( ) must be
final. This is required
by the anonymous inner class so that it can use an object from outside its
scope.
This design is an improvement
because the FilenameFilter class is now tightly bound to DirList2.
However, you can take this approach one step further and define the anonymous
inner class as an argument to list( ), in which case it’s even
smaller:
//: DirList3.java
// Building the anonymous inner class "in-place"
import java.io.*;
public class DirList3 {
public static void main(final String[] args) {
try {
File path = new File(".");
String[] list;
if(args.length == 0)
list = path.list();
else
list = path.list(
new FilenameFilter() {
public boolean
accept(File dir, String n) {
String f = new File(n).getName();
return f.indexOf(args[0]) != -1;
}
});
for(int i = 0; i < list.length; i++)
System.out.println(list[i]);
} catch(Exception e) {
e.printStackTrace();
}
}
} ///:~
The argument to main( )
is now final, since the anonymous inner class uses args[0]
directly.
This shows you how anonymous inner
classes allow the creation of quick-and-dirty classes to solve problems. Since
everything in Java revolves around classes, this can be a useful coding
technique. One benefit is that it keeps the code that solves a particular
problem isolated together in one spot. On the other hand, it is not always as
easy to read, so you must use it judiciously.
Ah, you say that you want the file
names sorted? Since there’s no support for sorting in Java 1.0 or
Java 1.1 (although sorting is included in Java
1.2), it will have to be added into the program directly
using the SortVector created in Chapter 8:
//: SortedDirList.java
// Displays sorted directory listing
import java.io.*;
import c08.*;
public class SortedDirList {
private File path;
private String[] list;
public SortedDirList(final String afn) {
path = new File(".");
if(afn == null)
list = path.list();
else
list = path.list(
new FilenameFilter() {
public boolean
accept(File dir, String n) {
String f = new File(n).getName();
return f.indexOf(afn) != -1;
}
});
sort();
}
void print() {
for(int i = 0; i < list.length; i++)
System.out.println(list[i]);
}
private void sort() {
StrSortVector sv = new StrSortVector();
for(int i = 0; i < list.length; i++)
sv.addElement(list[i]);
// The first time an element is pulled from
// the StrSortVector the list is sorted:
for(int i = 0; i < list.length; i++)
list[i] = sv.elementAt(i);
}
// Test it:
public static void main(String[] args) {
SortedDirList sd;
if(args.length == 0)
sd = new SortedDirList(null);
else
sd = new SortedDirList(args[0]);
sd.print();
}
} ///:~
A few other improvements have been
made. Instead of creating path and list as local variables to
main( ), they are members of the class so their values can be
accessible for the lifetime of the object. In fact, main( ) is now
just a way to test the class. You can see that the constructor of the class
automatically sorts the list once that list has been created.
The sort is case-insensitive so you
don’t end up with a list of all the words starting with capital letters,
followed by the rest of the words starting with all the lowercase letters.
However, you’ll notice that within a group of file names that begin with
the same letter the capitalized words are listed first, which is still not quite
the desired behavior for the sort. This problem will be fixed in Java
1.2.
The File class is more than
just a representation for an existing directory path, file, or group of files.
You can also use a File object to create a new
directory or an entire directory
path if it doesn’t exist. You can also look at the
characteristics of files (size,
last modification date, read/write), see whether a File object represents
a file or a directory, and delete a file. This program shows the remaining
methods available with the File class:
//: MakeDirectories.java
// Demonstrates the use of the File class to
// create directories and manipulate files.
import java.io.*;
public class MakeDirectories {
private final static String usage =
"Usage:MakeDirectories path1 ...\n" +
"Creates each path\n" +
"Usage:MakeDirectories -d path1 ...\n" +
"Deletes each path\n" +
"Usage:MakeDirectories -r path1 path2\n" +
"Renames from path1 to path2\n";
private static void usage() {
System.err.println(usage);
System.exit(1);
}
private static void fileData(File f) {
System.out.println(
"Absolute path: " + f.getAbsolutePath() +
"\n Can read: " + f.canRead() +
"\n Can write: " + f.canWrite() +
"\n getName: " + f.getName() +
"\n getParent: " + f.getParent() +
"\n getPath: " + f.getPath() +
"\n length: " + f.length() +
"\n lastModified: " + f.lastModified());
if(f.isFile())
System.out.println("it's a file");
else if(f.isDirectory())
System.out.println("it's a directory");
}
public static void main(String[] args) {
if(args.length < 1) usage();
if(args[0].equals("-r")) {
if(args.length != 3) usage();
File
old = new File(args[1]),
rname = new File(args[2]);
old.renameTo(rname);
fileData(old);
fileData(rname);
return; // Exit main
}
int count = 0;
boolean del = false;
if(args[0].equals("-d")) {
count++;
del = true;
}
for( ; count < args.length; count++) {
File f = new File(args[count]);
if(f.exists()) {
System.out.println(f + " exists");
if(del) {
System.out.println("deleting..." + f);
f.delete();
}
}
else { // Doesn't exist
if(!del) {
f.mkdirs();
System.out.println("created " + f);
}
}
fileData(f);
}
}
} ///:~
In fileData( ) you can
see the various file investigation methods put to use to display information
about the file or directory path.
The first method that’s
exercised by main( ) is
renameTo( ), which
allows you to rename (or move) a file to an entirely new path represented by the
argument, which is another File object. This also works with directories
of any length.
If you experiment with the above
program, you’ll find that you can make a directory path of any complexity
because mkdirs( )
will do all the work for you. In Java 1.0, the -d
flag reports that the directory is deleted but it’s still there; in Java
1.1 the directory is actually
deleted.
Although there are a lot of IO
stream classes in the library that can be combined in many different ways, there
are just a few ways that you’ll probably end up using them. However, they
require attention to get the correct combinations. The following rather long
example shows the creation and use of typical IO
configurations so you can use it as a reference when writing your own code. Note
that each configuration begins with a commented number and title that
corresponds to the heading for the appropriate explanation that follows in the
text.
//: IOStreamDemo.java
// Typical IO Stream Configurations
import java.io.*;
import com.bruceeckel.tools.*;
public class IOStreamDemo {
public static void main(String[] args) {
try {
// 1. Buffered input file
DataInputStream in =
new DataInputStream(
new BufferedInputStream(
new FileInputStream(args[0])));
String s, s2 = new String();
while((s = in.readLine())!= null)
s2 += s + "\n";
in.close();
// 2. Input from memory
StringBufferInputStream in2 =
new StringBufferInputStream(s2);
int c;
while((c = in2.read()) != -1)
System.out.print((char)c);
// 3. Formatted memory input
try {
DataInputStream in3 =
new DataInputStream(
new StringBufferInputStream(s2));
while(true)
System.out.print((char)in3.readByte());
} catch(EOFException e) {
System.out.println(
"End of stream encountered");
}
// 4. Line numbering & file output
try {
LineNumberInputStream li =
new LineNumberInputStream(
new StringBufferInputStream(s2));
DataInputStream in4 =
new DataInputStream(li);
PrintStream out1 =
new PrintStream(
new BufferedOutputStream(
new FileOutputStream(
"IODemo.out")));
while((s = in4.readLine()) != null )
out1.println(
"Line " + li.getLineNumber() + s);
out1.close(); // finalize() not reliable!
} catch(EOFException e) {
System.out.println(
"End of stream encountered");
}
// 5. Storing & recovering data
try {
DataOutputStream out2 =
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("Data.txt")));
out2.writeBytes(
"Here's the value of pi: \n");
out2.writeDouble(3.14159);
out2.close();
DataInputStream in5 =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("Data.txt")));
System.out.println(in5.readLine());
System.out.println(in5.readDouble());
} catch(EOFException e) {
System.out.println(
"End of stream encountered");
}
// 6. Reading/writing random access files
RandomAccessFile rf =
new RandomAccessFile("rtest.dat", "rw");
for(int i = 0; i < 10; i++)
rf.writeDouble(i*1.414);
rf.close();
rf =
new RandomAccessFile("rtest.dat", "rw");
rf.seek(5*8);
rf.writeDouble(47.0001);
rf.close();
rf =
new RandomAccessFile("rtest.dat", "r");
for(int i = 0; i < 10; i++)
System.out.println(
"Value " + i + ": " +
rf.readDouble());
rf.close();
// 7. File input shorthand
InFile in6 = new InFile(args[0]);
String s3 = new String();
System.out.println(
"First line in file: " +
in6.readLine());
in6.close();
// 8. Formatted file output shorthand
PrintFile out3 = new PrintFile("Data2.txt");
out3.print("Test of PrintFile");
out3.close();
// 9. Data file output shorthand
OutFile out4 = new OutFile("Data3.txt");
out4.writeBytes("Test of outDataFile\n\r");
out4.writeChars("Test of outDataFile\n\r");
out4.close();
} catch(FileNotFoundException e) {
System.out.println(
"File Not Found:" + args[0]);
} catch(IOException e) {
System.out.println("IO Exception");
}
}
} ///:~
Of course, one common thing
you’ll want to do is print formatted output to the console, but
that’s already been simplified in the package com.bruceeckel.tools
created in Chapter 5.
Parts 1 through 4 demonstrate the
creation and use of input streams (although part 4 also shows the simple use of
an output stream as a testing tool).
To open a file for input, you use a
FileInputStream with a
String or a File object as the file name. For speed, you’ll
want that file to be buffered so you give the resulting handle to the
constructor for a
BufferedInputStream. To
read input in a formatted fashion, you give that resulting handle to the
constructor for a
DataInputStream, which is
your final object and the interface you read from.
In this example, only the
readLine( ) method
is used, but of course any of the DataInputStream methods are available.
When you reach the end of the file, readLine( ) returns null
so that is used to break out of the while loop.
The String s2 is used to
accumulate the entire contents of the file (including newlines that must be
added since readLine( ) strips them off). s2 is then used in
the later portions of this program. Finally, close( ) is called to
close the file. Technically, close( ) will be called when
finalize( ) is run, and this is supposed to happen (whether or not
garbage collection occurs) as the program exits. However, Java
1.0 has a rather important bug, so this doesn’t
happen. In Java 1.1 you must explicitly call
System.runFinalizersOnExit(true) to guarantee
that finalize( ) will be called for every object in the system. The
safest approach is to explicitly call
close( ) for
files.
This piece takes the String
s2 that now contains the entire contents of the file and uses it to create a
StringBufferInputStream.
(A String, not a StringBuffer, is required
as the constructor argument.) Then read( ) is used to read each
character one at a time and send it out to the console. Note that
read( ) returns the next byte as an int and thus it must be
cast to a char to print properly.
The interface for
StringBufferInputStream is limited, so you usually enhance it by wrapping
it inside a
DataInputStream. However,
if you choose to read the characters out a byte at a time using
readByte( ), any value is valid so the return value cannot be used
to detect the end of input. Instead, you can use the
available( ) method
to find out how many more characters are available. Here’s an example that
shows how to read a file one byte at a time:
//: TestEOF.java
// Testing for the end of file while reading
// a byte at a time.
import java.io.*;
public class TestEOF {
public static void main(String[] args) {
try {
DataInputStream in =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("TestEof.java")));
while(in.available() != 0)
System.out.print((char)in.readByte());
} catch (IOException e) {
System.err.println("IOException");
}
}
} ///:~
Note that available( )
works differently depending on what sort of medium you’re reading from
– it’s literally “the number of bytes that can be read
without blocking.”
With a file this means the whole file, but with a different kind of stream this
might not be true, so use it thoughtfully.
You could also detect the end of
input in cases like these by catching an exception. However, the use of
exceptions for control flow is considered a misuse of that
feature.
This example shows the use of the
LineNumberInputStream to
keep track of the input line numbers. Here, you cannot simply gang all the
constructors together, since you have to keep a handle to the
LineNumberInputStream. (Note that this is not an inheritance
situation, so you cannot simply cast in4 to a
LineNumberInputStream.) Thus, li holds the handle to the
LineNumberInputStream, which is then used to create a
DataInputStream for easy reading.
This example also shows how to
write formatted data to a file. First, a
FileOutputStream is
created to connect to the file. For efficiency, this is made a
BufferedOutputStream,
which is what you’ll virtually always want to do, but you’re forced
to do it explicitly. Then for the formatting it’s turned into a
PrintStream. The data
file created this way is readable as an ordinary text file.
One of the methods that indicates
when a DataInputStream is
exhausted is
readLine( ), which
returns null when there are no more strings to read. Each line is printed
to the file along with its line number, which is acquired through
li.
You’ll see an explicit
close( ) for out1, which would make sense if the
program were to turn around and read the same file again. However, this program
ends without ever looking at the file IODemo.out. As mentioned before, if
you don’t call close( ) for all your output files, you might
discover that the buffers don’t get flushed so they’re
incomplete.
The two primary kinds of output
streams are separated by the way they write data: one writes it for human
consumption, and the other writes it to be re-acquired by a
DataInputStream. The
RandomAccessFile stands
alone, although its data format is compatible with the DataInputStream
and
DataOutputStream.
A
PrintStream formats data
so it’s readable by a human. To output data so that it can be recovered by
another stream, you use a DataOutputStream to write the data and a
DataInputStream to recover the data. Of course, these streams could be
anything, but here a file is used, buffered for both reading and
writing.
Note that the character string is
written using
writeBytes( ) and
not writeChars( ).
If you use the latter, you’ll be writing the 16-bit Unicode characters.
Since there is no complementary “readChars” method in
DataInputStream, you’re stuck pulling these characters off one at a
time with
readChar( ). So for
ASCII, it’s easier to write the characters as bytes followed by a newline;
then use readLine( )
to read back the bytes as a regular ASCII line.
The
writeDouble( )
stores the double number to the stream and the complementary
readDouble( )
recovers it. But for any of the reading methods to work correctly, you must know
the exact placement of the data item in the stream, since it would be equally
possible to read the stored double as a simple sequence of bytes, or as a
char, etc. So you must either have a fixed format for the data in the
file or extra information must be stored in the file that you parse to determine
where the data is located.
As previously noted, the
RandomAccessFile is almost totally isolated from the rest of the IO
hierarchy, save for the fact that it implements the DataInput and
DataOutput interfaces. So you cannot combine it with any of the aspects
of the InputStream and OutputStream subclasses. Even though it
might make sense to treat a ByteArrayInputStream as a random access
element, you can use RandomAccessFile to only open a file. You must
assume a RandomAccessFile is properly buffered since you cannot add
that.
The one option you have is in the
second constructor argument: you can open a RandomAccessFile to read
(“r”) or read and write
(“rw”).
Using a RandomAccessFile is
like using a combined DataInputStream and DataOutputStream
(because it implements the equivalent interfaces). In addition, you can see that
seek( ) is used to
move about in the file and change one of the
values.
Since there are certain canonical
forms that you’ll be using regularly with files, you may wonder why you
have to do all of that typing – this is one of the drawbacks of the
decorator pattern. This portion shows the creation and use of shorthand versions
of typical file reading and writing configurations. These shorthands are placed
in the package com.bruceeckel.tools that was begun in Chapter 5
(See page 196). To add each class to the library, simply place it in the
appropriate directory and add the package statement.
The creation of an object that
reads a file from a buffered DataInputStream can be encapsulated into a
class called InFile:
//: InFile.java
// Shorthand class for opening an input file
package com.bruceeckel.tools;
import java.io.*;
public class InFile extends DataInputStream {
public InFile(String filename)
throws FileNotFoundException {
super(
new BufferedInputStream(
new FileInputStream(filename)));
}
public InFile(File file)
throws FileNotFoundException {
this(file.getPath());
}
} ///:~
Both the String versions of
the constructor and the File versions are included, to parallel the
creation of a FileInputStream.
Now you can reduce your chances of
repetitive stress syndrome while creating files, as seen in the
example.
The same kind of approach can be
taken to create a PrintStream that writes to a buffered file.
Here’s the extension to com.bruceeckel.tools:
//: PrintFile.java
// Shorthand class for opening an output file
// for human-readable output.
package com.bruceeckel.tools;
import java.io.*;
public class PrintFile extends PrintStream {
public PrintFile(String filename)
throws IOException {
super(
new BufferedOutputStream(
new FileOutputStream(filename)));
}
public PrintFile(File file)
throws IOException {
this(file.getPath());
}
} ///:~
Note that it is not possible for a
constructor to catch an exception that’s thrown by a base-class
constructor.
Finally, the same kind of shorthand
can create a buffered output file for data storage (as opposed to human-readable
storage):
//: OutFile.java
// Shorthand class for opening an output file
// for data storage.
package com.bruceeckel.tools;
import java.io.*;
public class OutFile extends DataOutputStream {
public OutFile(String filename)
throws IOException {
super(
new BufferedOutputStream(
new FileOutputStream(filename)));
}
public OutFile(File file)
throws IOException {
this(file.getPath());
}
} ///:~
It is curious (and unfortunate)
that the Java library designers didn’t think to provide these conveniences
as part of their
standard.
Following the approach pioneered in
Unix of “standard input,” “standard output,” and
“standard error output,” Java has
System.in,
System.out, and
System.err. Throughout
the book you’ve seen how to write to standard output using
System.out, which is already pre-wrapped as a PrintStream object.
System.err is likewise a PrintStream, but System.in is a
raw InputStream, with no wrapping. This means that while you can use
System.out and System.err right away, System.in must be
wrapped before you can read from it.
Typically, you’ll want to
read input a line at a time using readLine( ), so you’ll want
to wrap System.in in a DataInputStream. This is the
“old” Java 1.0 way to do line input. A bit
later in the chapter you’ll see the Java 1.1
solution. Here’s an example that simply echoes each line that you type
in:
//: Echo.java
// How to read from standard input
import java.io.*;
public class Echo {
public static void main(String[] args) {
DataInputStream in =
new DataInputStream(
new BufferedInputStream(System.in));
String s;
try {
while((s = in.readLine()).length() != 0)
System.out.println(s);
// An empty line terminates the program
} catch(IOException e) {
e.printStackTrace();
}
}
} ///:~
The reason for the try block
is that readLine( )
can throw an IOException. Note that System.in should also be
buffered, as with most streams
It’s a bit inconvenient that
you’re forced to wrap System.in in a DataInputStream in each
program, but perhaps it was designed this way to allow maximum
flexibility.
The
PipedInputStream and
PipedOutputStream have
been mentioned only briefly in this chapter. This is not to suggest that they
aren’t useful, but their value is not apparent until you begin to
understand multithreading, since the piped streams are used to communicate
between threads. This is covered along with an example in Chapter
14.
Although
StreamTokenizer is not
derived from InputStream or OutputStream, it works only with
InputStream objects, so it rightfully belongs in the IO portion of the
library.
The StreamTokenizer class is
used to break any InputStream into a sequence of
“tokens,” which are bits of text delimited by whatever you choose.
For example, your tokens could be words, and then they would be delimited by
white space and punctuation.
Consider a program to count the
occurrence of words in a text file:
//: SortedWordCount.java
// Counts words in a file, outputs
// results in sorted form.
import java.io.*;
import java.util.*;
import c08.*; // Contains StrSortVector
class Counter {
private int i = 1;
int read() { return i; }
void increment() { i++; }
}
public class SortedWordCount {
private FileInputStream file;
private StreamTokenizer st;
private Hashtable counts = new Hashtable();
SortedWordCount(String filename)
throws FileNotFoundException {
try {
file = new FileInputStream(filename);
st = new StreamTokenizer(file);
st.ordinaryChar('.');
st.ordinaryChar('-');
} catch(FileNotFoundException e) {
System.out.println(
"Could not open " + filename);
throw e;
}
}
void cleanup() {
try {
file.close();
} catch(IOException e) {
System.out.println(
"file.close() unsuccessful");
}
}
void countWords() {
try {
while(st.nextToken() !=
StreamTokenizer.TT_EOF) {
String s;
switch(st.ttype) {
case StreamTokenizer.TT_EOL:
s = new String("EOL");
break;
case StreamTokenizer.TT_NUMBER:
s = Double.toString(st.nval);
break;
case StreamTokenizer.TT_WORD:
s = st.sval; // Already a String
break;
default: // single character in ttype
s = String.valueOf((char)st.ttype);
}
if(counts.containsKey(s))
((Counter)counts.get(s)).increment();
else
counts.put(s, new Counter());
}
} catch(IOException e) {
System.out.println(
"st.nextToken() unsuccessful");
}
}
Enumeration values() {
return counts.elements();
}
Enumeration keys() { return counts.keys(); }
Counter getCounter(String s) {
return (Counter)counts.get(s);
}
Enumeration sortedKeys() {
Enumeration e = counts.keys();
StrSortVector sv = new StrSortVector();
while(e.hasMoreElements())
sv.addElement((String)e.nextElement());
// This call forces a sort:
return sv.elements();
}
public static void main(String[] args) {
try {
SortedWordCount wc =
new SortedWordCount(args[0]);
wc.countWords();
Enumeration keys = wc.sortedKeys();
while(keys.hasMoreElements()) {
String key = (String)keys.nextElement();
System.out.println(key + ": "
+ wc.getCounter(key).read());
}
wc.cleanup();
} catch(Exception e) {
e.printStackTrace();
}
}
} ///:~
It makes sense to present these in
a sorted form, but since Java 1.0 and Java
1.1 don’t have any sorting methods, that will have
to be mixed in. This is easy enough to do with a StrSortVector. (This was
created in Chapter 8, and is part of the package created in that chapter.
Remember that the starting directory for all the subdirectories in this book
must be in your class path for the program to compile
successfully.)
To open the file, a
FileInputStream is used, and to turn the file into words a
StreamTokenizer is created from the FileInputStream. In
StreamTokenizer, there is a default list of separators, and you can add
more with a set of methods. Here, ordinaryChar( ) is used to say
“This character has no significance that I’m interested in,”
so the parser doesn’t include it as part of any of the words that it
creates. For example, saying st.ordinaryChar('.') means that periods will
not be included as parts of the words that are parsed. You can find more
information in the online documentation that comes with Java.
In countWords( ), the
tokens are pulled one at a time from the stream, and the ttype
information is used to determine what to do with each token, since a token can
be an end-of-line, a number, a string, or a single character.
Once a token is found, the
Hashtable counts is queried to see if it already
contains the token as a key. If it does, the corresponding Counter object
is incremented to indicate that another instance of this word has been found. If
not, a new Counter is created – since the Counter
constructor initializes its value to one, this also acts to count the
word.
SortedWordCount is not a
type of Hashtable, so it wasn’t inherited. It performs a specific
type of functionality, so even though the keys( ) and
values( ) methods must be re-exposed, that still doesn’t mean
that inheritance should be used
since a number of Hashtable methods are inappropriate here. In addition,
other methods like getCounter( ), which get the Counter for a
particular String, and sortedKeys( ), which produces an
Enumeration, finish the change in the shape of
SortedWordCount’s interface.
In main( ) you can see
the use of a SortedWordCount to open and count the words in a file
– it just takes two lines of code. Then an enumeration to a sorted list of
keys (words) is extracted, and this is used to pull out each key and associated
Count. Note that the call to cleanup( ) is necessary to
ensure that the file is closed.
Although it isn’t part of the
IO library, the StringTokenizer has sufficiently similar functionality to
StreamTokenizer that it will be described here.
The
StringTokenizer returns the tokens within a
string one at a time. These tokens are consecutive characters delimited by tabs,
spaces, and newlines. Thus, the tokens of the string “Where is my
cat?” are “Where”, “is”, “my”, and
“cat?” Like the StreamTokenizer, you can tell the
StringTokenizer to break up the input in any way that you want, but with
StringTokenizer you do this by passing a second argument to the
constructor, which is a String of the delimiters you wish to use. In
general, if you need more sophistication, use a
StreamTokenizer.
You ask a StringTokenizer
object for the next token in the string using the nextToken( )
method, which either returns the token or an empty string to indicate that no
tokens remain.
As an example, the following
program performs a limited analysis of a sentence, looking for key phrase
sequences to indicate whether happiness or sadness is implied.
//: AnalyzeSentence.java
// Look for particular sequences
// within sentences.
import java.util.*;
public class AnalyzeSentence {
public static void main(String[] args) {
analyze("I am happy about this");
analyze("I am not happy about this");
analyze("I am not! I am happy");
analyze("I am sad about this");
analyze("I am not sad about this");
analyze("I am not! I am sad");
analyze("Are you happy about this?");
analyze("Are you sad about this?");
analyze("It's you! I am happy");
analyze("It's you! I am sad");
}
static StringTokenizer st;
static void analyze(String s) {
prt("\nnew sentence >> " + s);
boolean sad = false;
st = new StringTokenizer(s);
while (st.hasMoreTokens()) {
String token = next();
// Look until you find one of the
// two starting tokens:
if(!token.equals("I") &&
!token.equals("Are"))
continue; // Top of while loop
if(token.equals("I")) {
String tk2 = next();
if(!tk2.equals("am")) // Must be after I
break; // Out of while loop
else {
String tk3 = next();
if(tk3.equals("sad")) {
sad = true;
break; // Out of while loop
}
if (tk3.equals("not")) {
String tk4 = next();
if(tk4.equals("sad"))
break; // Leave sad false
if(tk4.equals("happy")) {
sad = true;
break;
}
}
}
}
if(token.equals("Are")) {
String tk2 = next();
if(!tk2.equals("you"))
break; // Must be after Are
String tk3 = next();
if(tk3.equals("sad"))
sad = true;
break; // Out of while loop
}
}
if(sad) prt("Sad detected");
}
static String next() {
if(st.hasMoreTokens()) {
String s = st.nextToken();
prt(s);
return s;
}
else
return "";
}
static void prt(String s) {
System.out.println(s);
}
} ///:~
For each string being analyzed, a
while loop is entered and tokens are pulled off the string. Notice the
first if statement, which says to continue (go back to the
beginning of the loop and start again) if the token is neither an
“I” nor an “Are.” This
means that it will get tokens until an “I” or an “Are”
is found. You might think to use the == instead of the
equals( ) method,
but that won’t work correctly, since == compares handle values
while equals( ) compares contents.
The logic of the rest of the
analyze( ) method is that the pattern that’s being searched
for is “I am sad,” “I am not happy,” or “Are you
sad?” Without the break statement, the code for this would be even
messier than it is. You should be aware that a typical parser (this is a
primitive example of one) normally has a table of these tokens and a piece of
code that moves through the states in the table as new tokens are
read.
You should think of the
StringTokenizer only as shorthand for a simple and specific kind of
StreamTokenizer. However, if you have a String that you want to
tokenize and StringTokenizer is too limited, all you have to do is turn
it into a stream with StringBufferInputStream and then use that to create
a much more powerful
StreamTokenizer.
At this point you might be
scratching your head, wondering if there is another design for IO streams that
could require more typing. Could someone have come up with an odder
design?” Prepare yourself: Java 1.1 makes some significant modifications
to the IO stream library. When you see the
Reader and
Writer classes your first
thought (like mine) might be that these were meant to replace the
InputStream and OutputStream classes. But that’s not the
case. Although some aspects of the original streams library are deprecated (if
you use them you will receive a warning from the compiler), the old streams have
been left in for backwards compatibility and:
As a
result there are situations in which you have more layers of wrapping
with the new IO stream library than with the old. Again, this is a drawback of
the decorator pattern – the price you pay for added
flexibility.
The most important reason for
adding the Reader and Writer hierarchies in Java
1.1 is for
internationalization. The old IO
stream hierarchy supports only 8-bit byte streams and doesn’t handle the
16-bit Unicode characters well. Since Unicode is used for internationalization
(and Java’s native char is 16-bit
Unicode), the Reader and
Writer hierarchies were added to support Unicode in all IO operations. In
addition, the new libraries are designed for faster operations than the
old.
As is the practice in this book, I
will attempt to provide an overview of the classes but assume that you will use
online documentation to determine all the details, such as the exhaustive list
of methods.
Almost all of the Java
1.0 IO stream classes have corresponding Java
1.1 classes to provide native Unicode manipulation. It
would be easiest to say “Always use the new classes, never use the old
ones,” but things are not that simple. Sometimes you are forced into using
the Java 1.0 IO stream classes because of the library design; in particular, the
java.util.zip libraries are new additions to the old stream library and
they rely on old stream components. So the most sensible approach to take is to
try to use the Reader and Writer classes whenever you can,
and you’ll discover the situations when you have to drop back into the old
libraries because your code won’t compile.
Here is a table that shows the
correspondence between the sources and sinks of information (that is, where the
data physically comes from or goes to) in the old and new libraries.
|
Sources &
Sinks: |
Corresponding Java 1.1
class |
|
InputStream |
Reader
|
|
OutputStream |
Writer
|
|
FileInputStream |
FileReader |
|
FileOutputStream |
FileWriter |
|
StringBufferInputStream |
StringReader |
|
(no corresponding
class) |
StringWriter |
|
ByteArrayInputStream |
CharArrayReader |
|
ByteArrayOutputStream |
CharArrayWriter |
|
PipedInputStream |
PipedReader |
|
PipedOutputStream |
PipedWriter |
In general, you’ll find that
the interfaces in the old library components and the new ones are similar if not
identical.
In Java
1.0, streams were adapted for particular needs using
“decorator” subclasses of FilterInputStream and
FilterOutputStream. Java 1.1 IO streams continues
the use of this idea, but the model of deriving all of the decorators from the
same “filter” base class is not followed. This can make it a bit
confusing if you’re trying to understand it by looking at the class
hierarchy.
In the following table, the
correspondence is a rougher approximation than in the previous table. The
difference is because of the class organization: while
BufferedOutputStream is a subclass of FilterOutputStream,
BufferedWriter is not a subclass of FilterWriter (which,
even though it is abstract, has no subclasses and so appears to have been
put in either as a placeholder or simply so you wouldn’t wonder where it
was). However, the interfaces to the classes are quite a close match and
it’s apparent that you’re supposed to use the new versions instead
of the old whenever possible (that is, except in cases where you’re forced
to produce a Stream instead of a Reader or Writer).
|
Filters: |
Corresponding Java 1.1
class |
|---|---|
|
FilterInputStream |
FilterReader |
|
FilterOutputStream |
FilterWriter
(abstract class with no subclasses) |
|
BufferedInputStream |
BufferedReader |
|
BufferedOutputStream |
BufferedWriter |
|
DataInputStream |
use
DataInputStream |
|
PrintStream |
PrintWriter |
|
LineNumberInputStream |
LineNumberReader |
|
StreamTokenizer |
StreamTokenizer |
|
PushBackInputStream |
PushBackReader |
There’s one direction
that’s quite clear: Whenever you want to use readLine( ), you
shouldn’t do it with a DataInputStream any more (this is met with a
deprecation message at compile time), but instead use a BufferedReader.
Other than this, DataInputStream is still a “preferred”
member of the Java 1.1 IO library.
To make the transition to using a
PrintWriter easier, it has constructors that take any OutputStream
object. However, PrintWriter has no more support for formatting than
PrintStream does; the interfaces are virtually the
same.
Apparently, the Java library
designers felt that they got some of the classes right the first time so there
were no changes to these and you can go on using them as they are:
|
Java 1.0 classes without
corresponding Java 1.1 classes |
|---|
|
DataOutputStream |
|
File |
|
RandomAccessFile |
|
SequenceInputStream |
The DataOutputStream, in
particular, is used without change, so for storing and retrieving data in a
transportable format you’re forced to stay in the InputStream and
OutputStream hierarchies.
To see the effect of the new
classes, let’s look at the appropriate portion of the
IOStreamDemo.java example modified to use the Reader and
Writer classes:
//: NewIODemo.java
// Java 1.1 IO typical usage
import java.io.*;
public class NewIODemo {
public static void main(String[] args) {
try {
// 1. Reading input by lines:
BufferedReader in =
new BufferedReader(
new FileReader(args[0]));
String s, s2 = new String();
while((s = in.readLine())!= null)
s2 += s + "\n";
in.close();
// 1b. Reading standard input:
BufferedReader stdin =
new BufferedReader(
new InputStreamReader(System.in));
System.out.print("Enter a line:");
System.out.println(stdin.readLine());
// 2. Input from memory
StringReader in2 = new StringReader(s2);
int c;
while((c = in2.read()) != -1)
System.out.print((char)c);
// 3. Formatted memory input
try {
DataInputStream in3 =
new DataInputStream(
// Oops: must use deprecated class:
new StringBufferInputStream(s2));
while(true)
System.out.print((char)in3.readByte());
} catch(EOFException e) {
System.out.println("End of stream");
}
// 4. Line numbering & file output
try {
LineNumberReader li =
new LineNumberReader(
new StringReader(s2));
BufferedReader in4 =
new BufferedReader(li);
PrintWriter out1 =
new PrintWriter(
new BufferedWriter(
new FileWriter("IODemo.out")));
while((s = in4.readLine()) != null )
out1.println(
"Line " + li.getLineNumber() + s);
out1.close();
} catch(EOFException e) {
System.out.println("End of stream");
}
// 5. Storing & recovering data
try {
DataOutputStream out2 =
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("Data.txt")));
out2.writeDouble(3.14159);
out2.writeBytes("That was pi");
out2.close();
DataInputStream in5 =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("Data.txt")));
BufferedReader in5br =
new BufferedReader(
new InputStreamReader(in5));
// Must use DataInputStream for data:
System.out.println(in5.readDouble());
// Can now use the "proper" readLine():
System.out.println(in5br.readLine());
} catch(EOFException e) {
System.out.println("End of stream");
}
// 6. Reading and writing random access
// files is the same as before.
// (not repeated here)
} catch(FileNotFoundException e) {
System.out.println(
"File Not Found:" + args[1]);
} catch(IOException e) {
System.out.println("IO Exception");
}
}
} ///:~
In general, you’ll see that
the conversion is fairly straightforward and the code looks quite similar. There
are some important differences, though. First of all, since random access files
have not changed, section 6 is not repeated.
Section 1 shrinks a bit because if
all you’re doing is reading line input you need only to wrap a
BufferedReader around a FileReader. Section 1b shows the new way
to wrap System.in for
reading
console
input, and this expands because System.in is a DataInputStream and
BufferedReader needs a Reader argument, so
InputStreamReader is brought in to perform the
translation.
In section 2 you can see that if
you have a String and want to read from it you just use a
StringReader instead of a StringBufferInputStream and the rest of
the code is identical.
Section 3 shows a bug in the design
of the new IO stream library. If you have a String and you want to read
from it, you’re not supposed to use a
StringBufferInputStream any more. When you compile code involving a
StringBufferInputStream constructor, you get a deprecation message
telling you to not use it. Instead, you’re supposed to use a
StringReader. However, if you want to do formatted memory input as in
section 3, you’re forced to use a DataInputStream – there is
no “DataReader” to replace it – and a DataInputStream
constructor requires an InputStream argument. So you have no choice but
to use the deprecated StringBufferInputStream class. The compiler will
give you a deprecation message but there’s nothing you can do about
it.[45]
Section 4 is a reasonably
straightforward translation from the old streams to the new, with no surprises.
In section 5, you’re forced to use all the old streams classes because
DataOutputStream and DataInputStream require them and there are no
alternatives. However, you don’t get any deprecation messages at compile
time. If a stream is deprecated, typically its constructor produces a
deprecation message to prevent you from using the entire class, but in the case
of DataInputStream only the readLine( ) method is deprecated
since you’re supposed to use a BufferedReader for
readLine( ) (but a DataInputStream for all other formatted
input).
If you compare section 5 with that
section in IOStreamDemo.java, you’ll notice that in this
version, the data is written before the text. That’s because a bug
was introduced in Java 1.1, which is shown in the
following code:
//: IOBug.java
// Java 1.1 (and higher?) IO Bug
import java.io.*;
public class IOBug {
public static void main(String[] args)
throws Exception {
DataOutputStream out =
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("Data.txt")));
out.writeDouble(3.14159);
out.writeBytes("That was the value of pi\n");
out.writeBytes("This is pi/2:\n");
out.writeDouble(3.14159/2);
out.close();
DataInputStream in =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("Data.txt")));
BufferedReader inbr =
new Buffered