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.
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?
Reveal 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
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.
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.
alpha
beta- The
StringReadersupplies the characters; theBufferedReaderwraps it. - The first
readLine()call returns alpha, the first line. - The second
readLine()call continues forward and returns beta.
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?
- 1Start the reading position
Before any call, the reader is at the beginning of the input, before alpha.
- 2First call
readLine()returns alpha and moves the reading position past that line. The variable first now holds alpha. - 3Second call
The reader continues from where it stopped, so
readLine()returns beta. The reader never goes back to alpha. - 4Result
The variable second holds beta, and gamma has not been read yet.
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.
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);
}
}- 1Call 1 returns alpha, which is stored in line. The reading position is now after alpha.
- 2The print statement shows Line: alpha, which matches expectation.
- 3Call 2 has its result discarded. It still reads and consumes beta, so beta is gone from the reading position.
- 4Call 3 returns gamma, which is stored in next. This is the first point where the output differs from the intended Next: beta.
- 5The 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.
Line: alpha
Next: gammaReading 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.
ALPHA
BETA
GAMMAlines()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.
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 likeCalling readLine() twice and expecting the second call to repeat alpha.
Why it failsThe reader does not go back. A line that has already been read is not returned again.
Do this insteadStore 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 likeCalling reader.readLine(); with no assignment and then reading the next line.
Why it failsThe call still moves the reading position forward, so the skipped line is lost to later calls.
Do this insteadAssign 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 failsreadLine()declares that it throws IOException, so code that calls it must handle that exception or pass it along.Do this insteadDeclare 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 failsThe
BufferedReaderis now the layer that reads the characters for you. Mixing reads from both can make the reading position hard to predict.Do this insteadRead 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
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
BufferedReaderis 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, andforEachOrdered()passes each line in order to a consumer, usually a lambda expression, so no explicitreadLine()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.