I/O and Files

This section documents the current Java release line, with Java 25 LTS as the reference point (no specific patch version is pinned), as published at the Java developer portal, the Java Tutorials, and the Java SE API specification — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Java has two I/O layers: the original stream classes in java.io, and NIO.2 in java.nio.file, which is the modern default for the filesystem. This page covers streams and the decorator pattern, then the Path/Files API for everyday file work, then resource handling and a few sharp edges. References: dev.java: The Java I/O API, Common I/O Tasks in Modern Java, the Java Tutorials Basic I/O trail, and java.nio.file.Files.

Byte Streams vs. Character Streams

A byte stream moves raw 8-bit data: InputStream and OutputStream (and subclasses like FileInputStream). A character stream moves text, decoding/encoding bytes through a charset: Reader and Writer. Use byte streams for images, archives, and protocols; character streams for anything you would open in a text editor. See Byte Streams and Character Streams.

The decorator pattern

Stream classes wrap other streams, each adding one capability — buffering, character decoding, data typing. You compose the pipeline you need. See Buffered Streams.

import java.io.*;
import java.nio.charset.StandardCharsets;

// raw bytes -> decoded chars (InputStreamReader) -> buffered line reads (BufferedReader)
try (var in = new BufferedReader(
        new InputStreamReader(
            new FileInputStream("notes.txt"), StandardCharsets.UTF_8))) {

    String line;
    while ((line = in.readLine()) != null) {
        System.out.println(line);
    }
}

// chars -> encoded bytes -> buffered writes
try (var out = new BufferedWriter(
        new OutputStreamWriter(
            new FileOutputStream("out.txt"), StandardCharsets.UTF_8))) {
    out.write("first line");
    out.newLine();
}

InputStreamReader is the bridge from bytes to characters; BufferedReader turns many tiny reads into a few large ones and adds readLine(). Always name the charset explicitly.

System.in / out / err and Scanner

System exposes in (an InputStream), out and err (both PrintStream). For parsed console input, java.util.Scanner tokenises a stream; see Scanning.

import java.util.Scanner;

try (var sc = new Scanner(System.in)) {
    System.out.print("Name: ");
    String name = sc.nextLine();
    System.out.print("Age: ");
    int age = sc.nextInt();
    System.out.printf("%s is %d%n", name, age);
}

On Java 25 you can also read a line with the concise java.io.Console / System.console().readLine(…​), or java.util.Scanner as above. Reserve System.err for diagnostics so it stays separate from real program output.

NIO.2: java.nio.file

A Path is an abstract, possibly-non-existent location. Build one with Path.of(…​) (preferred) or Paths.get(…​). See The Path Class.

import java.nio.file.*;

Path p       = Path.of("data", "reports", "q1.csv");   // data/reports/q1.csv
Path abs     = p.toAbsolutePath();
Path parent  = p.getParent();                          // data/reports
Path name    = p.getFileName();                        // q1.csv
Path sibling = p.resolveSibling("q2.csv");             // data/reports/q2.csv
Path norm    = Path.of("a/./b/../c").normalize();      // a/c

Reading and writing whole files

Files has static one-call helpers. readString/writeString and readAllLines load the whole file into memory; lines and newBufferedReader stream it lazily for large files.

import java.nio.file.*;
import java.util.List;
import java.util.stream.Stream;

Path file = Path.of("notes.txt");

String text          = Files.readString(file);              // UTF-8 by default
List<String> lines   = Files.readAllLines(file);

Files.writeString(file, "one\ntwo\n");                       // create or truncate
Files.writeString(file, "three\n", StandardOpenOption.APPEND);
Files.write(file, List.of("a", "b", "c"));                  // iterable of lines

// lazy, closeable stream -- must be in try-with-resources
try (Stream<String> stream = Files.lines(file)) {
    long nonBlank = stream.filter(s -> !s.isBlank()).count();
}

try (var reader = Files.newBufferedReader(file)) {
    String first = reader.readLine();
}

Checking, creating, copying, moving, deleting

import java.nio.file.*;
import static java.nio.file.StandardCopyOption.*;

Path dir  = Path.of("build/output");
Path src  = Path.of("template.txt");
Path dst  = dir.resolve("copy.txt");

