Contents  Previous  Next


I/O

In this section, we discuss the basics of character stream file I/O in Java. We will use as our examples three programs that copy a file to a new location. These are not GUI programs - they run from the command line and report errors to standard output.

Simple file I/O

Here is our first example:
import java.io.*;

public class Copy1 {
  public static void main(String[] args) throws IOException {
    if (args.length == 2) {
      BufferedReader input = new BufferedReader(new FileReader(args[0]));
      BufferedWriter output = new BufferedWriter(new FileWriter(args[1]));
      while (input.ready()) {
        output.write(input.read());
      }
      input.close();
      output.close();
    }
    else {
      System.out.println("usage: java Copy1 source-file dest-file");
    }
  }
}
This program uses two command-line arguments; hence, in order to run the program, you will need to issue the DOS command
java Copy1 source-file dest-file
where source-file is an existing file, and dest-file is the name of the new copy.

Notice that this program uses the package java.io; you might want to refer to the documentation for that package.

In the method header for main, there is a clause throws IOException. We will discuss this clause later. In the meantime, notice how the parameter for main is used in this example. Two command-line arguments are required, so args.length is compared for identity with the int value 2. Every array in Java has, in addition to the fields inherited from Object, a final int length field, which tells how many elements belong to the array. The elements are indexed from 0 to length - 1 using square brackets, as in the first two statements in the body of the if statement above. Thus, the if statement is executed if there are exactly two command-line arguments, and these arguments are found in args[0] and args[1], respectively; otherwise, the else statement is executed, printing an error message to standard output.

Consider the first statement of the if block:

      BufferedReader input = new BufferedReader(new FileReader(args[0]));
This statement declares and constructs a BufferedReader, which is a class in the package java.io. This class provides for efficient reading from a character input stream. Because the input is considered to be a stream of characters, Java will read from it a character at a time; however, we don't want to do a disk or network access each time we read a character, because this would be very inefficient. The BufferedReader class causes the input to be buffered; i.e., a fixed-sized block of characters is first read into a memory buffer. Then the BufferedReader provides access to one character at a time until the buffer is empty, at which time it reads a new block of characters into the buffer.

The argument to the constructor is a FileReader, whose class also belongs to java.io. This class provides the most convenient way of associating a file of characters (note that any file can be considered to be a file of characters) with an object in Java. This is accomplished with one of the FileReader constructors. The constructor used in the above example simply takes the name of the file as a String.

Take a look at the documentation for the constructor FileReader(String). Notice the clause, throws FileNotFoundException, which has a similar syntax to the clause, throws IOException in the definition of our main method. A throws clause in a method header declares that the execution of that method can result in the occurrence of a particular kind of error. Suppose, for example, that the String passed to the constructor for FileReader is not the name of an existing file. It is then impossible for the constructor to complete its task, becase there is no file to associate with the object it is constructing. The mechanism that Java provides for handling such error conditions is that of throwing either exceptions or errors. Java provides a class Throwable (in java.lang) whose subclasses consist of all of the errors or exceptions that might be thrown. Unless the method that encounters the error or exception provides a means of handling it (we will see how to do that shortly), that method immediately terminates, and the error or exception is thrown in its calling method. The subclass Exception contains all of the exceptions that a Java application might want to handle on its own - we will see how to do that shortly. The subclass Error of Throwable contains more serious errors that Java applications should not try to handle. The subclass RuntimeException of Exception contains those exceptions that Java might want to handle in some cases, but which normally can be avoided by careful programming (in fact, the normal cause of these exceptions is an error in the program). An example of a RuntimeException is NullPointerException, which is thrown when a program attempts to access a field or method of a reference variable whose value is null; in a well-written program, such an exception should not occur. The remaining subclasses of Exception are exceptions which ordinarily should be handled somewhere in the program, so any method that can cause one of these exceptions to be thrown must alert the calling program to this fact by declaring that exception in a throws clause. If the calling program does not handle this exception, and does not declare it in a throws clause, a compile-time error will result.

Look at the documentation for FileNotFoundException. It is a subclass of Exception, but not RuntimeException; therefore, it must either be handled by main or declared in the throws clause of main. Notice that that FileNotFoundException is a subclass of IOException, so the clause throws IOException in the header of main includes FileNotFoundException.

The second line of the if block declares and constructs a BufferedWriter, which is associated with the file given as the second command-line argument.

The third line of the if block begins a while statement. The while statement has two possible forms:

while (boolean-expression) statement;
or
while (boolean-expression) {statement-list};
where boolean-expression is an expression whose result is of type boolean, statement is any statement that can be included in a method body, and statement-list is any sequence of statements that can be included in a method body. If boolean-expression evaluates to true, statement or statement-list is executed, and the while statement is executed again; otherwise, the remainder of the while statement is skipped.

The boolean expression in this while statement is

input.ready()
ready is a method of BufferedReader that returns true if there are more characters to be read; therefore, the loop iterates while there remain characters in the input file that have not been read. Notice that ready throws IOException. As we are discovering, several of the methods called within the main method throw IOException or one of its subclasses. By declaring that main throws IOException, we have covered all of these possibilities.

The body of the loop consists of a single statement:

        output.write(input.read());
write is a method of BufferedWriter; this method writes a single character. What is odd about this method is that its parameter is of type int. The write method writes the low-order 16 bits of its parameter (Java uses Unicode characters, which are 16 bits, although the subset which corresponds to ASCII characters is typically written as a single byte). Why would this method take an int parameter, if it is going to treat is as a character? It does this so that the write method is consistent with the read method of BufferedReader. Take a look at the documentation for this method. It reads a single character, but returns an int value. The reason for this is that this method was designed to handle an attempt to read past the end of the file by returning a value that will not be confused with a valid Unicode character, namely -1 (this should be included in the documentation, but is not). Conversions between char and int usually are done automatically in Java; however, care must be taken when invoking a method that is defined differently for the two different types of paramters. We will consider such an example later.

To summarize, the while loop repeatedly reads a character from the input file and writes it to the output file until the entire input has been consumed. Finally, both the input file and output file are closed. This is particularly important for a BufferedWriter, because closing the file causes its buffer to be flushed (i.e., all characters remaining in the buffer are written to the file), and updates the directory containing the the file.

Try running this program with a source file that does not exists. Your response will be something like

java.io.FileNotFoundException: badname
        at java.io.FileInputStream.(FileInputStream.java)
        at java.io.FileReader.(FileReader.java)
        at Copy1.main(Copy1.java:6)
where badname is the first command-line argument. This output contains a stack trace, which usually indicates a bug in the program. In the next section, we will show how to handle exceptions so that the stack trace doesn't appear.

Exception Handling

In this section, we will demonstrate exception handling by showing how to handle IOExceptions in our file copying program. Exceptions are handled using a try-catch block as in the following example:
import java.io.*;

public class Copy2 {
  public static void main(String[] args) {
    if (args.length == 2) {
      try {
        BufferedReader input = new BufferedReader(new FileReader(args[0]));
        BufferedWriter output = new BufferedWriter(new FileWriter(args[1]));
        while (input.ready()) {
          output.write(input.read());
        }
        input.close();
        output.close();
      }
      catch (IOException e) {
        System.out.println(e);
      }
    }
    else {
      System.out.println("usage: Copy2 source-file dest-file");
    }
  }
}
The only differences between Copy1 and Copy2 are: The additions to the body of the if block handle all IOExceptions, so that the throws clause is no longer required. The additions to the code comprise a try statement, which is of the form:
try {statement-list} catch-clauses
where catch-clauses is one or more clauses of the form
catch (throwable-type identifier) {statement-list}
and statement-list is any sequence of statements that may be included in a method body. In the catch clauses, throwable-type is the name of any subclass of Throwable, and identifier is a new variable name. The statements in the try block are executed normally, but if an error or exception is thrown by any of these statements, control exits the try block. At this point, execution proceeds as if the following method call were made:
catch (throwable);
where throwable is the Throwable object that was thrown. Thus, each of the catch clauses is treated like the definition of a method with return type void. If more than one of the catch clauses has parameter of a type to which the thrown object belongs, the first of these is called, then, assuming the catch clause returns normally, control resumes at the statement following the last catch clause. If none of the parameters of the catch clauses match the type of the thrown object, the try statement throws this object.

Let's see how this works in Copy2. The try block contains several statements that can throw an IOException object (or an object belonging to a subclass of IOException). If one of these exceptions occurs, the catch clause is called, with the exception thrown as its formal parameter e. This IOException object is then printed to standard output, and the main method finishes normally. This is not a very elegant exception handler, but it does handle all IOExceptions so that a stack trace is not generated when they occur (try copying a nonexistent file with program to see the result). Note also that only IOExceptions are handled by this exception handler.

Using Strings and StringBuffers in I/O

We conclude our discussion of I/O by showing how text can be read into or written from Strings or StringBuffers. This is illustrated in the following modification of our file copying program:
import java.io.*;

public class Copy3 {
  public static void main(String[] args) {
    if (args.length == 2) {
      try {
        BufferedReader input = new BufferedReader(new FileReader(args[0]));
        BufferedWriter output = new BufferedWriter(new FileWriter(args[1]));
        StringBuffer buf = new StringBuffer();
        while (input.ready()) {
          buf.append((char) input.read());
        }
        output.write(buf.toString());
        input.close();
        output.close();
      }
      catch (IOException e) {
        System.out.println(e);
      }
    }
    else {
      System.out.println("usage: Copy3 source-file dest-file");
    }
  }
}
The above program is included only as an illustration, not as an improvement over Copy2; Copy2 is, in fact a better solution to the file copying problem. However, this example is included because most programs that do I/O on text files need to store that text. This example shows how to do that.

A StringBuffer is similar to a String, except that the contents of a StringBuffer may change. For efficiency reasons, it is best to use a StringBuffer to accumulate the text. Instead of immediately writing each character as it is read, each character is now appended to a StringBuffer. Notice the statement that actually does the appending:

          buf.append((char) input.read());
This statement contains an explicit type-cast: (char). A type-cast causes the expression immediately following it to be treated as the given type. In this case, the read method returns and int, but our type-cast explicitly converts it to type char. We stated earlier that this conversion is normally done automatically when and int is passed to a method that requires a char; however, if we look at the StringBuffer documentation (in the package java.lang), we see that there are append methods that take single parameters of both of these types. Thus, if the type-cast is omitted, the append(int) method will be called, rather than the append(char) method. The problem with this is that these two methods have different results. The append(char) method appends its parameter as a single character to the StringBuffer. However, the append(int) method first converts its int parameter to a String giving the decimal representation of the int, then appends this String to the StringBuffer. Thus, if the character 'a' were read from the file and passed as an int to append, the value appended would be the String "97".

The other statement to notice is

        output.write(buf.toString());
The write method of BufferedWriter will take a String parameter, so there is no need to write the StringBuffer a character at a time. All we need to do is convert the StringBuffer to a String using its toString method, which is very efficient as long as we don't make any subsequent changes to the StringBuffer. The reason for this is that when converting a StringBuffer to a String, Java allows the two structures to share the storage containing their character contents until the contents of the StringBuffer are changed. If and when such a change occurs, the contents are copied to a new location.


Contents  Previous  Next


Copyright © 1998, Rod Howell
Permission is granted to copy any of the text in this tutorial for private noncommercial use. Permission is granted to distribute any part of this tutorial, provided that no fee is charged and that this copyright notice is included.

Last modified 8/24/98 - Rod Howell, howell@cis.ksu.edu