Concepts / Reading Text Data with Readers in Java

Reading Text Data with Readers in Java

Buffering improves I/O efficiency by moving data in large chunks, which suits some devices better than reading individual bytes or characters.

  • Programming

Why Reading One Character at a Time Is a Problem

When a Java program reads text from a file or another input device, it has to talk to that device. Many learners first meet BufferedReader as a class with a convenient method for reading a whole line of text. That convenience is real, but it is not the main reason the class exists. To understand the real purpose, you first need to understand a technique called buffering.

Some I/O devices work most efficiently when data is read or written in large chunks instead of as individual bytes or characters. This is the reason buffering exists. If a program asked such a device for one character at a time, it would be working against the way the device is best used. Buffering is the response to this fact: rather than matching the program's read pattern to the device, the reader fetches data in a form the device handles well and keeps that data ready for the program.

Key point

Buffering is a technique for improving I/O efficiency by moving data in large chunks rather than in individual bytes or characters.

What a Buffer Is

Definition

A buffer is internal memory that stores a chunk of data read from an I/O device. It is the place where a reader keeps data it has already fetched, so that the program can take that data without going back to the device every time.

The buffer holds several pieces of data at once, waiting to be read one after another. The source illustrates this with a generic buffer holding four characters that were read together from a source. Those characters are placeholders chosen for illustration, and the buffer's actual size is not specified in the source. The important idea is the logical behavior: the chunk sits in internal memory, and reads are served from it. The picture is a teaching model, not a statement about how the memory is physically arranged.

Where the Buffer's Data Comes From

The data in the buffer is supplied by the input source that the reader is attached to, such as a file reached through a FileReader. The reader performs a bulk read from that source, which fills the buffer with a whole chunk in one operation. The data then travels in one direction: from the file or device, into the buffer in internal memory, and from the buffer to the program as the program asks for characters.

Input sourcefile or deviceBufferchunk in internal memoryProgramrequests charactersone bulk read fills chunkcharacters served one at a time
Which component supplies the characters that end up in the buffer, and in what direction does the data travel from the file or device to the program?

Notice the two different rates in this flow. The device is contacted in large bulk reads, while the program may take characters from the buffer many times, one after another. The program is therefore reading from fast internal storage for most of its requests, and the device is only involved when a new chunk is needed.

How BufferedReader Decides Where to Read

When the program reads from a BufferedReader, the reader first checks its buffer. If the buffer can supply the data, it does so, and the input source is left alone. The reader only goes back to its input source for more data when the buffer has been emptied. This decision happens on every read, so the same check repeats until the buffer runs out.

read() calledBuffer has data?checked firstServe from bufferinput source untouchedReturn dataFetch new chunkfrom input sourceyesno, buffer emptiedbuffer now filled
When read() is called, what check happens first, and when does the reader go back to the underlying source such as a FileReader to fetch more data?

Tracing a Sequence of Reads

Worked example

Tracing reads through a four-character buffer

A hypothetical reader uses a buffer that holds four characters, and the source text is the word JAVA followed by more characters. The program calls read() five times in a row. For each call, decide whether the reader serves the character from the buffer or fetches a new chunk from the input source. The actual buffer size is not specified by the source, so this four-character size is an illustrative assumption.

  1. 1
    Call 1

    The buffer is empty at the start, so the reader must fetch a chunk. It bulk reads J, A, V, A into the buffer, then serves J to the program.

  2. 2
    Call 2

    The buffer still has A, V, A left, so the reader serves A from the buffer. The input source is not contacted.

  3. 3
    Call 3

    The buffer still has V and A, so the reader serves V from the buffer. Again, no trip to the source.

  4. 4
    Call 4

    The buffer still has one character, A, so the reader serves it. The buffer is now empty.

  5. 5
    Call 5

    The buffer is empty, so the check fails. The reader goes back to the input source, fetches the next chunk, and serves the first character of that chunk.

