JAVA I/O


Contents


Overview

Most Java I/O is defined using streams (random access I/O is also provided via the class java.io.RandomAccessFile, but that will not be discussed here). An input stream brings in information to your program from some source, and an output stream sends information from your program to some destination. The source of an input stream can be the standard input, a file, a pipe, or the program's memory. Similarly, the destination of an output stream can be the standard output, the standard error, a file, a pipe, or memory. (Only the use of standard input/output/error and files are discussed here. See a Java reference book for a more in-depth discussion of Java I/O.)

The package java.io provides two class hierarchies that support streams; one for byte streams and the other for character streams. (Don't forget to include

in code that uses these classes!) Only byte streams were included in the original version of Java; character streams were added to handle characters that require more than 1 byte of storage (for example, Chinese characters). Therefore, you should usually use character streams for I/O; however, there are some cases when it is useful to use byte streams. (The standard input, output, and error streams -- System.in, System.out, and System.err -- are all byte streams. Byte streams are also appropriate when reading/writing binary files, but that will not be discussed here.)

Which particular class you use for I/O will depend on where you want to read from or write to (e.g., the standard input or output vs. a file), and whether you want to read or write "human-readable" data (i.e., ascii characters) or binary data.

Ascii Output

As we have seen before, the simplest way to produce human-readable (ascii) output is to use System.out.print and System.out.println to write to standard output (these methods are overloaded to handle the output of all primitive types, as well as the output of non-primitive types using their toString methods). Similarly, you can use System.err.print and System.err.println to write to standard error.

To write human-readable output to a file, you should use a PrintWriter. The PrintWriter class includes the following methods:

Note that although standard output and standard error (System.out and System.err) provide the same methods, they are not PrintWriters; they are PrintStreams, which are byte streams rather than character streams. You can convert System.out to a PrintWriter by using System.out as the argument to the PrintWriter constructor (and similarly for System.err):

You would need to do this, for example, if you wanted to pass System.out as an argument to a method whose formal parameter was of type PrintWriter.

Not every PrintWriter writes to a file; for example, if you convert System.out to a PrintWriter as described above, it will still write to the standard output. To create a PrintWriter that does write to a file, you must first create a FileWriter. The FileWriter class has a constructor that takes a String as its argument (the name of the file), and another constructor that takes a File as its argument. So to create a FileWriter to write to the file output.text, you could use either of the following code fragments:

The advantage of first creating a File, is that you can test its properties before trying to write to it. The File class includes the following methods:

Efficiency must also be considered when writing to a file. If you create a PrintWriter from a FileWriter, the file will be accessed every time you call the print or println methods. It is more efficient to use buffering, which can be done by creating a BufferedWriter from your FileWriter, and using the BufferedWriter as the argument to the PrintWriter's constructor. For example, here's how you might create a PrintWriter to write to the file output.text:


TEST YOURSELF #1

  1. Write a program that writes the numbers from 1 to 10, one per line, to the file named on the command line if that file does not already exist, and otherwise gives an error message.

  2. Read more about the File class. Write a program that counts the number of "normal" (non-directory files) in the directory named on the command-line, and in all subdirectories, and writes the total to standard output.

solution


Ascii Input

Two reasonable ways to read ascii values are:

  1. Read each character and parse the input yourself (i.e., put the characters together to form numbers, names, etc).
  2. Use a StreamTokenizer to read one token at a time (skipping whitespace).
To read one character at a time from standard input, use System.in.read() (which returns the next byte of data as an int, or the value -1 if the end of input has been reached). To read one character at a time from a file, create a BufferedReader from a FileReader from a File, and use the BufferedReader's read method. For example, the following code counts the number of characters in the input file input.text:
	File inFile;
	BufferedReader br;

	inFile = new File("input.text");
	if (!inFile.exists() || !inFile.canRead()) {
	    System.err.println("ERROR: cannot read file input.text");
	}
	else {
	    int ch, total=0;
	    
	    br = new BufferedReader(new FileReader(inFile));
	    while ((ch = br.read()) != -1) total++;

	    System.out.println(total + " chars");
	    br.close();
	}


TEST YOURSELF #2

Write a program that copies the first file named on the command line to the second file named on the command line, removing all comments. A comment starts with two slashes (//) and continues to the end of the current line. For example, if the input file is:

then the output file should be:

solution


StreamTokenizer

The StreamTokenizer class provides the method nextToken, which skips whitespace, and returns an integer value indicating the type of the next token in the input. It also sets the StreamTokenizer field ttype to the same value. For tokens that are words, the nextToken method sets the sval field of the StreamTokenizer to the actual value of the token, and for tokens that are numbers, the nextToken method sets the nval field of the StreamTokenizer to the actual value of the token. There are four predefined values for token types:

If the end of file has not been reached, and the next token is a character that is not a newline, and is not part of a word or a number, the value returned by nextToken is the character itself.

The StreamTokenizer class also provides a pushBack method, which causes the next call to nextToken to return the same token again, and a lineno method, which returns the current line number.

To create a StreamTokenizer to read from standard input, call the StreamTokenizer constructor function with System.in as its argument. To create a StreamTokenizer to read from a file, call the StreamTokenizer constructor function with a BufferedReader, created using a FileReader associated with the file. (As for FileWriter, you can create a FileReader using a String -- the file name -- or using a File object.) For example, the following code uses a StreamTokenizer to read the file grades.text, which contains a list of student names, each followed by a list of grades. It computes the average for each student, and writes the students' names and their average grades to the file averages.text:

	BufferedReader br = new BufferedReader(new FileReader("grades.text"));
	StreamTokenizer input = new StreamTokenizer(br);
	PrintWriter output = new PrintWriter(new BufferedWriter(new FileWriter(
	                     "averages.text")));
	
	// read info for each student until end-of-file
	while (input.nextToken() != StreamTokenizer.TT_EOF) {
	    if (input.ttype != StreamTokenizer.TT_WORD) {
		// bad data in input -- give error msg and quit
		System.err.println("Bad input on line " + input.lineno());
		System.err.println("Student name expected.");
		System.exit(1);
	    }
	    String name = input.sval;
	    int grade, total = 0;
	    int numGrades = 0;
	    
	    // read all grades for current student
	    while (input.nextToken() == StreamTokenizer.TT_NUMBER) {
		grade = (int)input.nval;
		total += grade;
		numGrades++;
	    }
	    
	    // put back the current token (which should be EOF or WORD)
	    // to be read again in the main loop
	    input.pushBack();

	    // write student name and average
	    output.println(name + "\t" + (double)total/numGrades);
	}

	// close input and output files
	br.close();
	output.close();
StreamTokenizer methods are provided to define which characters make up words (the default is upper- and lower-case letters and digits), which characters are whitespace (the default is spaces, tabs, and newlines), which characters delimit quoted strings, and which characters start single-line comments (such comments are treated as whitespace).


TEST YOURSELF #3

Write a program that reads a file of sums and products (one per line) and writes the values of the expressions to another file. The two file names are to be supplied on the command line. For example, if the input file contains:

then the output file should contain:

solution


Summary

Shown below is part of the inheritance hierarchy related to Java I/O (all of these classes are defined in the package java.io). The classes that implement character streams are shown in red; those that implement byte streams are shown in blue.

The following tables summarize how to construct one class from another, by giving the types of the arguments to the classes's constructor functions. (Note: these tables only provide partial information; in many cases there are other constructors as well.)

CHARACTER STREAMS

CLASS CONSTRUCTOR ARG TYPES
BufferedWriter Writer
FileWriter File or String
PrintWriter OutputStream or Writer
BufferedReader Reader
FileReader File or String

OTHER

CLASS CONSTRUCTOR ARG TYPES
File String
StreamTokenizer InputStream or Reader

Solutions to Self-Study Questions

Test Yourself #1

Test Yourself #2

Test Yourself #3