java.io and java.nio.file
Read, write, and navigate files with java.io and java.nio.file: Path, Files, BufferedReader, BufferedWriter, and directory walking.
Introduction
Java provides two main packages for file and stream IO: java.io (original, stream-oriented) and java.nio.file (added in Java 7, buffer-oriented with better performance and richer semantics). The java.nio.file package — often called NIO.2 — is the modern choice for most file operations, introducing the Path abstraction, the Files utility class with comprehensive static methods, and Files.walk() for directory tree traversal. The legacy java.io package remains relevant for character stream processing and interoperability with older libraries, but new code should use NIO unless there is a specific reason not to.
The core tension in file IO is between simplicity and scalability. Reading a small text file into a String with Files.readString() is one line, but calling the same method on a multi-gigabyte file exhausts memory and crashes the process. Writing to a file seems straightforward until you encounter path traversal attacks (user input containing ../etc/passwd), symbolic link loops that cause infinite traversal, or file locking across networked filesystems. Production file operations require understanding the failure modes before they happen, not after.
This guide covers the architecture of both packages, the modern NIO.2 API for common file operations, path manipulation and the Path abstraction, directory walking with Files.walk(), and the failure scenarios that cause production incidents. Security considerations around path traversal and symbolic link attacks are covered in detail because file operations are a common attack surface for untrusted input.
When to Use
| Task | Recommended API |
|---|---|
| Simple file read/write | Files.readString(), Files.writeString() (Java 11+) |
| Reading lines from a file | Files.readAllLines(), BufferedReader.lines() |
| Buffered binary read/write | BufferedInputStream / BufferedOutputStream |
| Buffered character read/write | BufferedReader / BufferedWriter |
| Walking a directory tree | Files.walk() |
| Path manipulation | java.nio.file.Path |
| Stream-based processing | java.nio.file.Files.lines() |
| File metadata and attributes | Files.readAttributes(), Files.getLastModifiedTime() |
When NOT to Use
- New code requiring cross-platform file locking: Use
FileChannelwith explicit locking rather thanFile.canWrite()/canRead()checks that are not atomic. - High-throughput binary files: Use
java.nio.ByteBufferandFileChannelfor zero-copy I/O instead of stream wrappers. - Unbounded file reading: Never use
readAllBytes()on potentially large files (GB scale) — useFiles.lines()with try-with-resources and a stream approach. - Path string construction: Avoid string concatenation for paths — use
Path.resolve()instead to handle edge cases like double slashes.
Architecture
flowchart TD
subgraph java.io
A1[InputStream / OutputStream]
A2[Reader / Writer]
A3[FileInputStream / FileOutputStream]
A4[BufferedReader / BufferedWriter]
end
subgraph java.nio.file
B1[Path]
B2[Files]
B3[FileSystem]
B4[DirectoryStream]
end
subgraph java.nio.channels
C1[ByteChannel]
C2[FileChannel]
C3[SeekableByteChannel]
end
B1 --> B2
B2 --> C2
B2 --> B4
style java.nio.file fill:#1a1a2e,stroke:#00fff9,color:#00fff9
style java.io fill:#0d0d1a,stroke:#ff00ff,color:#fff
Code Examples
java.nio.file — Modern File Operations
import java.nio.file.*;
import java.io.IOException;
Path path = Path.of("/tmp/data.txt");
// Read entire file (Java 11+)
String content = Files.readString(path);
// Write file (Java 11+)
Files.writeString(path, "Hello, World!", StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
// Read all lines
List<String> lines = Files.readAllLines(path);
// Write lines
Files.write(path, List.of("Line 1", "Line 2"));
// Check existence and permissions
if (Files.exists(path) && Files.isReadable(path)) {
// ...
}
// Copy and move
Files.copy(Path.of("/tmp/source.txt"), Path.of("/tmp/dest.txt"), StandardCopyOption.REPLACE_EXISTING);
Files.move(Path.of("/tmp/src"), Path.of("/tmp/dst"), StandardCopyOption.ATOMIC_MOVE);
// Temp files
Path tempFile = Files.createTempFile("prefix", ".txt");
Path tempDir = Files.createTempDirectory("prefix");
Path Manipulation
import java.nio.file.Path;
Path base = Path.of("/home/user/projects");
Path file = Path.of("/home/user/projects/src/main/java/App.java");
// Resolve — append path segments
Path joined = base.resolve("src/config.yaml"); // /home/user/projects/src/config.yaml
// Relativize — find relative path between two absolutes
Path relative = base.relativize(file); // src/main/java/App.java
// Normalize — resolve . and ..
Path messy = Path.of("/home/user/../user/./projects/./app");
Path normalized = messy.normalize(); // /home/user/projects/app
// Subpath
Path sub = file.subpath(1, 3); // projects/src
// Get parts
file.getFileName(); // App.java
file.getParent(); // /home/user/projects/src/main/java
file.getRoot(); // /
Directory Walking
import java.nio.file.*;
import java.util.stream.*;
import java.io.IOException;
// Walk entire tree
try (Stream<Path> stream = Files.walk(Path.of("/tmp/myproject"))) {
stream.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
// Walk with max depth
try (Stream<Path> stream = Files.walk(Path.of("/tmp"), 2)) {
// Only visits depth 0, 1, 2
}
// Find files with specific glob
try (DirectoryStream<Path> ds = Files.newDirectoryStream(Path.of("/tmp"), "*.txt")) {
for (Path p : ds) {
System.out.println(p);
}
}
// List directory contents
try (DirectoryStream<Path> ds = Files.newDirectoryStream(Path.of("/tmp"))) {
ds.forEach(System.out::println);
}
Buffered Character Streams (java.io)
import java.io.*;
import java.nio.file.Path;
Path logFile = Path.of("/tmp/app.log");
// BufferedReader — reading lines
try (BufferedReader reader = new BufferedReader(new FileReader(logFile.toFile()))) {
reader.lines()
.filter(line -> line.contains("ERROR"))
.forEach(System.out::println);
}
// BufferedWriter — writing
try (BufferedWriter writer = new BufferedWriter(new FileWriter(logFile.toFile()))) {
writer.write("Application started at " + java.time.LocalDateTime.now());
writer.newLine();
writer.flush();
}
// Try-with-resources for multi-stream handling
try (BufferedReader reader = new BufferedReader(new FileReader("/tmp/in.txt"));
BufferedWriter writer = new BufferedWriter(new FileWriter("/tmp/out.txt"))) {
reader.transferTo(writer); // Java 9+
}
File Attributes
import java.nio.file.*;
import java.io.IOException;
import java.nio.file.attribute.*;
Path file = Path.of("/tmp/data.txt");
// Basic attributes
BasicFileAttributes attrs = Files.readAttributes(file, BasicFileAttributes.class);
attrs.size(); // file size in bytes
attrs.creationTime(); // creation time
attrs.lastModifiedTime(); // last modified time
attrs.isRegularFile(); // true
attrs.isDirectory(); // true/false
attrs.isSymbolicLink(); // true/false
// POSIX attributes (Unix)
PosixFileAttributes posixAttrs = Files.readAttributes(file, PosixFileAttributes.class);
posixAttrs.owner(); // OwnerPrincipal
posixAttrs.group(); // GroupPrincipal
posixAttrs.permissions(); // POSIX permissions set
// Set last modified time
Files.setLastModifiedTime(file, FileTime.fromMillis(System.currentTimeMillis()));
Failure Scenarios
| Scenario | Problem | Solution |
|---|---|---|
Files.readString() on large file | OutOfMemoryError | Stream via Files.lines() with limit or paginated processing |
| Race condition on file existence check | TOCTOU vulnerability | Use Files.createFile() with StandardOpenOption.CREATE_NEW to atomically check-and-create |
Symbolic link loops in walk() | Infinite loop if symlinks form cycles | Use Files.walk(file, Path::isSymbolicLink, LinkOption.NOFOLLOW_LINKS) or set max depth |
Files.move() across filesystem boundaries | AtomicMoveNotSupportedException | Copy then delete, or use REPLACE_EXISTING with copy |
| Encoding issues with FileReader | Uses platform default encoding | Always specify StandardCharsets.UTF_8 explicitly |
Trade-off Table
| Aspect | java.io | java.nio.file |
|---|---|---|
| API design | Stream-oriented (byte/char) | Buffer-oriented with Path abstraction |
| Performance | Good for small files | Better for large files with ByteBuffer |
| Directory operations | Manual recursion | Files.walk() built-in |
| Symbolic links | Limited support | Full support via Path and LinkOption |
| Non-blocking IO | Not supported | Supported via AsynchronousFileChannel |
| Modern code preference | Legacy interop | New code |
Observability Checklist
// Instrument file operations
import java.nio.file.*;
import java.time.Instant;
public class InstrumentedFile {
public static String readFile(Path path) throws IOException {
long start = System.nanoTime();
try {
String content = Files.readString(path);
System.out.println("metric=file_read path=" + path +
" size=" + content.length() +
" duration_ns=" + (System.nanoTime() - start));
return content;
} catch (IOException e) {
System.out.println("metric=file_read path=" + path + " error=true");
throw e;
}
}
}
- Track file read/write latency as structured metrics.
- Monitor file sizes at ingestion time to detect anomalous payloads.
- Log
IOExceptionwith the failing path and root cause (but not stack traces to untrusted callers). - Use
Files.walk()with max depth to prevent accidental traversal into enormous directory trees. - Instrument directory walk completion with number of files visited and total size.
Security Notes
- Path traversal attacks: User-supplied paths like
../../etc/passwdcan escape the intended directory if you concatenate strings instead of usingpath.resolve(). Always usepath.resolve(userInput)and validate the resolved path starts with the expected base directory. - Symbolic link attacks: A malicious symlink in a directory you walk can cause reads/writes to unintended files. Set
LinkOption.NOFOLLOW_LINKSunless you explicitly want to follow symlinks. - Symbolic link loops:
Files.walk()following symlinks can enter infinite loops if directory structures have cycles. Use theLinkOption.NOFOLLOW_LINKSoption or limit depth. - Temporary file creation: Always use
Files.createTempFile()with proper permissions — avoid race conditions in temp file naming.
Pitfalls
Path.of()vsPaths.get():Path.of()(Java 11+) is the preferred static factory;Paths.get()is the older form. Both work, but preferPath.of()for consistency.Files.readAllBytes()on huge files: This loads the entire file into memory — for files that could be multi-GB, useFiles.lines()with a stream orBufferedReaderline-by-line.- FileReader encoding:
new FileReader(file)uses the platform default encoding, which varies by OS. Always wrap withnew InputStreamReader(Files.newInputStream(file), StandardCharsets.UTF_8). Files.walk()holds resources: The stream returned byFiles.walk()must be used within a try-with-resources block, as it holds a directory handle open until the stream is closed.transferTo()in BufferedReader: ThetransferTo()method onBufferedReader(Java 10+) transfers directly without explicit encoding handling — ensure the underlying streams use compatible encodings.
Quick Recap
- Use
java.nio.file(Files,Path) for all new file operations. Files.readString()/Files.writeString()(Java 11+) for simple text file operations.Files.walk()for directory tree traversal — always use in try-with-resources.Path.resolve()to build paths safely; never concatenate strings.Path.normalize()to resolve..and.in paths.- Use
BufferedReader.lines()for line-by-line stream processing of large files. - Specify charset explicitly when wrapping character streams.
- Use
StandardOpenOption.CREATE_NEWfor atomic check-and-create operations.
Interview Questions
Model Answer: "`Files.walk()` returns a `Stream
Model Answer: "`Files.createFile(path)` creates an empty file and throws `FileAlreadyExistsException` if it exists — it is purely a creation operation. `Files.write(path, content)` creates the file and writes content in one call, using `StandardOpenOption.CREATE` which also creates if absent but does not fail if the file exists. For pure creation without immediate content writing, `createFile()` is clearer. For create-and-write-in-one-step, `write()` is appropriate. Both are atomic when used with `CREATE_NEW` option."
Model Answer: "This option prevents symbolic links from being followed during file operations. By default, `Files.walk()`, `Files.copy()`, and `Files.isRegularFile()` follow symlinks — this can cause security issues (symlink attacks) and infinite loops if directory structures have cycles. Pass `LinkOption.NOFOLLOW_LINKS` to any operation that should treat symlinks as regular files rather than resolving them. Use it when traversing directories you do not control or when you want to detect rather than follow symlinks."
Model Answer: "`Files.lines()` throws `UncheckedIOException` wrapping an `IOException` if one occurs during line reading — it does not expose checked IO exceptions through the stream pipeline directly. This means you must handle it with a try-catch around the stream creation or usage. Unlike `Files.readAllLines()` which propagates `IOException` explicitly, `lines()` converts it to an unchecked exception so stream pipelines do not need explicit exception declarations. The stream must be used in try-with-resources to ensure the file handle is closed."
Model Answer: "`toAbsolutePath()` converts a possibly-relative path to an absolute one by resolving it against the current working directory — if the path is already absolute, it returns it unchanged. `normalize()` removes redundant `.` and `..` segments from an already-absolute path to produce a syntactically cleaner path, but it does not make the path absolute. They are often chained: `path.normalize().toAbsolutePath()` first resolves `.` and `..`, then makes the result absolute."
Model Answer: "By default, `Files.move()` attempts an atomic rename which only works within the same filesystem — if source and destination are on different filesystems, it throws `AtomicMoveNotSupportedException`. The workaround uses `StandardCopyOption.REPLACE_EXISTING` without `ATOMIC_MOVE` — this falls back to copy-then-delete, which works across filesystems but is not atomic and does not preserve all attributes (permissions may be lost). If atomicity is critical across filesystems, use a library that implements distributed copy or implement explicit versioning."
Model Answer: "`FileTime` represents a timestamp from a filesystem (creation, modified, last-accessed) independent of the filesystem type. `FileTime.from()` accepts a `Long` epoch milliseconds, a `long` epoch seconds, or an `Instant`. For setting file times, use `FileTime.fromMillis(millis)` or `FileTime.from(instant)`. When reading attributes via `Files.readAttributes()`, file times are returned as `FileTime` objects. Convert to `Instant` with `fileTime.toInstant()` for java.time interoperability."
Model Answer: "`DirectoryStream` is a low-level iterator-based API returned by `Files.newDirectoryStream()` — it returns a single directory's contents matching a glob pattern and must be closed in try-with-resources. `Files.walk()` is a higher-level lazy `Stream
Model Answer: "`Files.walk()` following symlinks can enter infinite loops if directory structures contain cycles (a symlink points to an ancestor), causing `DirectoryIteratorException` or `StackOverflowError`. It can also escape intended boundaries via path traversal symlinks. Mitigation: always use `walk()` with `LinkOption.NOFOLLOW_LINKS` unless symlink following is explicitly intended; set `maxDepth` to limit traversal depth; validate that the starting path is within an approved directory after resolving."
Model Answer: "`FileReader` uses the platform default charset — on a US system this is UTF-8, on a German system it might be ISO-8859-1. This means the same code produces different results across JVM configurations, causing encoding bugs when reading files created on different systems. Always wrap `Files.newInputStream(path)` with `new InputStreamReader(stream, StandardCharsets.UTF_8)` for predictable encoding. `FileReader` does not accept a charset parameter, making it unsuitable for any code that needs consistent behavior across environments."
Model Answer: "`CREATE_NEW` creates the file only if it does not exist and fails with `FileAlreadyExistsException` if it does — it atomically checks-and-creates, making it safe for lock-file patterns. `CREATE` (or just `WRITE`) creates the file if it does not exist but opens it if it does exist, allowing accidental data destruction. Use `CREATE_NEW` when you need exclusive file creation; use `CREATE` when create-or-open is acceptable."
Model Answer: "For a directory, `Files.size()` returns an implementation-defined value — typically the sum of all entry sizes or sometimes a fixed value like 0 or 4096, depending on the filesystem. It does not recursively sum the contents of the directory. This is because directories are filesystem metadata structures, not data files, and the interpretation varies across filesystems. If you need the total size of a directory's contents, use `Files.walk()` to traverse and sum the sizes of all regular files."
Model Answer: "`Files.readString()` (Java 11+) reads an entire file as a single `String` — appropriate for small text files where the whole content is needed at once. `Files.readAllLines()` reads all lines into a `List
Model Answer: "On most filesystems, `REPLACE_EXISTING` can overwrite a read-only file if the process has permission to change file permissions — it strips the read-only attribute during the copy operation. On Unix-like systems with strict permission models, you may need to explicitly `setPosixFilePermissions()` or change ownership first. Always check `Files.isWritable()` before copy if the target's writability status matters, and handle `AccessDeniedException` explicitly."
Model Answer: "`Files.createTempFile()` generates a cryptographically secure random filename (unlike manual naming which could collide), uses the OS-designated temp directory with proper permissions, and registers the file with the JVM's temp file cleanup tracking in some configurations. Manual temp file creation risks naming collisions, race conditions, and accidentally placing files in wrong directories. Always use `createTempFile()` or `createTempDirectory()` for security and correctness."
Model Answer: "`toAbsolutePath()` returns a fully resolved `Path` using the local filesystem — the result is usable for file operations on the local machine. `toUri()` returns a `URI` representation (`file:///home/user/file.txt`) — a portable identifier for the resource. Use `toAbsolutePath()` when you need to open, read, or manipulate the file; use `toUri()` when you need a portable reference for sharing, serialization, or embedding in data formats that require URI identifiers."
Model Answer: "It returns `null` when the content type cannot be determined from the file or the filesystem's type-detection mechanism. The method relies on the OS's file type association — if the OS has no registered handler for the file extension (like `.xyz`), it returns null. Always check for null return value. For web serving or MIME-type-sensitive operations, use `URLConnection.guessContentTypeFromName()` or an explicit MIME type registry rather than relying on `probeContentType()`."
Model Answer: "`Files.size()` is O(1) — it reads the file size directly from the inode or directory entry metadata stored by the filesystem, requiring no traversal of file contents. This is because filesystems store file size as a direct attribute in the file's metadata structure. However, for directories, 'size' is filesystem-defined and may not reflect actual content bytes."
Model Answer: "By default, `Files.writeString()` opens an existing file for writing and truncates it to zero length before writing — effectively overwriting the content. To fail on existing files, add `StandardOpenOption.CREATE_NEW` which throws `FileAlreadyExistsException` if the file exists, making it safe for exclusive creation. This is the same atomic check-create pattern as `Files.createFile()`."
Model Answer: "`InputStreamReader`/`OutputStreamWriter` wrap an `InputStream`/`OutputStream` and accept an explicit `Charset` or `CharsetEncoder`, making encoding behavior explicit and correct. `FileReader`/`FileWriter` (in `java.io`) use the platform default encoding — variable across systems and JVM configurations. Always prefer the stream variants with explicit charset specification for production code. The stream variants also work with any stream source, not just files, making them more flexible."
Java’s file IO story improved significantly in Java 7 with java.nio.file, which introduced the Path abstraction, rich file utility methods, and directory walking capabilities. For most new code, java.nio.file is the right choice — Files.readString() and Files.writeString() (Java 11+) handle simple text file operations cleanly, while Files.walk() replaces manual recursive directory traversal with a lazy stream-based approach.
The Path abstraction solves the problems with using bare strings for file paths. Path.resolve() properly handles path segment joining (avoiding double-slash bugs and other string concatenation edge cases), Path.normalize() resolves .. and . segments, and Path.relativize() computes relative paths between two absolute paths. Always use Path methods when constructing paths from user input or multiple segments — string concatenation is a path traversal vulnerability waiting to happen.
Files.walk() is powerful but requires discipline. The stream it returns holds a directory handle open until the stream is closed, so it must be used inside try-with-resources. By default it follows symbolic links, which can cause infinite loops if directory structures have cycles — use LinkOption.NOFOLLOW_LINKS or limit max depth to prevent this. For very large directory trees, always set a max depth to avoid exhausting file descriptors.
For large file processing, streaming is non-negotiable. Files.readAllBytes() and Files.readAllLines() load entire files into memory — fine for small config files, catastrophic for multi-GB datasets. Use Files.lines() which returns a Stream<String> processed lazily, or BufferedReader.lines() for line-by-line processing without loading the whole file.
File operations integrate with java.util.Collections when you need to collect file listing results, and java.util.stream.Stream for processing file contents as a stream of lines.
- Use
java.nio.file(Files,Path) for all new file operations — avoidjava.iofor new code Files.readString()/Files.writeString()(Java 11+) for simple text filesFiles.walk()must be used in try-with-resources — it holds an open directory handle- Use
Path.resolve()for path construction — never concatenate strings for paths - For large files, use streaming approaches (
Files.lines(),BufferedReader) — never load entire file into memory Files.walk()with max depth andNOFOLLOW_LINKSto prevent infinite loops and symlink attacks
Further Reading
- Oracle: File I/O (featuring NIO.2) — official Java tutorial on NIO.2 file operations
- Baeldung: Java Files API Guide — practical coverage of
java.nio.fileutilities - NIO.2 Path API vs legacy File API — migration patterns from
java.io.Filetojava.nio.file.Path - Symbolic link security in Java — general security guidance on symlink attacks applicable to
Files.walk() - Java 11 Files.writeString source code — read the actual implementation to understand the guarantees and edge cases
Category
Related Posts
Abstract Classes in Java
Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.