java-io-streams-lectura-escritura-bytes-char

Reading and Writing in Java: bytes and characters

  • 4 min

The I/O streams transport bytes or characters between the program and a source or destination.

In the previous article, we used Files.readString() and Files.readAllLines(), two convenient methods for small text files.

But what if you want to copy a JPG image? Or a PDF? Or what if the text file is 50GB and doesn’t fit in RAM?
That’s where the utility methods fall short. We need manual control. We need Streams.

In Java I/O, there is a basic division:

  1. Bytes (Binary): For images, sounds, PDFs, executables.
  2. Characters (Text): For .txt, .json, .xml, .java files.

Guide to choosing an I/O class

Data TypeOperationBase Class (Abstract)File ImplementationBuffer (Recommended)
TextReadingReaderFileReaderBufferedReader
TextWritingWriterFileWriterBufferedWriter
BinaryReadingInputStreamFileInputStreamBufferedInputStream
BinaryWritingOutputStreamFileOutputStreamBufferedOutputStream

Working with text (characters)

When working with text, Java needs to know which encoding to use (UTF-8, ISO-8859-1…). The classes responsible for this are called Readers and Writers.

The key tool: BufferedReader and BufferedWriter

Reading character by character is inefficient (every letter means a trip to the hard drive). To avoid this, we use Buffers. A buffer reads a large chunk at once into memory and feeds it to you gradually.

Efficient line-by-line reading:

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;

Path path = Path.of("quijote.txt");

// Use try-with-resources to automatically close the stream
// Files.newBufferedReader creates an optimized Reader
try (BufferedReader reader = Files.newBufferedReader(path)) {

    String line;
    // readLine() returns null when the end of the file is reached
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }

} catch (IOException e) {
    e.printStackTrace();
}
Copied!

Efficient writing:

Path output = Path.of("output.txt");

try (BufferedWriter writer = Files.newBufferedWriter(output)) {

    writer.write("Hello World");
    writer.newLine(); // Line separator compatible with the OS (\n or \r\n)
    writer.write("Second line");

} catch (IOException e) { ... }
Copied!

Watch out for Encoding!
By default, Java uses UTF-8 (in modern versions). If you need to read an old Windows file, specify it: Files.newBufferedReader(path, Charset.forName("windows-1252"));

Working with binaries (bytes)

If you try to read an image using a text Reader, you will break it. The Reader will try to interpret the bytes of the photo as letters, change things it doesn’t understand, and the resulting file will be corrupted garbage.

For Raw Data, we use InputStream and OutputStream. There are no letters here, just numbers from 0 to 255.

Example: Copying an image (Byte by Byte)

import java.io.*;

File source = new File("photo.jpg");
File destination = new File("copy_photo.jpg");

try (
    FileInputStream in = new FileInputStream(source);
    FileOutputStream out = new FileOutputStream(destination)
) {
    // Manual buffer of 1KB (we read 1024 bytes at a time)
    byte[] buffer = new byte[1024];
    int bytesRead;

    // in.read(buffer) fills the array and returns how many bytes were read
    // Returns -1 when the file ends
    while ((bytesRead = in.read(buffer)) != -1) {
        // Write to the destination ONLY the bytes that were read
        out.write(buffer, 0, bytesRead);
    }

} catch (IOException e) { ... }
Copied!

This is the basic mechanism behind how internet downloads or file copies work in the operating system.

The bridge: InputStreamReader

Sometimes you have a byte stream (like keyboard input System.in or a network connection Socket) and you want to read it as text.

You need an adapter that converts Bytes -> Characters. That is the InputStreamReader.

// System.in is an InputStream (Bytes)
InputStream byteStream = System.in;

// Convert it to a Reader (Characters)
InputStreamReader converter = new InputStreamReader(byteStream);

// Wrap it in a Buffer to read lines comfortably
BufferedReader keyboardReader = new BufferedReader(converter);

System.out.print("Type something: ");
String text = keyboardReader.readLine();
Copied!

This is why in older Java courses you would see that monstrous line:
new BufferedReader(new InputStreamReader(System.in))
(For simple console input, we usually use Scanner, which offers a more convenient API and allows converting tokens to different types).

Scanner vs. BufferedReader

This is a common question. Both read text.

  • Scanner: It’s powerful for parsing text. It can find integers (nextInt), booleans, or use regular expressions. It’s a bit slower because it does a lot of parsing.
  • BufferedReader: It’s simple and pure. It only reads Strings or complete lines. It is very fast.

Verdict:

  • If you are reading console commands or a complex CSV: Scanner.
  • If you are reading a giant text file line by line: BufferedReader.