Skip to content

Java Input and Output


No matter where the data is coming from or going to and no matter what its type, the algorithms for sequentially reading and writing data are basically the same:

  • Reading
    • open a stream
    • while more information
      • read information
    • close the stream
  • Writing
    • open a stream
    • while more information
      • write information
    • close the stream

The java.io package contains a collection of stream classes that support these algorithms for reading and writing. To use these classes, a program needs to import the java.io package. The stream classes are divided into two class hierarchies, based on the data type (either characters or bytes) on which they operate.

Character Streams

Reader and Writer are the abstract superclasses for character streams in java.io. Reader provides the API and partial implementation for readers - streams that read 16-bit characters and Writer provides the API and partial implementation for writers—streams that write 16-bit characters. Subclasses of Reader and Writer implement specialized streams and are divided into two categories: those that read from or write to data sinks (shown in gray in the following figures) and those that perform some sort of processing (shown in white). The figure shows the class hierarchies for the Reader and Writer classes Most programs should use readers and writers to read and write textual information. The reason is that they can handle any character in the Unicode character set, whereas the byte streams are limited to ISO-Latin-1 8-bit bytes.

Reader Class Hierarchy

Java Reader class hierarchy. Reader is the abstract base class. Its direct subclasses are BufferedReader, CharArrayReader, InputStreamReader, FilterReader, PipedReader, and StringReader. BufferedReader has one subclass: LineNumberReader. InputStreamReader has one subclass: FileReader, which reads characters from a file on disk. FilterReader has one subclass: PushbackReader.

Reader class hierarchy summary

  • Reader is the abstract base class for all character input streams.
  • Direct subclasses of Reader:
  • BufferedReader reads text efficiently by buffering input. Its subclass LineNumberReader also tracks the current line number.
  • CharArrayReader reads from a character array in memory.
  • InputStreamReader bridges byte streams to character streams. Its subclass FileReader reads characters from a file on disk. This is the class you will use most often.
  • FilterReader is the base class for filtered readers. Its subclass PushbackReader allows characters to be pushed back into the stream after reading.
  • PipedReader reads from a piped character stream, used for communication between threads.
  • StringReader reads from a String in memory.

Writer Class Hierarchy

Java Writer class hierarchy. Writer is the abstract base class. Its direct subclasses are BufferedWriter, CharArrayWriter, OutputStreamWriter, FilterWriter, PipedWriter, and StringWriter. OutputStreamWriter has one subclass: FileWriter, which writes characters to a file on disk.

Writer class hierarchy summary

  • Writer is the abstract base class for all character output streams.
  • Direct subclasses of Writer:
  • BufferedWriter writes text efficiently by buffering output.
  • CharArrayWriter writes to a character array in memory.
  • OutputStreamWriter bridges character streams to byte streams. Its subclass FileWriter writes characters to a file on disk. This is the class you will use most often for file output.
  • FilterWriter is the base class for filtered writers.
  • PipedWriter writes to a piped character stream, used for communication between threads.
  • StringWriter writes to a String buffer in memory.

Using Streams - Input

The most common way to read from a file on disk:

This works just fine:

FileReader reader = new FileReader("testFile.txt");

BufferedReader in = new BufferedReader(reader);

This is usually coded like this, in nested constructors:

BufferedReader  in = new BufferedReader(new FileReader("testFile.txt"));

However! There’s an issue. This code won’t compile because the compiler requires exception handling. Wha? Let’s look at the Java API for these constructors.

public void run() {
    BufferedReader  in = new BufferedReader(new FileReader("testFile.txt"));
}

Java SE 21 API: BufferedReader

If the compiler requires it, we have to do it. We’ll look at exception handling more a bit later. We also need to declare the BufferedReader out of the try so it can be closed in the finally block.

BufferedReader inputReader = null;
try {
    inputReader = new BufferedReader(new FileReader("testFile.txt"));

    // code to process the input file

} catch (FileNotFoundException fileNotFound) {
    System.out.println("There was a problem finding the file.");
    fileNotFound.printStackTrace();
} catch (IOException inputOutputException) {
    System.out.println("There was a problem reading the file.");
    inputOutputException.printStackTrace();
} catch (Exception exception) {
    System.out.println("There was a problem.");
    exception.printStackTrace();
} finally {
    try {
        if (inputReader != null) {
            inputReader.close();
        }
    } catch (Exception exception) {
        System.out.println("There was a problem closing the BufferedReader.");
        exception.printStackTrace();
    }
}

Once the file is opened you will need to read the contents. We do this in a loop and read one line at a time. Here’s what the while loop will look like. (This will go inside the try block above.)

while (inputReader.ready()) {
    // read each line in here
}

Each line of the file is read with this code.

// Declare a String variable to hold the current line
    String line = null;

//  ...    

// Inside the while loop
    line = inputReader.readLine();

Put it all together and you get this.

BufferedReader inputReader = null;
try {
    inputReader = new BufferedReader(new FileReader("testFile.txt"));

    // code to process the input file

    // Declare a String variable to hold the current line
    String line = null;

    while (inputReader.ready()) {
        // read each line in here
        line = inputReader.readLine();

        // now process the line in some way
        // this will usually be in another method that
        // we pass the line to.
    }

} catch (FileNotFoundException fileNotFound) {
    System.out.println("There was a problem finding the file.");
    fileNotFound.printStackTrace();
} catch (IOException ioException) {
    System.out.println("There was a problem reading the file.");
    ioException.printStackTrace();
} catch (Exception exception) {
    System.out.println("There was a problem.");
    exception.printStackTrace();
} finally {
    try {
        if (inputReader != null) {
            inputReader.close();
        }
    } catch (Exception exception) {
        System.out.println("There was a problem closing the BufferedReader.");
        exception.printStackTrace();
    }
}  

AI and I/O code

When you ask an AI tool to write file-reading code, it will almost always use try-with-resources (shown on the next page). That's fine, but you need to understand the BufferedReader + finally pattern first. If you can't read the old form, you can't debug what the AI wrote or explain it in a code review.


Using Streams - Output

For writing to a file we will need three objects.

FileWriter fileWriter = new FileWriter("output.txt");
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
PrintWriter outputWriter = new PrintWriter(bufferedWriter);

You'll never see it that way and we need to get used to nested constructors.

//FileWriter fileWriter = new FileWriter("output.txt");
//BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
//PrintWriter outputWriter = new PrintWriter(bufferedWriter);

PrintWriter outputWriter = new PrintWriter(new BufferedWriter(new FileWriter("output.txt")));

Here's the full code. Notice that we have two catch blocks, not three as in the reader above.

PrintWriter outputWriter = null;
try {
    outputWriter = new PrintWriter(new BufferedWriter(new FileWriter("output.txt")));

    outputWriter.println("Sample output.");
} catch (IOException inputOutputException) {
    System.out.println("There was a problem writing to the file.");
    inputOutputException.printStackTrace();
} catch (Exception exception) {
    System.out.println("There was a problem.");
    exception.printStackTrace();
} finally {
    try {
        if (outputWriter != null) {
            outputWriter.close();
        }
    } catch (Exception exception) {
        System.out.println("There was a problem closing the PrintWriter.");
        exception.printStackTrace();
    }
}

Further Readings

I/O in Java

Exception Handling