Typical
uses of IO streams
Although
there are a lot of IO stream classes in the library that can be combined in
many different ways, there are just a few ways that you’ll probably end
up using them. However, they require attention to get the correct combinations.
The following rather long example shows the creation and use of typical
IO configurations so you can use it as a reference when writing your own code.
Note that each configuration begins with a commented number and title that
corresponds to the heading for the appropriate explanation that follows in the
text.
//: IOStreamDemo.java
// Typical IO Stream Configurations
import java.io.*;
import com.bruceeckel.tools.*;
public class IOStreamDemo {
public static void main(String[] args) {
try {
// 1. Buffered input file
DataInputStream in =
new DataInputStream(
new BufferedInputStream(
new FileInputStream(args[0])));
String s, s2 = new String();
while((s = in.readLine())!= null)
s2 += s + "\n";
in.close();
// 2. Input from memory
StringBufferInputStream in2 =
new StringBufferInputStream(s2);
int c;
while((c = in2.read()) != -1)
System.out.print((char)c);
// 3. Formatted memory input
try {
DataInputStream in3 =
new DataInputStream(
new StringBufferInputStream(s2));
while(true)
System.out.print((char)in3.readByte());
} catch(EOFException e) {
System.out.println(
"End of stream encountered");
}
// 4. Line numbering & file output
try {
LineNumberInputStream li =
new LineNumberInputStream(
new StringBufferInputStream(s2));
DataInputStream in4 =
new DataInputStream(li);
PrintStream out1 =
new PrintStream(
new BufferedOutputStream(
new FileOutputStream(
"IODemo.out")));
while((s = in4.readLine()) != null )
out1.println(
"Line " + li.getLineNumber() + s);
out1.close(); // finalize() not reliable!
} catch(EOFException e) {
System.out.println(
"End of stream encountered");
}
// 5. Storing & recovering data
try {
DataOutputStream out2 =
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("Data.txt")));
out2.writeBytes(
"Here's the value of pi: \n");
out2.writeDouble(3.14159);
out2.close();
DataInputStream in5 =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("Data.txt")));
System.out.println(in5.readLine());
System.out.println(in5.readDouble());
} catch(EOFException e) {
System.out.println(
"End of stream encountered");
}
// 6. Reading/writing random access files
RandomAccessFile rf =
new RandomAccessFile("rtest.dat", "rw");
for(int i = 0; i < 10; i++)
rf.writeDouble(i*1.414);
rf.close();
rf =
new RandomAccessFile("rtest.dat", "rw");
rf.seek(5*8);
rf.writeDouble(47.0001);
rf.close();
rf =
new RandomAccessFile("rtest.dat", "r");
for(int i = 0; i < 10; i++)
System.out.println(
"Value " + i + ": " +
rf.readDouble());
rf.close();
// 7. File input shorthand
InFile in6 = new InFile(args[0]);
String s3 = new String();
System.out.println(
"First line in file: " +
in6.readLine());
in6.close();
// 8. Formatted file output shorthand
PrintFile out3 = new PrintFile("Data2.txt");
out3.print("Test of PrintFile");
out3.close();
// 9. Data file output shorthand
OutFile out4 = new OutFile("Data3.txt");
out4.writeBytes("Test of outDataFile\n\r");
out4.writeChars("Test of outDataFile\n\r");
out4.close();
} catch(FileNotFoundException e) {
System.out.println(
"File Not Found:" + args[0]);
} catch(IOException e) {
System.out.println("IO Exception");
}
}
} ///:~
Input
streams
Of
course, one common thing you’ll want to do is print formatted output to
the console, but that’s already been simplified in the package
com.bruceeckel.tools
created in Chapter 5.
Parts
1 through 4 demonstrate the creation and use of input streams (although part 4
also shows the simple use of an output stream as a testing tool).
1.
Buffered input file
To
open a file for input, you use a FileInputStream
with a
String
or a
File
object as the file name. For speed, you’ll want that file to be buffered
so you give the resulting handle to the constructor for a BufferedInputStream.
To read input in a formatted fashion, you give that resulting handle to the
constructor for a DataInputStream,
which is your final object and the interface you read from.
In
this example, only the readLine( )
method is used, but of course any of the
DataInputStream
methods are available. When you reach the end of the file,
readLine( )
returns
null
so that is used to break out of the
while
loop.
The
String
s2
is used to accumulate the entire contents of the file (including newlines that
must be added since
readLine( )
strips them off).
s2
is
then used in the later portions of this program. Finally,
close( )
is called to close the file. Technically,
close( )
will be called when
finalize( )
is run, and this is supposed to happen (whether or not garbage collection
occurs) as the program exits. However, Java 1.0
has a rather important bug, so this doesn’t happen. In Java 1.1
you must explicitly call System.runFinalizersOnExit(true)
to guarantee that
finalize( )
will be called for every object in the system. The safest approach is to
explicitly call close( )
for files.
2.
Input from memory
This
piece takes the
String
s2
that now contains the entire contents of the file and uses it to create a StringBufferInputStream.
(A
String,
not a StringBuffer,
is required as the constructor argument.) Then
read( )
is used to read each character one at a time and send it out to the console.
Note that
read( )
returns the next byte as an
int
and thus it must be cast to a
char
to print properly.
3.
Formatted memory input
The
interface for
StringBufferInputStream
is limited, so you usually enhance it by wrapping it inside a DataInputStream.
However, if you choose to read the characters out a byte at a time using
readByte( ),
any value is valid so the return value cannot be used to detect the end of
input. Instead, you can use the available( )
method
to find out how many more characters are available. Here’s an example
that shows how to read a file one byte at a time:
//: TestEOF.java
// Testing for the end of file while reading
// a byte at a time.
import java.io.*;
public class TestEOF {
public static void main(String[] args) {
try {
DataInputStream in =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("TestEof.java")));
while(in.available() != 0)
System.out.print((char)in.readByte());
} catch (IOException e) {
System.err.println("IOException");
}
}
} ///:~
Note
that
available( )
works differently depending on what sort of medium you’re reading from
– it’s literally “the number of bytes that can be read
without
blocking
.”
With a file this means the whole file, but with a different kind of stream this
might not be true, so use it thoughtfully.
You
could also detect the end of input in cases like these by catching an
exception. However, the use of exceptions for control flow is considered a
misuse of that feature.
4.
Line numbering and file output
This
example shows the use of the LineNumberInputStream
to keep track of the input line numbers. Here, you cannot simply gang all the
constructors together, since you have to keep a handle to the
LineNumberInputStream.
(Note that this is
not
an inheritance situation, so you cannot simply cast
in4
to a
LineNumberInputStream.)
Thus,
li
holds the handle to the
LineNumberInputStream,
which is then used to create a
DataInputStream
for easy reading.
This
example also shows how to write formatted data to a file. First, a FileOutputStream
is created to connect to the file. For efficiency, this is made a BufferedOutputStream,
which is what you’ll virtually always want to do, but you’re forced
to do it explicitly. Then for the formatting it’s turned into a PrintStream.
The data file created this way is readable as an ordinary text file.
One
of the methods that indicates when a DataInputStream
is exhausted is readLine( ),
which returns
null
when there are no more strings to read. Each line is printed to the file along
with its line number, which is acquired through
li. You’ll
see an explicit
close( )
for
out1,
which would make sense
if
the program were to turn around and read the same file again. However, this
program ends without ever looking at the file
IODemo.out.
As mentioned before, if you don’t call
close( )
for all your output files, you might discover that the buffers don’t get
flushed so they’re incomplete.
Output
streams
The
two primary kinds of output streams are separated by the way they write data:
one writes it for human consumption, and the other writes it to be re-acquired
by a DataInputStream.
The RandomAccessFile
stands alone, although its data format is compatible with the
DataInputStream
and DataOutputStream.
5.
Storing and recovering data
A
PrintStream
formats data so it’s readable by a human. To output data so that it can
be recovered by another stream, you use a
DataOutputStream
to write the data and a
DataInputStream
to recover the data. Of course, these streams could be anything, but here a
file is used, buffered for both reading and writing.
Note
that the character string is written using writeBytes( )
and not writeChars( ).
If you use the latter, you’ll be writing the 16-bit Unicode characters.
Since there is no complementary “readChars” method in
DataInputStream,
you’re stuck pulling these characters off one at a time with readChar( ).
So for ASCII, it’s easier to write the characters as bytes followed by a
newline; then use readLine( )
to read back the bytes as a regular ASCII line.
The
writeDouble( )
stores the
double
number to the stream and the complementary readDouble( )
recovers it. But for any of the reading methods to work correctly, you must
know the exact placement of the data item in the stream, since it would be
equally possible to read the stored
double
as a simple sequence of bytes, or as a
char,
etc. So you must either have a fixed format for the data in the file or extra
information must be stored in the file that you parse to determine where the
data is located.
6.
Reading and writing random access files
As
previously noted, the
RandomAccessFile
is almost totally isolated from the rest of the IO hierarchy, save for the fact
that it implements the
DataInput
and
DataOutput
interfaces. So you cannot combine it with any of the aspects of the
InputStream
and
OutputStream
subclasses. Even though it might make sense to treat a
ByteArrayInputStream
as a random access element, you can use
RandomAccessFile
to only open a file. You must assume a
RandomAccessFile
is properly buffered since you cannot add that.
The
one option you have is in the second constructor argument: you can open a
RandomAccessFile
to read (
“r”)
or read and write (
“rw”). Using
a
RandomAccessFile
is like using a combined
DataInputStream
and
DataOutputStream
(because it implements the equivalent interfaces). In addition, you can see that seek( )
is used to move about in the file and change one of the values.
Shorthand
for file manipulation
Since
there are certain canonical forms that you’ll be using regularly with
files, you may wonder why you have to do all of that typing – this is one
of the drawbacks of the decorator pattern. This portion shows the creation and
use of shorthand versions of typical file reading and writing configurations.
These shorthands are placed in the
package
com.bruceeckel.tools
that was begun in Chapter 5 (See page
196).
To add each class to the library, simply place it in the appropriate directory
and add the
package
statement.
7.
File input shorthand
The
creation of an object that reads a file from a buffered
DataInputStream
can be encapsulated into a class called
InFile:
//: InFile.java
// Shorthand class for opening an input file
package com.bruceeckel.tools;
import java.io.*;
public class InFile extends DataInputStream {
public InFile(String filename)
throws FileNotFoundException {
super(
new BufferedInputStream(
new FileInputStream(filename)));
}
public InFile(File file)
throws FileNotFoundException {
this(file.getPath());
}
} ///:~
Both
the
String
versions of the constructor and the
File
versions are included, to parallel the creation of a
FileInputStream. Now
you can reduce your chances of repetitive stress syndrome while creating files,
as seen in the example.
8.
Formatted file output shorthand
The
same kind of approach can be taken to create a
PrintStream
that writes to a buffered file. Here’s the extension to
com.bruceeckel.tools:
//: PrintFile.java
// Shorthand class for opening an output file
// for human-readable output.
package com.bruceeckel.tools;
import java.io.*;
public class PrintFile extends PrintStream {
public PrintFile(String filename)
throws IOException {
super(
new BufferedOutputStream(
new FileOutputStream(filename)));
}
public PrintFile(File file)
throws IOException {
this(file.getPath());
}
} ///:~
Note
that it is not possible for a constructor to catch an exception that’s
thrown by a base-class constructor.
9.
Data file output shorthand
Finally,
the same kind of shorthand can create a buffered output file for data storage
(as opposed to human-readable storage):
//: OutFile.java
// Shorthand class for opening an output file
// for data storage.
package com.bruceeckel.tools;
import java.io.*;
public class OutFile extends DataOutputStream {
public OutFile(String filename)
throws IOException {
super(
new BufferedOutputStream(
new FileOutputStream(filename)));
}
public OutFile(File file)
throws IOException {
this(file.getPath());
}
} ///:~
It
is curious (and unfortunate) that the Java library designers didn’t think
to provide these conveniences as part of their standard.
Reading
from standard input
Following
the approach pioneered in Unix of “standard input,” “standard
output,” and “standard error output,” Java has System.in,
System.out,
and System.err.
Throughout the book you’ve seen how to write to standard output using
System.out,
which is already pre-wrapped as a
PrintStream
object.
System.err
is likewise a
PrintStream,
but
System.in
is a raw
InputStream,
with no wrapping. This means that while you can use
System.out
and
System.err
right away,
System.in
must be wrapped before you can read from it.
Typically,
you’ll want to read input a line at a time using
readLine( ),
so you’ll want to wrap
System.in
in a
DataInputStream.
This is the “old” Java 1.0
way to do line input. A bit later in the chapter you’ll see the Java 1.1
solution. Here’s an example that simply echoes each line that you type in:
//: Echo.java
// How to read from standard input
import java.io.*;
public class Echo {
public static void main(String[] args) {
DataInputStream in =
new DataInputStream(
new BufferedInputStream(System.in));
String s;
try {
while((s = in.readLine()).length() != 0)
System.out.println(s);
// An empty line terminates the program
} catch(IOException e) {
e.printStackTrace();
}
}
} ///:~
The
reason for the
try
block is that readLine( )
can throw an
IOException.
Note that
System.in
should also be buffered, as with most streams
It’s
a bit inconvenient that you’re forced to wrap
System.in
in a
DataInputStream
in each program, but perhaps it was designed this way to allow maximum
flexibility.
Piped
streams
The
PipedInputStream
and PipedOutputStream
have been mentioned only briefly in this chapter. This is not to suggest that
they aren’t useful, but their value is not apparent until you begin to
understand multithreading, since the piped streams are used to communicate
between threads. This is covered along with an example in Chapter 14.