Concepts / Reading Characters from a Reader

Reading Characters from a Reader

BufferedReader is a Reader that buffers input from a character-input stream, and any Reader can be wrapped in one to read full lines of text.

  • Programming

Why Reading One Line at a Time

Much of the input and output in Java deals with human-readable characters, and a very common job is reading a file or other text source one line at a time. Lines are the natural unit of many text sources, so a program that can ask for the next line, and get exactly that line back, is far easier to write than one that handles raw characters by itself.

Java's character input classes do not all offer the same convenient interface. A Reader can supply characters, but it does not directly give you a way to ask for a whole line of text. The standard class BufferedReader fills that gap. As you work through this article, you will trace small programs call by call, keeping track of what each variable holds and where the reading position sits after every statement.

A Quick Prediction

What do you think happens?

A BufferedReader wraps input whose text is alpha, beta, gamma on three lines. You call readLine() once. What String comes back?

  • alpha

  • beta

  • gamma

  • The whole text as one string

Reveal answer
Answer:

alpha

The first call reads from the beginning of the input and returns the first line. Each later call continues from where the previous one stopped, so the returned line is always the next unread one.

Why Readers Need a Wrapper

Definition

BufferedReader is a Reader that buffers input from a character-input stream. Buffering characters is what allows it to read characters, arrays, and lines efficiently. Any Reader can be placed inside a BufferedReader.

The source describes the relationship as a wrapper. The wrapper adds line-oriented reading on top of the original character source. In the declaration pattern used in the source, a Reader named reader is handed to the BufferedReader constructor, and the result is stored in a variable of type BufferedReader. The code looks like this: BufferedReader wrapped = new BufferedReader(reader);

The reason this matters is that the original Reader is not replaced by something different. It is still the source of the characters. The BufferedReader adds the buffering and the readLine() method around it. When you read through the wrapper, the characters still originate in the underlying Reader, and the wrapper decides how they are gathered and handed to you.

Key point

A BufferedReader does not change where the characters come from. It layers line-based reading on top of an existing Reader, so the same wrapping pattern works for any Reader.

How Characters Reach the Program

The path from the input to your variables passes through three roles: the source text, the underlying Reader that supplies its characters, and the BufferedReader that holds those characters and returns complete lines. The diagram below shows that path for a single readLine() call. Notice that the program only ever talks to the BufferedReader, never directly to the source.

Read the diagram from left to right. The characters enter the picture at the source and are supplied by the underlying Reader. The BufferedReader gathers them in its buffer and produces a complete line when readLine() is called. Only that line, a String, is handed to the program. Because the wrapper sits in the middle, a single BufferedReader can serve any Reader that you choose to wrap.

Tracing Successive readLine() Calls

The defining property of readLine() is that each call moves forward through the input. The reader does not go back, so a line that has already been read is not returned again. The example below wraps a StringReader, which is a Reader, so that the input is the text alpha, beta, gamma written on three lines. It calls readLine() twice and prints both results.

java
Output
alpha
beta
Notes on the code
  • The StringReader supplies the characters; the BufferedReader wraps it.
  • The first readLine() call returns alpha, the first line.
  • The second readLine() call continues forward and returns beta.
Worked example

Predicting the value of second

Using the same input of alpha, beta, gamma on three lines, what does the variable second hold after the program runs?

  1. 1
    Start the reading position

    Before any call, the reader is at the beginning of the input, before alpha.

  2. 2
    First call

    readLine() returns alpha and moves the reading position past that line. The variable first now holds alpha.

  3. 3
    Second call

    The reader continues from where it stopped, so readLine() returns beta. The reader never goes back to alpha.

  4. 4
    Result

    The variable second holds beta, and gamma has not been read yet.

Result

second holds beta, and the printed output is alpha on one line and beta on the next.

Tracing a Divergent Program

Debugging line-reading code usually means finding the exact call where the result stops matching what you expected. The method is to trace the program statement by statement, writing down what each variable holds after it runs and where the reading position now sits. The next program has a mistake that is easy to miss when you only look at its shape.

Code walkthrough
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;

public class SkipByAccident {
    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new StringReader("alpha\nbeta\ngamma"));
        String line = reader.readLine();
        System.out.println("Line: " + line);
        reader.readLine();
        String next = reader.readLine();
        System.out.println("Next: " + next);
    }
}
  1. 1
    Call 1 returns alpha, which is stored in line. The reading position is now after alpha.
  2. 2
    The print statement shows Line: alpha, which matches expectation.
  3. 3
    Call 2 has its result discarded. It still reads and consumes beta, so beta is gone from the reading position.
  4. 4
    Call 3 returns gamma, which is stored in next. This is the first point where the output differs from the intended Next: beta.
  5. 5
    The mistake is not in the printing. A call that moves the reader forward has a lasting effect even when its return value is ignored.
  • The author intended the program to print Next: beta.
  • The divergence happens at the third readLine() call, the one with no assignment.
Output
Line: alpha
Next: gamma

Reading Every Line with lines()

BufferedReader also defines the instance method lines(). It returns a value of type Stream<String>, which means it produces the lines of the reader as a stream that can be used with Java's stream API. The usual pattern calls the forEachOrdered() operator on that stream. The reader is the BufferedReader, lines() supplies the stream of lines, and forEachOrdered() passes each line to an action in order.

The action is a consumer of strings and is usually written as a lambda expression. Because the processing is expressed as an operation applied to the stream, you do not write an explicit loop that calls readLine(). The pipeline below shows how each line moves from the source to the consumer.

java
Output
ALPHA
BETA
GAMMA
Notes on the code
  • lines() returns a Stream<String> built from the reader.
  • forEachOrdered() passes each line to the lambda in order.
  • No explicit loop calls readLine().

Choosing readLine() or lines()

Use readLine() when your program needs control over each step, such as checking a line before deciding whether to read the next one. Use lines() with forEachOrdered() when the same action applies to every line and you want that action expressed as a stream operation. Both approaches start from the same BufferedReader, so the choice depends on the shape of the work rather than on where the data comes from.

Best practice

Choose the approach that matches the work. If every line gets the same treatment, a stream pipeline reads clearly. If the program must decide, line by line, whether to keep reading, call readLine() directly so each step stays visible in the code.

Mistakes When Reading Lines

  • Common mistake

    Expecting readLine() to return the same line again

    What it looks like

    Calling readLine() twice and expecting the second call to repeat alpha.

    Why it fails

    The reader does not go back. A line that has already been read is not returned again.

    Do this instead

    Store the result of each call in its own variable, and expect each call to return the next unread line.

  • Common mistake

    Ignoring the return value of a reading call

    What it looks like

    Calling reader.readLine(); with no assignment and then reading the next line.

    Why it fails

    The call still moves the reading position forward, so the skipped line is lost to later calls.

    Do this instead

    Assign the result to a variable, or deliberately discard it only when you know the line should be skipped.

  • Common mistake

    Forgetting that readLine() throws IOException

    Why it fails

    readLine() declares that it throws IOException, so code that calls it must handle that exception or pass it along.

    Do this instead

    Declare the exception on the calling method with throws IOException, or handle it with a try-catch block.

  • Common mistake

    Reading from the original Reader after wrapping it

    Why it fails

    The BufferedReader is now the layer that reads the characters for you. Mixing reads from both can make the reading position hard to predict.

    Do this instead

    Read through the BufferedReader variable once it has been created.

  • Common mistake

    Assuming the wrapper replaces the source instead of layering on top of it

Practice: Trace Before You Run

EASY

A BufferedReader wraps a Reader whose text is alpha, beta, gamma on three lines. The code calls readLine() three times and stores the results in first, second, and third. Trace each call and state the value of third. Then state the first call where your trace would differ from a program that expected third to hold alpha.

Hints
  • Write down the reading position before the first call, and update it after each one.
  • Each call returns the next unread line, so the third call returns the line after the second one.
  • Compare your traced values with the intended values one call at a time.

Key Takeaways

  • BufferedReader is a Reader that buffers input from a character-input stream, and any Reader can be wrapped in one to gain the ability to read full lines of text.
  • Each readLine() call returns one line and moves the reading position forward; it never returns a line that has already been read, and it declares that it throws IOException.
  • lines() returns a Stream<String> of the reader's lines, and forEachOrdered() passes each line in order to a consumer, usually a lambda expression, so no explicit readLine() loop is needed.
  • When a line-reading program misbehaves, trace each call in sequence and locate the first call where the actual value differs from the expected one.