Creating a good input/output (I/O)
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 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.).
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.
This chapter will give you an
introduction to the variety of I/O classes in the standard Java library and how
to use them.
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.
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.
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.
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:
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.
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 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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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[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).
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.
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).
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.
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 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.
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.
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.
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.
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).
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.
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:
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.
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 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.
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.
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.
The PrintWriter constructor also
has an option to perform automatic flushing, which happens after every
println( ) if the constructor flag is
set.
|
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.
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.
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.
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.
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.writeChars("That was pi\n");
out2.writeBytes("That was pi\n");
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());
// But the line comes out funny.
// The one created with writeBytes is OK:
System.out.println(in5br.readLine());
} 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();
}
} ///:~
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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].
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.
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.
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.
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.
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());
}
} ///:~
It appears that anything you write after
a call to writeBytes( ) is not recoverable. The answer is apparently
the same as the answer to the old vaudeville joke: “Doc, it hurts when I
do this!” “Don’t do
that!”
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.
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.
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.
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.
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.
The Java System class allows you
to redirect the standard input, output, and error I/O streams using simple
static method calls:
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.*;
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.
I/O redirection manipulates streams of
bytes, not streams of characters, thus InputStreams and
OutputStreams are used rather than Readers and
Writers.
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.
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.
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).
//: 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 FileInputStream("test.zip");
CheckedInputStream csumi =
new CheckedInputStream(
fi, new Adler32());
ZipInputStream in2 =
new ZipInputStream(
new BufferedInputStream(csumi));
ZipEntry ze;
while((ze = in2.getNextEntry()) != null) {
System.out.println("Reading file " + ze);
int x;
while((x = in2.read()) != -1)
System.out.write(x);
}
System.out.println("Checksum: " +
csumi.getChecksum().getValue());
in2.close();
// Alternative way to open and read
// zip files:
ZipFile zf = new ZipFile("test.zip");
Enumeration e = zf.entries();
while(e.hasMoreElements()) {
ZipEntry ze2 = (ZipEntry)e.nextElement();
System.out.println("File: " + ze2);
// ... and extract the data as before
}
}
} ///:~
For each file to add to the archive, you
must call putNextEntry( ) and pass it a
ZipEntry object. The
ZipEntry object contains an extensive interface that allows you to get
and set all the data available on that particular entry in your Zip file: name,
compressed and uncompressed sizes, date, CRC checksum, extra field data,
comment, compression method, and whether it’s a directory entry. However,
even though the Zip format has a way to set a password, this is not supported in
Java’s Zip library. And although CheckedInputStream and
CheckedOutputStream support both Adler32 and CRC32
checksums, the ZipEntry class supports only an interface for CRC. This is
a restriction of the underlying Zip format, but it might limit you from using
the faster Adler32.
To extract files, ZipInputStream
has a getNextEntry( ) method that returns the next ZipEntry
if there is one. As a more succinct alternative, you can read the file using a
ZipFile object, which has a method entries( ) to return an
Enumeration to the ZipEntries.
In order to read the checksum you must
somehow have access to the associated Checksum object. Here, a reference
to the CheckedOutputStream and CheckedInputStream objects is
retained, but you could also just hold onto a reference to the Checksum
object.
A baffling method in Zip streams is
setComment( ). As shown above, you can set a comment when
you’re writing a file, but there’s no way to recover the comment in
the ZipInputStream. Comments appear to be supported fully on an
entry-by-entry basis only via ZipEntry.
Of course, you are not limited to files
when using the GZIP or Zip libraries—you can compress
anything, including data to be sent through a network
connection.
The Zip format is also used in the
JAR (Java ARchive) file format,
which is a way to collect a group of files into a single compressed file, just
like Zip. However, like everything else in Java, JAR files are cross-platform so
you don’t need to worry about platform issues. You can also include audio
and image files as well as class files.
JAR files are particularly helpful when
you deal with the Internet. Before JAR files, your Web browser would have to
make repeated requests of a Web server in order to download all of the files
that make up an applet. In addition, each of these files was uncompressed. By
combining all of the files for a particular applet into a single JAR file, only
one server request is necessary and the transfer is faster because of
compression. And each entry in a JAR file can be digitally signed for security
(refer to the Java documentation for details).
A JAR file consists of a single file
containing a collection of zipped files along with a
“manifest” that describes them. (You can
create your own manifest file; otherwise the jar program will do it for
you.) You can find out more about JAR manifests in the JDK HTML
documentation.
The jar utility that comes with
Sun’s JDK automatically compresses the files of your choice. You invoke it
on the command line:
jar [options] destination [manifest] inputfile(s)
The options are simply a collection of
letters (no hyphen or any other indicator is necessary). Unix/Linux users will
note the similarity to the tar options. These are:
|
c |
Creates a new or empty archive.
|
|
t |
Lists the table of contents.
|
|
x |
Extracts all files. |
|
x file |
Extracts the named file. |
|
f |
Says: “I’m going to give you
the name of the file.” If you don’t use this, jar assumes that its
input will come from standard input, or, if it is creating a file, its output
will go to standard output. |
|
m |
Says that the first argument will be the
name of the user-created manifest file. |
|
v |
Generates verbose output describing what
jar is doing. |
|
0 |
Only store the files; doesn’t
compress the files (use to create a JAR file that you can put in your
classpath). |
|
M |
Don’t automatically create a
manifest file. |
If a subdirectory is included in the
files to be put into the JAR file, that subdirectory is automatically added,
including all of its subdirectories, etc. Path information is also
preserved.
Here are some typical ways to invoke
jar:
jar cf myJarFile.jar *.class
This creates a JAR file called
myJarFile.jar that contains all of the class files in the current
directory, along with an automatically generated manifest file.
jar cmf myJarFile.jar myManifestFile.mf *.class
Like the previous example, but adding a
user-created manifest file called myManifestFile.mf.
jar tf myJarFile.jar
Produces a table of contents of the files
in myJarFile.jar.
jar tvf myJarFile.jar
Adds the “verbose” flag to
give more detailed information about the files in
myJarFile.jar.
jar cvf myApp.jar audio classes image
Assuming audio, classes,
and image are subdirectories, this combines all of the subdirectories
into the file myApp.jar. The “verbose” flag is also included
to give extra feedback while the jar program is working.
If you create a JAR file using the
0 option, that file can be placed in your CLASSPATH:
CLASSPATH="lib1.jar;lib2.jar;"
Then Java can search lib1.jar and
lib2.jar for class files.
The jar tool isn’t as useful
as a zip utility. For example, you can’t add or update files to an
existing JAR file; you can create JAR files only from scratch. Also, you
can’t move files into a JAR file, erasing them as they are moved. However,
a JAR file created on one platform will be transparently readable by the
jar tool on any other platform (a problem that sometimes plagues
zip utilities).
Java’s object serialization
allows you to take any object that implements the Serializable interface
and turn it into a sequence of bytes that can later be fully restored to
regenerate the original object. This is even true across a network, which means
that the serialization mechanism automatically compensates for differences in
operating systems. That is, you can create an object on a Windows machine,
serialize it, and send it across the network to a Unix machine where it will be
correctly reconstructed. You don’t have to worry about the data
representations on the different machines, the byte ordering, or any other
details.
By itself, object serialization is
interesting because it allows you to implement lightweight persistence.
Remember that persistence means an object’s lifetime is not determined by
whether a program is executing—the object lives in between
invocations of the program. By taking a serializable object and writing it to
disk, then restoring that object when the program is reinvoked, you’re
able to produce the effect of persistence. The reason it’s called
“lightweight” is that you can’t simply define an object using
some kind of “persistent” keyword and let the system take care of
the details (although this might happen in the future). Instead, you must
explicitly serialize and deserialize the objects in your
program.
Object serialization was added to the
language to support two major features. Java’s remote method
invocation (RMI) allows objects that live on other machines to behave as if
they live on your machine. When sending messages to remote objects, object
serialization is necessary to transport the arguments and return values. RMI is
discussed in Chapter 15.
Object serialization is also necessary
for JavaBeans, described in Chapter 13. When a Bean is used, its state
information is generally configured at design-time. This state information must
be stored and later recovered when the program is started; object serialization
performs this task.
Serializing an object is quite simple, as
long as the object implements the Serializable interface (this interface
is just a flag and has no methods). When serialization was added to the
language, many standard library classes were changed to make them serializable,
including all of the wrappers for the primitive types, all of the container
classes, and many others. Even Class objects can be serialized. (See
Chapter 12 for the implications of this.)
To serialize an object, you create some
sort of OutputStream object and then wrap it inside an
ObjectOutputStream
object. At this point you need only call
writeObject( ) and
your object is serialized and sent to the OutputStream. To reverse the
process, you wrap an InputStream inside an ObjectInputStream and
call readObject( ).
What comes back is, as usual, a reference to an upcast Object, so you
must downcast to set things straight.
A particularly clever aspect of object
serialization is that it not only saves an image of your object but it also
follows all the references contained in your object and saves those
objects, and follows all the references in each of those objects, etc. This
is sometimes referred to as the
“web of objects”
that a single object can be connected to, and it includes arrays of references
to objects as well as member objects. If you had to maintain your own object
serialization scheme, maintaining the code to follow all these links would be a
bit mind-boggling. However, Java object serialization seems to pull it off
flawlessly, no doubt using an optimized algorithm that traverses the web of
objects. The following example tests the serialization mechanism by making a
“worm” of linked objects, each of which has a link to the next
segment in the worm as well as an array of references to objects of a different
class, Data:
//: c11:Worm.java
// Demonstrates object serialization.
import java.io.*;
class Data implements Serializable {
private int i;
Data(int x) { i = x; }
public String toString() {
return Integer.toString(i);
}
}
public class Worm implements Serializable {
// Generate a random int value:
private static int r() {
return (int)(Math.random() * 10);
}
private Data[] d = {
new Data(r()), new Data(r()), new Data(r())
};
private Worm next;
private char c;
// Value of i == number of segments
Worm(int i, char x) {
System.out.println(" Worm constructor: " + i);
c = x;
if(--i > 0)
next = new Worm(i, (char)(x + 1));
}
Worm() {
System.out.println("Default constructor");
}
public String toString() {
String s = ":" + c + "(";
for(int i = 0; i < d.length; i++)
s += d[i].toString();
s += ")";
if(next != null)
s += next.toString();
return s;
}
// Throw exceptions to console:
public static void main(String[] args)
throws ClassNotFoundException, IOException {
Worm w = new Worm(6, 'a');
System.out.println("w = " + w);
ObjectOutputStream out =
new ObjectOutputStream(
new FileOutputStream("worm.out"));
out.writeObject("Worm storage");
out.writeObject(w);
out.close(); // Also flushes output
ObjectInputStream in =
new ObjectInputStream(
new FileInputStream("worm.out"));
String s = (String)in.readObject();
Worm w2 = (Worm)in.readObject();
System.out.println(s + ", w2 = " + w2);
ByteArrayOutputStream bout =
new ByteArrayOutputStream();
ObjectOutputStream out2 =
new ObjectOutputStream(bout);
out2.writeObject("Worm storage");
out2.writeObject(w);
out2.flush();
ObjectInputStream in2 =
new ObjectInputStream(
new ByteArrayInputStream(
bout.toByteArray()));
s = (String)in2.readObject();
Worm w3 = (Worm)in2.readObject();
System.out.println(s + ", w3 = " + w3);
}
} ///:~
To make things interesting, the array of
Data objects inside Worm are initialized with random numbers.
(This way you don’t suspect the compiler of keeping some kind of
meta-information.) Each Worm segment is labeled with a char
that’s automatically generated in the process of recursively generating
the linked list of Worms. When you create a Worm, you tell the
constructor how long you want it to be. To make the next reference it
calls the Worm constructor with a length of one less, etc. The final
next reference is left as null, indicating the end of the
Worm.
The point of all this was to make
something reasonably complex that couldn’t easily be serialized. The act
of serializing, however, is quite simple. Once the ObjectOutputStream is
created from some other stream, writeObject( ) serializes the
object. Notice the call to writeObject( ) for a String, as
well. You can also write all the primitive data types using the same methods as
DataOutputStream (they share the same interface).
There are two separate code sections that
look similar. The first writes and reads a file and the second, for variety,
writes and reads a ByteArray. You can read and write an object using
serialization to any DataInputStream or DataOutputStream
including, as you will see in the Chapter 15, a network. The output from one run
was:
Worm constructor: 6 Worm constructor: 5 Worm constructor: 4 Worm constructor: 3 Worm constructor: 2 Worm constructor: 1 w = :a(262):b(100):c(396):d(480):e(316):f(398) Worm storage, w2 = :a(262):b(100):c(396):d(480):e(316):f(398) Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398)
You can see that the deserialized object
really does contain all of the links that were in the original
object.
Note that no constructor, not even the
default constructor, is called in the process of deserializing a
Serializable object. The entire object is restored by recovering data
from the InputStream.
You might wonder what’s necessary
for an object to be recovered from its serialized state. For example, suppose
you serialize an object and send it as a file or through a network to another
machine. Could a program on the other machine reconstruct the object using only
the contents of the file?
The best way to answer this question is
(as usual) by performing an experiment. The following file goes in the
subdirectory for this chapter:
//: c11:Alien.java
// A serializable class.
import java.io.*;
public class Alien implements Serializable {
} ///:~
The file that creates and serializes an
Alien object goes in the same directory: