![]() |
James Thornton |
| Internet Business Consultant | Call Toll Free: 1 (800) 409-2501 |
| About James | My MySpace | Internet Marketing | Enron Loophole | Lock Bumping | Contact Me |
|---|
This is evidenced by the number of
different approaches. The challenge seems to be in covering all eventualities.
Not only are there different sources and sinks of I/O 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, buffered, binary,
character, by lines, by words, etc.).
[ Add Comment ]
The Java library designers attacked this
problem by creating lots of classes. In fact, there are so many classes for
Java’s I/O system that it can be intimidating at first (ironically, the
Java I/O design actually prevents an explosion of classes). There was also a
significant change in the I/O library after Java 1.0,
when the original byte-oriented library was supplemented with
char-oriented, Unicode-based I/O classes. As a result there are a fair
number of classes to learn before you understand enough of Java’s I/O
picture that you can use it properly. In addition, it’s rather important
to understand the evolution history of the I/O library, even if your first
reaction is “don’t bother me with history, just show me how to use
it!” The problem is that without the historical perspective you will
rapidly become confused with some of the classes and when you should and
shouldn’t use them.
[ Add Comment ]
This chapter will give you an
introduction to the variety of I/O classes in the standard Java library and how
to use them.
[ Add Comment ]
Before getting into the classes that
actually read and write data to streams, we’ll look a utility provided
with the library to assist you in handling file directory issues.
[ Add Comment ]
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
container 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 for the class. This
section shows an example of the use of this class, including the associated
FilenameFilter interface.
[ Add Comment ]
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, if you want 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.
[ Add Comment ]
Here’s the code for the example.
Note that the result has been effortlessly sorted (alphabetically) using the
java.utils.Array.sort( ) method and the AlphabeticComparator
defined in Chapter 9:
//: c11:DirList.java
// Displays directory listing.
import java.io.*;
import java.util.*;
import com.bruceeckel.util.*;
public class DirList {
public static void main(String[] args) {
File path = new File(".");
String[] list;
if(args.length == 0)
list = path.list();
else
list = path.list(new DirFilter(args[0]));
Arrays.sort(list,
new AlphabeticComparator());
for(int i = 0; i < list.length; i++)
System.out.println(list[i]);
}
}
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. It’s
useful to see how simple the FilenameFilter interface is:
[ Add Comment ]
public interface FilenameFilter {
boolean accept(File dir, String name);
}
It says 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) or the
Command Pattern.
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.
[ Add Comment ]
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.
[ Add Comment ]
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( ).
[ Add Comment ]
To make sure the element you’re
working with is only the file 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 “glob” expression wildcard matching—such as
“fo?.b?r*”—which is much more difficult to implement.
[ Add Comment ]
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++.
[ Add Comment ]
This example is ideal for rewriting using
an
anonymous
inner class (described in Chapter 8). As a first cut, a method filter( )
is created that returns a reference to a
FilenameFilter:
//: c11:DirList2.java
// Uses anonymous inner classes.
import java.io.*;
import java.util.*;
import com.bruceeckel.util.*;
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) {
File path = new File(".");
String[] list;
if(args.length == 0)
list = path.list();
else
list = path.list(filter(args[0]));
Arrays.sort(list,
new AlphabeticComparator());
for(int i = 0; i < list.length; i++)
System.out.println(list[i]);
}
} ///:~
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.
[ Add Comment ]
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:
//: c11:DirList3.java
// Building the anonymous inner class "in-place."
import java.io.*;
import java.util.*;
import com.bruceeckel.util.*;
public class DirList3 {
public static void main(final String[] args) {
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;
}
});
Arrays.sort(list,
new AlphabeticComparator());
for(int i = 0; i < list.length; i++)
System.out.println(list[i]);
}
} ///:~
The argument to main( ) is
now final, since the anonymous inner class uses args[0] directly.
[ Add Comment ]
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.
[ Add Comment ]
The File class is more than just a
representation for an existing file or directory. 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 some of the other
methods available with the File class (see the HTML documentation from
java.sun.com for the full set):
//: c11: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
various file investigation methods used to display information about the file or
directory path.
[ Add Comment ]
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.
[ Add Comment ]
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.
[ Add Comment ]
I/O libraries often use the abstraction
of a stream, which represents any data source or sink as an object
capable of producing or receiving pieces of data. The stream hides the details
of what happens to the data inside the actual I/O device.
[ Add Comment ]
The Java library classes for I/O are
divided by input and output, as you can see by looking at the online Java class
hierarchy with your Web browser. By inheritance, everything derived from the
InputStream or Reader classes have basic methods called
read( ) for reading a single byte or array of bytes. Likewise,
everything derived from OutputStream or Writer classes 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 that other
classes can use them—these other classes 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.
[ Add Comment ]
It’s helpful to categorize the
classes by their functionality. In Java 1.0, 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.
[ Add Comment ]
InputStream’s job is to
represent classes that produce input from different sources. These sources can
be:
Each
of these 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.
[ Add Comment ]
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.”
[ Add Comment ]
In addition, the
FilterOutputStream provides a base class for "decorator" classes that
attach attributes or useful interfaces to output streams. This is discussed
later.
[ Add Comment ]
The use of layered objects to dynamically
and transparently add responsibilities to individual objects is referred to as
the Decorator pattern.
(Patterns[57] are
the subject of Thinking in Patterns with Java, downloadable at
www.BruceEckel.com.) The decorator pattern specifies that all objects
that wrap around your initial object have the same interface. This makes the
basic 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 I/O 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).
[ Add Comment ]
Decorators are often used when simple
subclassing results in a large number of subclasses in order to satisfy every
possible combination that is needed—so many subclasses that it becomes
impractical. The Java I/O library requires many different combinations of
features, which is why the decorator pattern is used. 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 I/O library is
awkward to use is that you must create many classes—the “core”
I/O type plus all the decorators—in order to get the single I/O object
that you want.
[ Add Comment ]
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. FilterInputStream and
FilterOutputStream are abstract classes that are derived from the base
classes of the I/O library, InputStream and OutputStream, which is
the key requirement of the decorator (so that it provides the common interface
to all the objects that are being decorated).
[ Add Comment ]
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 11-1.
[ Add Comment ]
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.
[ Add Comment ]
You’ll probably need to buffer your
input almost every time, regardless of the I/O device you’re connecting
to, so it would have made more sense for the I/O library to make a special case
(or simply a method call) for unbuffered input rather than buffered input.
[ Add Comment ]
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.
[ Add Comment ]
The original intent of PrintStream
was to print all of the primitive data types and String objects in a
viewable format. This is different from DataOutputStream, whose goal is
to put data elements on a stream in a way that DataInputStream can
portably reconstruct them.
[ Add Comment ]
The two important methods in
PrintStream are print( ) and println( ), which
are overloaded to print all the various types. The difference between
print( ) and println( ) is that the latter adds a
newline when it’s done.
[ Add Comment ]
PrintStream can be problematic
because it traps all IOExceptions (You must explicitly test the error
status with checkError( ), which returns true if an error has
occurred). Also, PrintStream doesn’t internationalize properly and
doesn’t handle line breaks in a platform independent way (these problems
are solved with PrintWriter).
[ Add Comment ]
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 I/O.
[ Add Comment ]
Java 1.1 made some significant
modifications to the fundamental I/O stream library (Java 2, however, did not
make fundamental modifications). 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
InputStream and OutputStream classes still provide valuable
functionality in the form of byte-oriented I/O, while the Reader
and Writer classes provide Unicode-compliant, character-based I/O. In
addition:
[ Add Comment ]
The
most important reason for the Reader and Writer hierarchies is for
internationalization. The old
I/O 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 I/O operations.
In addition, the new libraries are designed for faster operations than the old.
[ Add Comment ]
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.
[ Add Comment ]
Almost all of the original Java I/O
stream classes have corresponding Reader and Writer classes to
provide native Unicode manipulation. However, there are some places where the
byte-oriented InputStreams and OutputStreams are the
correct solution; in particular, the java.util.zip libraries are
byte-oriented rather than char-oriented. 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 use the byte-oriented libraries because your code won’t compile.
[ Add Comment ]
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 two hierarchies.
|
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 for the two different hierarchies are similar if not
identical.
For InputStreams and
OutputStreams, streams were adapted for particular needs using
“decorator” subclasses of FilterInputStream and
FilterOutputStream. The Reader and Writer class hierarchies
continue the use of this idea—but not exactly.
[ Add Comment ]
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.
|
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 I/O library.
To make the transition to using a
PrintWriter easier, it has constructors that take any OutputStream
object, as well as Writer objects. However, PrintWriter has no
more support for formatting than PrintStream does; the interfaces are
virtually the same.
[ Add Comment ]
The PrintWriter constructor also
has an option to perform automatic flushing, which happens after every
println( ) if the constructor flag is set.
[ Add Comment ]
|
Java 1.0 classes without corresponding
Java 1.1 classes |
|---|
|
DataOutputStream |
|
File |
|
RandomAccessFile |
|
SequenceInputStream |
DataOutputStream, in particular,
is used without change, so for storing and retrieving data in a transportable
format you use the InputStream and OutputStream
hierarchies.
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.
[ Add Comment ]
At first it’s a little bit hard to
believe that RandomAccessFile is not part of the InputStream or
OutputStream hierarchy. However, 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 I/O types, since you can move
forward and backward within a file. In any event, it stands alone, as a direct
descendant of Object.
[ Add Comment ]
Essentially, a RandomAccessFile
works like a DataInputStream pasted together with a
DataOutputStream, along with 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.
[ Add Comment ]
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 very useful.
[ Add Comment ]
Although you can combine the I/O stream
classes in many different ways, you’ll probably just use a few
combinations. The following example can be used as a basic reference; it shows
the creation and use of typical I/O configurations. 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.
//: c11:IOStreamDemo.java
// Typical I/O stream configurations.
import java.io.*;
public class IOStreamDemo {
// Throw exceptions to console:
public static void main(String[] args)
throws IOException {
// 1. Reading input by lines:
BufferedReader in =
new BufferedReader(
new FileReader("IOStreamDemo.java"));
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(
new ByteArrayInputStream(s2.getBytes()));
while(true)
System.out.print((char)in3.readByte());
} catch(EOFException e) {
System.err.println("End of stream");
}
// 4. File output
try {
BufferedReader in4 =
new BufferedReader(
new StringReader(s2));
PrintWriter out1 =
new PrintWriter(
new BufferedWriter(
new FileWriter("IODemo.out")));
int lineCount = 1;
while((s = in4.readLine()) != null )
out1.println(lineCount++ + ": " + s);
out1.close();
} catch(EOFException e) {
System.err.println("End of stream");
}
// 5. Storing & recovering data
try {
DataOutputStream out2 =
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("Data.txt")));
out2.writeDouble(3.14159);
out2.writeCharswriteUTF("That was pi\n");
out2.writeBytes("That was pi\n");
out2.writeDouble(1.41413);
out2.writeUTF("Square root of 2");
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());
// Only readUTF() will recover the
// Java-UTF String properly:
// Can now use the "proper" readLine():
System.out.println(in5br.readLinereadUTF());
// But the line comes out funny.
// The one created with writeBytes is OK:
System.out.println(in5br.readLine());
// Read the following double and String:
System.out.println(in5.readDouble());
System.out.println(in5.readUTF());
} catch(EOFException e) {
System.err.println("End of stream");
}
// 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();
}
} ///:~
Here are the descriptions for the
numbered sections of the program:
[ Add Comment ]
Parts 1 through 4 demonstrate the
creation and use of input streams. Part 4 also shows the simple use of an output
stream.
To open a file for character input, you
use a FileInputReader
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 reference
to the constructor for a
BufferedReader. Since
BufferedReader also provides the readLine( ) method, this is
your final object and the interface you read from. When you reach the end of the
file, readLine( ) returns null so that is used to break out
of the while loop.
[ Add Comment ]
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( ) runs, and this is supposed to happen (whether or not
garbage collection occurs) as the program exits. However, this has been
inconsistently implemented, so the only safe approach is to explicitly call
close( ) for files.
[ Add Comment ]
Section 1b shows how you can wrap
System.in for reading
console
input. System.in is a DataInputStream and BufferedReader
needs a Reader argument, so InputStreamReader is brought in to
perform the translation.
[ Add Comment ]
This section takes the String s2
that now contains the entire contents of the file and uses it to create a
StringReader. 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.
[ Add Comment ]
To read “formatted” data, you
use a DataInputStream,
which is a byte-oriented I/O class (rather than char oriented).
Thus you must use all InputStream classes rather than Reader
classes. Of course, you can read anything (such as a file) as bytes using
InputStream classes, but here a String is used. To convert the
String to an array of bytes, which is what is appropriate for a
ByteArrayInputStream, String has a
getBytes( ) method to do the job. At that
point, you have an appropriate InputStream to hand to
DataInputStream.
[ Add Comment ]
If you read the characters from a
DataInputStream one byte at a time using readByte( ), any
byte value is a legitimate result 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:
//: c11:TestEOF.java
// Testing for the end of file
// while reading a byte at a time.
import java.io.*;
public class TestEOF {
// Throw exceptions to console:
public static void main(String[] args)
throws IOException {
DataInputStream in =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("TestEof.java")));
while(in.available() != 0)
System.out.print((char)in.readByte());
}
} ///:~
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.
[ Add Comment ]
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.
[ Add Comment ]
This example also shows how to write data
to a file. First, a
FileWriter is created to
connect to the file. You’ll virtually always want to buffer the output by
wrapping it in a
BufferedWriter (try
removing this wrapping to see the impact on the performance—buffering
tends to dramatically increase performance of I/O operations). Then for the
formatting it’s turned into a
PrintWriter. The data
file created this way is readable as an ordinary text file.
[ Add Comment ]
As the lines are written to the file,
line numbers are added. Note that LineNumberInputStream is not
used, because it’s a silly class and you don’t need it. As shown
here, it’s trivial to keep track of your own line numbers.
[ Add Comment ]
When
the input stream is exhausted,
readLine( ) returns
null. You’ll see an explicit close( ) for out1,
because 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.
[ Add Comment ]
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 reacquired by a
DataInputStream. The
RandomAccessFile stands
alone, although its data format is compatible with the DataInputStream
and DataOutputStream.
[ Add Comment ]
A PrintWriter formats data so
it’s readable by a human. However, 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.
DataOutputStream and DataInputStream are byte-oriented and
thus require the InputStreams and OutputStreams.
[ Add Comment ]
If you use a DataOutputStream to
write the data, then Java guarantees that you can accurately recover the data
using a DataInputStream—regardless of what different platforms
write and read the data. This is incredibly valuable, as anyone knows who has
spent time worrying about platform-specific data issues. That problem vanishes
if you have Java on both
platforms[58].
[ Add Comment ]
Note
that the character string is written using both writeChars( ) and
writeBytes( ). When you run the program, you’ll discover that
writeChars( ) outputs 16-bit Unicode characters. When you read the
line using readLine( ), you’ll see that there is a space
between each character, because of the extra byte inserted by Unicode. 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.When using a
DataOutputStream, the only reliable way to write a String so that
it can be recovered by a DataInputStream is to use UTF-8 encoding,
accomplished above using writeUTF( ) and readUTF( ). UTF-8 is a
variation on Unicode, which stores all characters in 2 bytes. If you’re
working with ASCII or mostly ASCII characters (which only occupy 7 bits), this
is a tremendous waste of space and/or bandwidth, so UTF-8 encodes ASCII
characters in a single byte, and non-ASCII characters in two or three bytes. In
addition, the length of the string is stored in the first two bytes. However,
writeUTF( ) and readUTF( ) use a special variation of UTF-8 for
Java (which is completely described in the JavaSoft HTML documentation for those
methods) and so if you read a string written with writeUTF( ) using a
non-Java program, you must write special code in order to read the string
properly.
[ Add Comment ]
With writeUTF( ) and
readUTF( ), you can intermingle Strings and other types of data
using a DataOutputStream with the knowledge that the Strings will
be properly stored as Unicode, and will be easily recoverable with a
DataInputStream.
[ Add Comment ]
The
writeDouble( )
stores the double number to the stream and the complementary
readDouble( )
recovers it (there are similar methods for reading and writing the other types).
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. Note that object serialization (described later in
this chapter) may be an easier way to store and retrieve complex data
structures.
[ Add Comment ]
As previously noted, the
RandomAccessFile is almost totally isolated from the rest of the I/O
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.
[ Add Comment ]
The one option you have is in the second
constructor argument: you can open a RandomAccessFile to read
(“r”) or read and write (“rw”).
[ Add Comment ]
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.
[ Add Comment ]
If you look at section 5,
you’ll see that the data is written before the text. That’s
because a problem was introduced in Java 1.1 (and persists in Java 2) that sure
seems like a bug to me, but I reported it and the bug people at JavaSoft said
that this is the way it is supposed to work (however, the problem did not
occur in Java 1.0, which makes me suspicious). The problem is shown in the
following code:
//: c11:IOProblem.java
// Java 1.1 and higher I/O Problem.
import java.io.*;
public class IOProblem {
// Throw exceptions to console:
public static void main(String[] args)
throws IOException {
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 BufferedReader(
new InputStreamReader(in));
// The doubles written BEFORE the line of text
// read back correctly:
System.out.println(in.readDouble());
// Read the lines of text:
System.out.println(inbr.readLine());
System.out.println(inbr.readLine());
// Trying to read the doubles after the line
// produces an end-of-file exception:
System.out.println(in.readDouble());
}}
///:~
[Removed from text] (This was a bug in
my reasoning – I did not understand the intention of writeUTF( )
and readUTF( ). Also, creating two buffers for the same stream turns
out to be a big mistake – the first buffer that you read sucks up extra
bytes so the second buffer gets indeterminate output).
[ Add Comment ]
The PipedInputStream,
PipedOutputStream, PipedReader and PipedWriter 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.
[ Add Comment ]
The term standard I/O refers to
the Unix concept (which is reproduced in some form in Windows and many other
operating systems) of a single stream of information that is used by a program.
All the program’s input can come from standard input, all its
output can go to standard output, and all of its error messages can be
sent to standard error. The value of standard I/O is that programs can
easily be chained together and one program’s standard output can become
the standard input for another program. This is a powerful tool.
[ Add Comment ]
Following the standard I/O model, Java
has System.in, System.out, and System.err. Throughout this
book you’ve seen how to write to standard output using System.out,
which is already prewrapped 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.
[ Add Comment ]
Typically, you’ll want to read
input a line at a time using readLine( ), so you’ll want to
wrap System.in in a BufferedReader. To do this, you must convert
System.in to a Reader using InputStreamReader. Here’s
an example that simply echoes each line that you type in:
//: c11:Echo.java
// How to read from standard input.
import java.io.*;
public class Echo {
public static void main(String[] args)
throws IOException {
BufferedReader in =
new BufferedReader(
new InputStreamReader(System.in));
String s;
while((s = in.readLine()).length() != 0)
System.out.println(s);
// An empty line terminates the program
}
} ///:~
The reason for the exception
specification is that
readLine( ) can
throw an IOException. Note that System.in should usually be
buffered, as with most streams.
[ Add Comment ]
System.out is a
PrintStream, which is an OutputStream. PrintWriter has a
constructor that takes an OutputStream as an argument. Thus, if you want
you can convert System.out into a PrintWriter using that
constructor:
//: c11:ChangeSystemOut.java
// Turn System.out into a PrintWriter.
import java.io.*;
public class ChangeSystemOut {
public static void main(String[] args) {
PrintWriter out =
new PrintWriter(System.out, true);
out.println("Hello, world");
}
} ///:~
It’s important to use the
two-argument version of the PrintWriter constructor and to set the second
argument to true in order to enable automatic flushing, otherwise you may
not see the output.
[ Add Comment ]
The Java System class allows you
to redirect the standard input, output, and error I/O streams using simple
static method calls:
setIn(InputStream)
setOut(PrintStream)
setErr(PrintStream)
[ Add Comment ]
Redirecting output is especially useful
if you suddenly start creating a large amount of output on your screen and
it’s scrolling past faster than you can read
it.[59] Redirecting
input is valuable for a command-line program in which you want to test a
particular user-input sequence repeatedly. Here’s a simple example that
shows the use of these methods:
//: c11:Redirecting.java
// Demonstrates standard I/O redirection.
import java.io.*;
public class Redirecting {
// Throw exceptions to console:
public static void main(String[] args)
throws IOException {
BufferedInputStream in =
new BufferedInputStream(
new FileInputStream(
"Redirecting.java"));
PrintStream out =
new PrintStream(
new BufferedOutputStream(
new FileOutputStream("test.out")));
System.setIn(in);
System.setOut(out);
System.setErr(out);
BufferedReader br =
new BufferedReader(
new InputStreamReader(System.in));
String s;
while((s = br.readLine()) != null)
System.out.println(s);
out.close(); // Remember this!
}
} ///:~
This program attaches standard input to a
file, and redirects standard output and standard error to another file.
[ Add Comment ]
I/O redirection manipulates streams of
bytes, not streams of characters, thus InputStreams and
OutputStreams are used rather than Readers and Writers.
[ Add Comment ]
The Java I/O library contains classes to
support reading and writing streams in a compressed format. These are wrapped
around existing I/O classes to provide compression functionality.
[ Add Comment ]
These classes are not derived from the
Reader and Writer classes, but instead are part of the
InputStream and OutputStream hierarchies. This is because the
compression library works with bytes, not characters. However, you might
sometimes be forced to mix the two types of streams. (Remember that you can use
InputStreamReader and OutputStreamWriter to provide easy
conversion between one type and another.)
|
Compression class |
Function |
|---|---|
|
CheckedInputStream |
GetCheckSum( ) produces
checksum for any InputStream (not just decompression). |
|
CheckedOutputStream |
GetCheckSum( ) produces
checksum for any OutputStream (not just compression). |
|
DeflaterOutputStream |
Base class for compression
classes. |
|
ZipOutputStream |
A DeflaterOutputStream that
compresses data into the Zip file format. |
|
GZIPOutputStream |
A DeflaterOutputStream that
compresses data into the GZIP file format. |
|
InflaterInputStream |
Base class for decompression
classes. |
|
ZipInputStream |
An InflaterInputStream that
decompresses data that has been stored in the Zip file format. |
|
GZIPInputStream |
An InflaterInputStream that
decompresses data that has been stored in the GZIP file format. |
Although there are many compression
algorithms, Zip and GZIP are possibly the most commonly used. Thus you can
easily manipulate your compressed data with the many tools available for reading
and writing these formats.
The GZIP interface is simple and thus is
probably more appropriate when you have a single stream of data that you want to
compress (rather than a container of dissimilar pieces of data). Here’s an
example that compresses a single file:
//: c11:GZIPcompress.java
// Uses GZIP compression to compress a file
// whose name is passed on the command line.
import java.io.*;
import java.util.zip.*;
public class GZIPcompress {
// Throw exceptions to console:
public static void main(String[] args)
throws IOException {
BufferedReader in =
new BufferedReader(
new FileReader(args[0]));
BufferedOutputStream out =
new BufferedOutputStream(
new GZIPOutputStream(
new FileOutputStream("test.gz")));
System.out.println("Writing file");
int c;
while((c = in.read()) != -1)
out.write(c);
in.close();
out.close();
System.out.println("Reading file");
BufferedReader in2 =
new BufferedReader(
new InputStreamReader(
new GZIPInputStream(
new FileInputStream("test.gz"))));
String s;
while((s = in2.readLine()) != null)
System.out.println(s);
}
} ///:~
The use of the compression classes is
straightforward—you simply wrap your output stream in a
GZIPOutputStream or ZipOutputStream and your input stream in a
GZIPInputStream or ZipInputStream. All else is ordinary I/O
reading and writing. This is an example of mixing the char-oriented
streams with the byte-oriented streams: in uses the Reader
classes, whereas GZIPOutputStream’s constructor can accept only an
OutputStream object, not a Writer object. When the file is opened,
the GZIPInputStream is converted to a Reader.
[ Add Comment ]
The library that supports the Zip format
is much more extensive. With it you can easily store multiple files, and
there’s even a separate class to make the process of reading a Zip file
easy. The library uses the standard Zip format so that it works seamlessly with
all the tools currently downloadable on the Internet. The following example has
the same form as the previous example, but it handles as many command-line
arguments as you want. In addition, it shows the use of the
Checksum classes to calculate and verify the
checksum for the file. There are two Checksum types:
Adler32 (which is faster) and
CRC32 (which is slower but slightly more
accurate).
[ Add Comment ]
//: c11:ZipCompress.java
// Uses Zip compression to compress any
// number of files given on the command line.
import java.io.*;
import java.util.*;
import java.util.zip.*;
public class ZipCompress {
// Throw exceptions to console:
public static void main(String[] args)
throws IOException {
FileOutputStream f =
new FileOutputStream("test.zip");
CheckedOutputStream csum =
new CheckedOutputStream(
f, new Adler32());
ZipOutputStream out =
new ZipOutputStream(
new BufferedOutputStream(csum));
out.setComment("A test of Java Zipping");
// No corresponding getComment(), though.
for(int i = 0; i < args.length; i++) {
System.out.println(
"Writing file " + args[i]);
BufferedReader in =
new BufferedReader(
new FileReader(args[i]));
out.putNextEntry(new ZipEntry(args[i]));
int c;
while((c = in.read()) != -1)
out.write(c);
in.close();
}
out.close();
// Checksum valid only after the file
// has been closed!
System.out.println("Checksum: " +
csum.getChecksum().getValue());
// Now extract the files:
System.out.println("Reading file");
FileInputStream fi =
new FileInp