Result

Only two bulk fetches from the source were needed for five reads. Four of the five calls were answered from the buffer, which is the behavior the buffer exists to provide.

Unbuffered and Buffered Reading Compared

The benefit of buffering becomes clearer when you compare it with reading directly from the device without a buffer. In the unbuffered approach, each request for a character goes to the device. In the buffered approach, a single request brings in a chunk, and the following requests are served from memory until the chunk is used up. The text being read is the same in both cases; what differs is how many times the device must be contacted to deliver it.

One trip percharacterevery request reachesdeviceMany device contactsdevice used as thebottleneckOne trip per chunkbulk read fills bufferReads served frommemorydevice only contacted fornew chunksrepeatsrepeats for each chunk
How many trips to the device does each approach make to read the same amount of text, and what is the practical difference in cost?

The practical difference is that the buffered approach contacts the device far less often for the same amount of text. Because the source states that some devices work best with large chunks, reducing the number of small requests is the point of the design.

Mistakes Beginners Make About BufferedReader

  • Common mistake

    Believing BufferedReader exists only to read lines of text

    What it looks like

    A learner uses readLine() and concludes that the class is just a line-reading helper, so they never think about what happens between the device and the program.

    Why it fails

    The source states directly that the main purpose of BufferedReader is not simply to make it easier to read lines. Lines are one thing the class handles well, but the underlying design is the chunked, buffered movement of data between the program and the I/O device.

    Do this instead

    Treat readLine() as one method that uses the buffer. The buffer is the general mechanism, and line reading is one convenient way to draw from it.

  • Common mistake

    Assuming every read goes to the device

    What it looks like

    A learner imagines that each call to read() sends a fresh request to the file.

    Why it fails

    The reader first checks its buffer. If the buffer can supply the data, the input source is left alone. The source is contacted only after the buffer has been emptied.

    Do this instead

    Think of the input source as a backup that is consulted when the buffer runs dry, not as the target of every call.

  • Common mistake

    Confusing the buffer with the input source

    What it looks like

    A learner says the buffer is the file itself.

    Why it fails

    The source defines a buffer as internal memory that stores a chunk of data read from an I/O device. The file or device is where the data originates, and the buffer is a copy held in internal memory.

    Do this instead

    Keep the two separate: the input source supplies data, and the buffer stores the chunk that was supplied.

  • Common mistake

    Forgetting that the buffer can hold several characters at once, so reads after the first one are cheap

Practice: Predicting Where a Read Is Served

What do you think happens?

A BufferedReader's buffer currently holds two unread characters. The program calls read() once. Does the reader contact the input source?

  • Yes, it always goes back to the input source on every call

  • No, it serves the character from the buffer because the buffer still has data

Reveal answer
Answer:

No, it serves the character from the buffer because the buffer still has data

The reader first checks its buffer. If the buffer can supply the data, it does so and leaves the input source alone. It only goes back to the source once the buffer has been emptied.

MEDIUM

Using the four-character buffer from the worked example, suppose the program makes six read() calls on a source containing the characters ABCDEFGH. How many times does the reader fetch a chunk from the input source, and which characters are served from the buffer on the calls that do not fetch? Explain your answer using the rule that the reader only returns to the source after the buffer is emptied.

Hints
  • Start with an empty buffer, so the first call must fetch a chunk.
  • Count how many characters each chunk supplies before the buffer is empty again.

Key Takeaways

  • Buffering improves I/O efficiency by moving data in large chunks rather than as individual bytes or characters, which suits devices that work best with bulk transfers.
  • A buffer is internal memory that stores a chunk of data read from an I/O device, and the data in it comes from the underlying input source.
  • A BufferedReader checks its buffer first on each read and returns to its input source only when the buffer is emptied.
  • BufferedReader is not simply a tool for reading lines of text; readLine() is one method that draws on the buffer, whose main purpose is the chunked movement of data.