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,
Stringline handling, and text blocks for fixtures. -
Exceptions —
IOException,UncheckedIOException, and how try-with-resources suppresses close failures. -
Streams and Collectors — consuming
Files.linesandFiles.walkresults. -
Records and Sealed Classes — the safe, explicit alternative to
Serializablefor data you persist or send.