Learning Resources
Data and Object Streams
Data Streams
Data streams support binary I/O of primitive data type values (boolean
, char
, byte
, short
, int
, long
, float
, and double
) as well as String values. All data streams implement either the
DataInput
interface or the
DataOutput
interface. This section focuses on the most widely-used implementations of these interfaces,
DataInputStream
and
DataOutputStream
.
The
DataStreams
example demonstrates data streams by writing out a set of data records, and then reading them in again. Each record consists of three values related to an item on an invoice, as shown in the following table:
Order in record | Data type | Data description | Output Method | Input Method | Sample Value |
---|---|---|---|---|---|
1 | double |
Item price | DataOutputStream.writeDouble |
DataInputStream.readDouble |
19.99 |
2 | int |
Unit count | DataOutputStream.writeInt |
DataInputStream.readInt |
12 |
3 | String |
Item description | DataOutputStream.writeUTF |
DataInputStream.readUTF |
"Java T-Shirt" |
Let's examine crucial code in DataStreams
. First, the program defines some constants containing the name of the data file and the data that will be written to it:
static final String dataFile = "invoicedata"; static final double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 }; static final int[] units = { 12, 8, 13, 29, 50 }; static final String[] descs = { "Java T-shirt", "Java Mug", "Duke Juggling Dolls", "Java Pin", "Java Key Chain" };
Then DataStreams
opens an output stream. Since a DataOutputStream
can only be created as a wrapper for an existing byte stream object, DataStreams
provides a buffered file output byte stream.
out = new DataOutputStream(new BufferedOutputStream( new FileOutputStream(dataFile)));
DataStreams
writes out the records and closes the output stream.
for (int i = 0; i < prices.length; i ++) { out.writeDouble(prices[i]); out.writeInt(units[i]); out.writeUTF(descs[i]); }
The writeUTF
method writes out String
values in a modified form of UTF-8. This is a variable-width character encoding that only needs a single byte for common Western characters.
Now DataStreams
reads the data back in again. First it must provide an input stream, and variables to hold the input data. Like DataOutputStream
, DataInputStream
must be constructed as a wrapper for a byte stream.
in = new DataInputStream(new BufferedInputStream(new FileInputStream(dataFile))); double price; int unit; String desc; double total = 0.0;
Now DataStreams
can read each record in the stream, reporting on the data it encounters.
try { while (true) { price = in.readDouble(); unit = in.readInt(); desc = in.readUTF(); System.out.format("You ordered %d" + " units of %s at $%.2f%n", unit, desc, price); total += unit * price; } } catch (EOFException e) { }
Notice that DataStreams
detects an end-of-file condition by catching
EOFException
, instead of testing for an invalid return value. All implementations of DataInput
methods use EOFException
instead of return values.
Also notice that each specialized write
in DataStreams
is exactly matched by the corresponding specialized read
. It is up to the programmer to make sure that output types and input types are matched in this way: The input stream consists of simple binary data, with nothing to indicate the type of individual values, or where they begin in the stream.
DataStreams
uses one very bad programming technique: it uses floating point numbers to represent monetary values. In general, floating point is bad for precise values. It's particularly bad for decimal fractions, because common values (such as 0.1
) do not have a binary representation.
The correct type to use for currency values is
java.math.BigDecimal
. Unfortunately, BigDecimal
is an object type, so it won't work with data streams. However, BigDecimal
will work with object streams, which are covered in the next section.
Object Streams
Just as data streams support I/O of primitive data types, object streams support I/O of objects. Most, but not all, standard classes support serialization of their objects. Those that do implement the marker interface
Serializable
.
The object stream classes are
ObjectInputStream
and
ObjectOutputStream
. These classes implement
ObjectInput
and
ObjectOutput
, which are subinterfaces of DataInput
and DataOutput
. That means that all the primitive data I/O methods covered in Data Streams are also implemented in object streams. So an object stream can contain a mixture of primitive and object values. The
ObjectStreams
example illustrates this. ObjectStreams
creates the same application as DataStreams
, with a couple of changes. First, prices are now
BigDecimal
objects, to better represent fractional values. Second, a
Calendar
object is written to the data file, indicating an invoice date.
If readObject()
doesn't return the object type expected, attempting to cast it to the correct type may throw a
ClassNotFoundException
. In this simple example, that can't happen, so we don't try to catch the exception. Instead, we notify the compiler that we're aware of the issue by adding ClassNotFoundException
to the main
method's throws
clause.
Output and Input of Complex Objects
The writeObject
and readObject
methods are simple to use, but they contain some very sophisticated object management logic. This isn't important for a class like Calendar, which just encapsulates primitive values. But many objects contain references to other objects. If readObject
is to reconstitute an object from a stream, it has to be able to reconstitute all of the objects the original object referred to. These additional objects might have their own references, and so on. In this situation, writeObject
traverses the entire web of object references and writes all objects in that web onto the stream. Thus a single invocation of writeObject
can cause a large number of objects to be written to the stream.
This is demonstrated in the following figure, where writeObject
is invoked to write a single object named a. This object contains references to objects b and c, while b contains references to d and e. Invoking writeobject(a)
writes not just a, but all the objects necessary to reconstitute a, so the other four objects in this web are written also. When a is read back by readObject
, the other four objects are read back as well, and all the original object references are preserved.
I/O of multiple referred-to objects
You might wonder what happens if two objects on the same stream both contain references to a single object. Will they both refer to a single object when they're read back? The answer is "yes." A stream can only contain one copy of an object, though it can contain any number of references to it. Thus if you explicitly write an object to a stream twice, you're really writing only the reference twice. For example, if the following code writes an object ob
twice to a stream:
Object ob = new Object(); out.writeObject(ob); out.writeObject(ob);
Each writeObject
has to be matched by a readObject
, so the code that reads the stream back will look something like this:
Object ob1 = in.readObject(); Object ob2 = in.readObject();
This results in two variables, ob1
and ob2
, that are references to a single object.
However, if a single object is written to two different streams, it is effectively duplicated a single program reading both streams back will see two distinct objects.