Bruce Eckel's Thinking in Java Contents | Prev | Next

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.

Contents | Prev | Next