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.

published: reading time: 19 min read author: Geek Workbench

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

TaskRecommended API
Simple file read/writeFiles.readString(), Files.writeString() (Java 11+)
Reading lines from a fileFiles.readAllLines(), BufferedReader.lines()
Buffered binary read/writeBufferedInputStream / BufferedOutputStream
Buffered character read/writeBufferedReader / BufferedWriter
Walking a directory treeFiles.walk()
Path manipulationjava.nio.file.Path
Stream-based processingjava.nio.file.Files.lines()
File metadata and attributesFiles.readAttributes(), Files.getLastModifiedTime()

When NOT to Use

  • New code requiring cross-platform file locking: Use FileChannel with explicit locking rather than File.canWrite() / canRead() checks that are not atomic.
  • High-throughput binary files: Use java.nio.ByteBuffer and FileChannel for zero-copy I/O instead of stream wrappers.
  • Unbounded file reading: Never use readAllBytes() on potentially large files (GB scale) — use Files.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

ScenarioProblemSolution
Files.readString() on large fileOutOfMemoryErrorStream via Files.lines() with limit or paginated processing
Race condition on file existence checkTOCTOU vulnerabilityUse Files.createFile() with StandardOpenOption.CREATE_NEW to atomically check-and-create
Symbolic link loops in walk()Infinite loop if symlinks form cyclesUse Files.walk(file, Path::isSymbolicLink, LinkOption.NOFOLLOW_LINKS) or set max depth
Files.move() across filesystem boundariesAtomicMoveNotSupportedExceptionCopy then delete, or use REPLACE_EXISTING with copy
Encoding issues with FileReaderUses platform default encodingAlways specify StandardCharsets.UTF_8 explicitly

Trade-off Table

Aspectjava.iojava.nio.file
API designStream-oriented (byte/char)Buffer-oriented with Path abstraction
PerformanceGood for small filesBetter for large files with ByteBuffer
Directory operationsManual recursionFiles.walk() built-in
Symbolic linksLimited supportFull support via Path and LinkOption
Non-blocking IONot supportedSupported via AsynchronousFileChannel
Modern code preferenceLegacy interopNew 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 IOException with 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/passwd can escape the intended directory if you concatenate strings instead of using path.resolve(). Always use path.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_LINKS unless you explicitly want to follow symlinks.
  • Symbolic link loops: Files.walk() following symlinks can enter infinite loops if directory structures have cycles. Use the LinkOption.NOFOLLOW_LINKS option or limit depth.
  • Temporary file creation: Always use Files.createTempFile() with proper permissions — avoid race conditions in temp file naming.

Pitfalls

  1. Path.of() vs Paths.get(): Path.of() (Java 11+) is the preferred static factory; Paths.get() is the older form. Both work, but prefer Path.of() for consistency.
  2. Files.readAllBytes() on huge files: This loads the entire file into memory — for files that could be multi-GB, use Files.lines() with a stream or BufferedReader line-by-line.
  3. FileReader encoding: new FileReader(file) uses the platform default encoding, which varies by OS. Always wrap with new InputStreamReader(Files.newInputStream(file), StandardCharsets.UTF_8).
  4. Files.walk() holds resources: The stream returned by Files.walk() must be used within a try-with-resources block, as it holds a directory handle open until the stream is closed.
  5. transferTo() in BufferedReader: The transferTo() method on BufferedReader (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_NEW for atomic check-and-create operations.

Interview Questions

1. What is the difference between `Files.walk()` and `Files.walkFileTree()` for directory traversal?

Model Answer: "`Files.walk()` returns a `Stream` for lazy directory traversal and is the preferred high-level API for most use cases — it integrates naturally with the Stream API for filtering and collecting. `Files.walkFileTree()` visits each file with a `FileVisitor` callback interface (`preVisitDirectory`, `postVisitDirectory`, `visitFile`, `visitFileFailed`) giving you precise control at each stage. Use `walk()` for simple traversals; use `walkFileTree()` when you need custom logic at each directory entry, like tracking depth or handling errors differently per file type."

2. How does `Files.createFile()` differ from `Files.write()` for creating a new file?

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."

3. What is the purpose of `LinkOption.NOFOLLOW_LINKS` and when should you use it?

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."

4. What is the behavior of `Files.lines()` regarding exception handling for IO errors?

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."

5. What is the difference between `Path.toAbsolutePath()` and `Path.normalize()`?

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."

6. How does `Files.move()` behave when the destination is on a different filesystem?

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."

7. What is the purpose of `FileTime` and what does `FileTime.from()` accept as input?

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."

8. What is the difference between `DirectoryStream` and `Stream` returned by `Files.walk()`?

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` that recursively traverses an entire directory tree. Use `DirectoryStream` for targeted single-level traversal with glob filtering; use `walk()` for recursive full-tree operations where lazy evaluation and stream chaining are beneficial."

9. What is the security concern with `Files.walk()` and how do you mitigate it?

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."

10. Why is `FileReader` dangerous for production code despite being convenient?

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."

11. What is the purpose of `StandardOpenOption.CREATE_NEW` and how does it differ from `CREATE`?

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."

12. What happens when `Files.size()` is called on a directory versus a file?

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."

13. What is the difference between `Files.readString()` (Java 11+) and `Files.readAllLines()`?

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` — one element per line. Both load the entire file into memory, making them unsuitable for large files. For large files, use `Files.lines()` which returns a lazy `Stream` processing one line at a time. The choice depends on whether you need line structure or the full text as a string."

14. How does `Files.copy()` with `REPLACE_EXISTING` behave when the destination is read-only?

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."

15. What is the purpose of `Files.createTempFile()` versus creating temp files manually?

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."

16. What is the difference between `Path.toUri()` and `Path.toAbsolutePath()`?

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."

17. What does `Files.probeContentType()` return when the content type cannot be determined?

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()`."

18. What is the time complexity of `Files.size()` for a file on a filesystem?

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."

19. How does `Files.writeString()` (Java 11+) handle an existing file versus `Files.createFile()`?

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()`."

20. What is the difference between `InputStreamReader` and `OutputStreamWriter` versus their `FileReader`/`FileWriter` counterparts?

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 — avoid java.io for new code
  • Files.readString() / Files.writeString() (Java 11+) for simple text files
  • Files.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 and NOFOLLOW_LINKS to prevent infinite loops and symlink attacks

Further Reading

Category

Related Posts

Abstract Classes in Java

Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.

#java-abstract-classes #java #java-fundamentals

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals