PrepZone Logo
PrepZone

Reading and Writing Data

Byte streams versus character streams, why buffering matters, and how to take console input.

Read these first

Why this matters

  • Reading a file one byte at a time instead of through a buffer can be hundreds of times slower for no visible reason in the code.
  • Choosing a byte stream for text, or relying on the platform's default character set, produces corrupted output that only appears on another machine.
  • Forgetting to close a resource leaks file handles, and the modern syntax that prevents it is one line.

Four families

Every classic I/O class in java.io sits in one of four boxes, defined by direction and unit.

Binary data
InputStream / OutputStreamImages, PDFs, audio
BufferedInputStreamReads in blocks
Text data
Reader / WriterHandles character encoding
BufferedReaderGives you readLine()
Pick the family by data type, then wrap a buffer around it. Buffering is what turns thousands of tiny system calls into a handful of big ones.
AspectByte streamsCharacter streams
Unit8-bit bytes16-bit characters
Base classesInputStream, OutputStreamReader, Writer
Use forImages, audio, zip, any binaryText of any kind
Character set awareNoYes — decodes and encodes
Typical pairFileInputStream / FileOutputStreamFileReader / FileWriter
  • Unit

    Byte streams8-bit bytes
    Character streams16-bit characters
  • Base classes

    Byte streamsInputStream, OutputStream
    Character streamsReader, Writer
  • Use for

    Byte streamsImages, audio, zip, any binary
    Character streamsText of any kind
  • Character set aware

    Byte streamsNo
    Character streamsYes — decodes and encodes
  • Typical pair

    Byte streamsFileInputStream / FileOutputStream
    Character streamsFileReader / FileWriter

Using a byte stream for text works for plain ASCII and silently breaks on anything else.

Buffering is not optional

An unbuffered read asks the operating system for data on every single call. Buffering reads a block once and serves your calls from memory.

Java
// Slow: one system call per byte
try (FileInputStream in = new FileInputStream("data.bin")) {
    int byteRead;
    while ((byteRead = in.read()) != -1) {
        process(byteRead);
    }
}

// Fast: reads in blocks, serves from memory
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream("data.bin"))) {
    int byteRead;
    while ((byteRead = in.read()) != -1) {
        process(byteRead);
    }
}

The loop is identical. The only change is wrapping the stream, and that wrapping is the difference between a program that finishes and one that appears to hang.

Always specify the character set

Java
// Depends on the machine's default — the same code gives different results elsewhere
try (BufferedReader reader = new BufferedReader(new FileReader("notes.txt"))) { }

// Explicit and reproducible
try (BufferedReader reader = Files.newBufferedReader(
        Path.of("notes.txt"), StandardCharsets.UTF_8)) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

Before Java 18 the default character set came from the operating system, so a file written on a developer's laptop could be unreadable on a Linux server. Java 18 made UTF-8 the default everywhere, which fixed the common case — but naming the charset explicitly is still the right habit, because it documents intent and works on every version.

try-with-resources

Any object implementing AutoCloseable can be declared in the parentheses of a try, and the JVM closes it when the block exits — on success, on exception, on return, always.

Java
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8);
     BufferedWriter writer = Files.newBufferedWriter(output, StandardCharsets.UTF_8)) {

    String line;
    while ((line = reader.readLine()) != null) {
        writer.write(line.strip());
        writer.newLine();
    }
}

Details worth knowing

  • Multiple resources are separated by semicolons and closed in reverse order of declaration, so a wrapper closes before what it wraps.
  • If the body throws and close() also throws, the body's exception wins and the close failure is attached as a suppressed exception, reachable via getSuppressed().
  • Closing the outermost wrapper closes the whole chain. You never need to close the inner stream yourself.

Prefer java.nio.file for files

The Files and Path classes are clearer than File for almost every task, and they report real errors instead of returning false.

Java
Path path = Path.of("config", "app.properties");

List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
String whole = Files.readString(path, StandardCharsets.UTF_8);

Files.writeString(path, "key=value\n", StandardCharsets.UTF_8);
Files.createDirectories(path.getParent());
boolean exists = Files.exists(path);

// Streaming a large file without loading it into memory
try (Stream<String> stream = Files.lines(path, StandardCharsets.UTF_8)) {
    stream.filter(line -> !line.isBlank()).forEach(System.out::println);
}

Console input

Java
// Simple and convenient; parses as it reads
Scanner scanner = new Scanner(System.in);
System.out.print("Name: ");
String name = scanner.nextLine();
System.out.print("Age: ");
int age = scanner.nextInt();

// Faster for bulk input; you parse yourself
BufferedReader reader = new BufferedReader(
        new InputStreamReader(System.in, StandardCharsets.UTF_8));
int value = Integer.parseInt(reader.readLine().strip());

Scanner is the right default for small interactive programs. For competitive programming or bulk piped input, BufferedReader is substantially faster because Scanner does regular expression work on every token.

Common misreadings

  • "Buffering is a micro-optimisation." It changes the number of system calls by orders of magnitude. It is the difference between usable and unusable.
  • "FileReader handles any encoding." Before Java 18 it used the platform default; it has never let you choose. Use Files.newBufferedReader with an explicit charset.
  • "flush() is unnecessary." Buffered writers hold data until the buffer fills. Closing flushes, but if you never close, the tail of your output is lost.
  • "File.delete() tells you what went wrong." It returns false with no reason. Files.delete throws an exception that names the cause.
  • "One stream per file handle must be closed individually." Closing the outermost wrapper closes everything beneath it.

Quick recall

Everything you need if you only revisit this box.

  • Two axes: bytes vs characters, input vs output. InputStream/OutputStream for binary, Reader/Writer for text.
  • Always wrap in a buffered variant. Unbuffered reads make one system call per byte.
  • Name the character set explicitly; UTF-8 became the default only in Java 18.
  • Use try-with-resources always. Resources close in reverse order, and a failing close() is recorded as suppressed rather than hiding the real exception.
  • Prefer java.nio.file.Files and Path. Use Files.lines for large files and close the stream.
  • Scanner for small interactive input, BufferedReader for bulk — and never mix nextInt() with nextLine() carelessly.

Test yourself

Answer these before moving on — recall is what makes it stick.