boolean exists = Files.exists(dir);
boolean isDir  = Files.isDirectory(dir);

Files.createDirectories(dir);            // makes every missing parent; no error if it exists
Files.copy(src, dst, REPLACE_EXISTING);
Files.move(dst, dir.resolve("final.txt"), REPLACE_EXISTING, ATOMIC_MOVE);

Files.deleteIfExists(dir.resolve("final.txt"));   // no exception if already gone
long size = Files.size(src);

Files.delete throws NoSuchFileException when the target is missing; deleteIfExists returns a boolean instead. Neither removes a non-empty directory — walk it first.

Traversing directories

Files.walk returns a lazy, recursive Stream<Path> (close it); Files.newDirectoryStream iterates one directory level with an optional glob. See Walking the File Tree and Listing a Directory’s Contents.

import java.nio.file.*;
import java.util.stream.Stream;

Path root = Path.of("src");

// recursive: every .java file under src/
try (Stream<Path> tree = Files.walk(root)) {
    tree.filter(Files::isRegularFile)
        .filter(pth -> pth.toString().endsWith(".java"))
        .forEach(System.out::println);
}

// one level, filtered by glob
try (DirectoryStream<Path> ds = Files.newDirectoryStream(root, "*.{md,txt}")) {
    for (Path entry : ds) {
        System.out.println(entry.getFileName());
    }
}

// recursive delete: children before parents
try (Stream<Path> tree = Files.walk(root)) {
    tree.sorted(java.util.Comparator.reverseOrder())
        .forEach(pth -> {
            try { Files.delete(pth); }
            catch (java.io.IOException e) { throw new java.io.UncheckedIOException(e); }
        });
}

Resources, Serialization, and jwebserver

try-with-resources for every stream

Every stream, reader, writer, Scanner, and lazy Files stream implements AutoCloseable. A try-with-resources statement closes them in reverse order, even on exception, and even if a close() itself throws (that exception is suppressed, not lost). See The try-with-resources Statement.

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

// multiple resources, semicolon-separated; both closed automatically
try (var in  = Files.newBufferedReader(Path.of("in.txt"));
     var out = Files.newBufferedWriter(Path.of("out.txt"))) {

    String line;
    while ((line = in.readLine()) != null) {
        out.write(line.toUpperCase());
        out.newLine();
    }
}   // out.close() then in.close(), guaranteed

Never write a finally { stream.close(); } by hand — it is verbose and gets suppression wrong.

java.io.Serializable — avoid it for data exchange

Serializable turns an object graph into bytes via ObjectOutputStream. It is fragile (any field change can break old data), opaque (not human-readable), and a well-known security hole: deserializing untrusted bytes can execute attacker-chosen code, because readObject runs before you can validate anything.

// AVOID: reading serialized objects from an untrusted source
try (var oin = new java.io.ObjectInputStream(Files.newInputStream(Path.of("data.ser")))) {
    Object dangerous = oin.readObject();   // may run arbitrary code during construction
}

Prefer an explicit text format — JSON, or a small hand-written encoder over records — where you control parsing and can reject bad input. If you must use Java serialization, apply a deserialization filter (ObjectInputFilter) to allow-list classes.

The bundled web server: jwebserver

The JDK ships a minimal static file server — the jwebserver command and the com.sun.net.httpserver.SimpleFileServer API — for local previewing only (no HTTPS, GET only). Command line:

# serve the current directory on http://127.0.0.1:8000
jwebserver

# choose a directory and port; bind to all interfaces
jwebserver -d /var/www/site -p 9000 -b 0.0.0.0

Or start it from code:

import com.sun.net.httpserver.SimpleFileServer;
import java.net.InetSocketAddress;
import java.nio.file.Path;

var server = SimpleFileServer.createFileServer(
        new InetSocketAddress(8000),
        Path.of("public").toAbsolutePath(),
        SimpleFileServer.OutputLevel.INFO);
server.start();
System.out.println("Serving on http://localhost:8000");

See Also

  • Strings and Text — charsets, String line handling, and text blocks for fixtures.

  • Exceptions — IOException, UncheckedIOException, and how try-with-resources suppresses close failures.

  • Streams and Collectors — consuming Files.lines and Files.walk results.

  • Records and Sealed Classes — the safe, explicit alternative to Serializable for data you persist or send.