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.
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
The java.nio.file package (NIO.2, Java 7) handles file and directory operations. It fixes the problems with java.io.File, which jammed file, directory, and path concepts together and had no support for symbolic links or fine-grained attribute access. NIO.2 splits these up: Path is a file or directory location, Files has static utility methods for file operations, and FileSystem wraps the underlying filesystem.
The Files utility class is where most operations start. It gives you readString(Path), writeString(Path, CharSequence, OpenOption...), readAllLines(Path), copy(Path, Path, CopyOption...), move(Path, Path, MoveOption...), and more. All of these take Path objects instead of strings, which makes path construction composable and correct. StandardOpenOption controls file creation, truncation, and append behavior. Need atomic check-and-create, like making a lock file that must not already exist? Use StandardOpenOption.CREATE_NEW.
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
Path is an immutable file or directory location. Unlike a string, Path knows the filesystem’s path separator, handles normalization of . and .. segments, and provides compositional methods for building paths from parts. Path.of(String) (Java 11+) or the older Paths.get(String) creates a Path from a string. One rule: never concatenate strings to build paths. base + "/" + name breaks on Windows (which uses \) and misses edge cases like double slashes. Use base.resolve(name).
resolve() adds a path segment to an existing path. relativize() finds the relative path between two absolute paths. normalize() removes redundant . and .. segments, giving you a cleaner path. subpath(int begin, int end) pulls out a portion of the path for tree-walking or display. toAbsolutePath() converts a relative path to absolute by resolving it against the current working directory. These methods cover path construction and decomposition without string 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
Files.walk(Path) returns a lazy Stream<Path> that traverses a directory tree depth-first. This replaces the manual recursive traversal code you had to write before NIO.2. The stream is lazy, so it only reads directory entries as you consume them, making it memory-efficient for large trees. Use it in a try-with-resources block because it holds a directory handle open until the stream closes or fully consumes.
By default, Files.walk() follows symbolic links. This can cause infinite loops if your directory structure has cycles (a symlink pointing to an ancestor). Pass LinkOption.NOFOLLOW_LINKS to prevent this. You can also limit depth by passing an integer: Files.walk(root, maxDepth). For targeted single-level directory listing with glob patterns, Files.newDirectoryStream(Path, String) returns a DirectoryStream<Path> that is more efficient than walking when you only need one level.
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)
BufferedReader and BufferedWriter wrap character streams with buffering, which speeds up character-by-character or line-by-line processing. Without buffering, each read() or write() call translates to a native IO operation, which is expensive. A BufferedReader reads ahead into an in-memory buffer, so subsequent read() calls pull from the buffer without hitting the OS. BufferedWriter buffers writes before flushing them to the underlying Writer in larger chunks.
BufferedReader.lines() (Java 8+) returns a Stream<String> of all lines in the file, lazy and one line at a time, without loading the entire file into memory. This is the right approach for processing large files line by line. The transferTo() method (Java 10+) on BufferedReader transfers the entire stream content directly to a Writer without intermediate buffering at the Java level, which is efficient for copying. Always specify the charset explicitly when constructing character streams. Wrapping FileReader directly uses the platform default encoding, which varies across systems and JVM configurations.
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
Files.readAttributes(Path, Class<A>) reads filesystem-agnostic basic file attributes: size, creation time, last modified time, and file type (regular file, directory, symbolic link). The BasicFileAttributes interface gives you size() for file size in bytes, creationTime() returning a FileTime, lastModifiedTime() for the modification timestamp, and isRegularFile() / isDirectory() / isSymbolicLink() type checks. These attributes work across all major filesystems and are the correct choice for cross-platform attribute access.
On Unix-like systems (Linux, macOS), Files.readAttributes(path, PosixFileAttributes.class) provides POSIX attributes: owner, group, and permission bits, the same information you see from ls -l. On Windows, use DosFileAttributes. FileTime values from attribute reads convert to java.time.Instant via fileTime.toInstant() for interoperability with the java.time API. To set file times, use Files.setLastModifiedTime(path, FileTime.fromMillis(millis)). Files.getFileStore(path) tells you which physical or logical volume a file lives on, useful for detecting cross-filesystem moves.
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
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
- Text Formatting — text handling and formatting companion topic
- Java Collections Utility — utility methods for collections that overlap with file processing patterns
Conclusion
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
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.