Skip to main content

Ch 15.5 NIO and the Files API (Java 7–11+)

Java 7 introduced NIO.2 (New I/O 2) via the java.nio.file package, providing a significantly more powerful and convenient API for file and directory operations. It resolves most of the shortcomings of the legacy java.io.File class.

1. Path — Representing File Paths

Use java.nio.file.Path instead of java.io.File for path manipulation.

import java.nio.file.*;

// Creating paths
Path path1 = Path.of("data/config.txt"); // Java 11+
Path path2 = Paths.get("data", "config.txt"); // Java 7+
Path absolute = Path.of("/home/user/data.txt"); // absolute path

// Path manipulation
Path parent = path1.getParent(); // data
Path fileName = path1.getFileName(); // config.txt
String str = path1.toString(); // data/config.txt
Path resolved = Path.of("data").resolve("config.txt"); // data/config.txt

// Normalize (removes ../ and ./)
Path messy = Path.of("data/../logs/./app.log");
Path cleaned = messy.normalize(); // logs/app.log
Path abs = messy.toAbsolutePath(); // full absolute path

// Relative path between two paths
Path base = Path.of("/home/user");
Path target = Path.of("/home/user/docs/report.pdf");
Path rel = base.relativize(target); // docs/report.pdf

// Path comparisons
System.out.println(Path.of("a/b").startsWith("a")); // true
System.out.println(Path.of("a/b/c").endsWith("b/c")); // true

2. The Files Utility Class

java.nio.file.Files provides all common file operations as static methods.

Reading Files

Path path = Path.of("hello.txt");

// Read entire content as String (Java 11+, suitable for small files)
String content = Files.readString(path);
System.out.println(content);

// Non-UTF-8 encoding
String eucKr = Files.readString(path, java.nio.charset.Charset.forName("EUC-KR"));

// Read all lines as List<String> (Java 7+)
List<String> lines = Files.readAllLines(path);
lines.forEach(System.out::println);

// Lazy line streaming (suitable for large files, must close!)
try (Stream<String> lineStream = Files.lines(path)) {
lineStream
.filter(line -> !line.isBlank())
.map(String::trim)
.forEach(System.out::println);
}

// Read as byte array
byte[] bytes = Files.readAllBytes(path);

Writing Files

Path output = Path.of("output.txt");

// Write a string (Java 11+, overwrites by default)
Files.writeString(output, "Hello, NIO!\nSecond line.");

// Write a list of lines (Java 7+)
List<String> lines = List.of("First line", "Second line", "Third line");
Files.write(output, lines);

// Append mode
Files.writeString(output, "\nAppended content", StandardOpenOption.APPEND);

// Write bytes
byte[] data = "Binary data".getBytes();
Files.write(output, data);

File and Directory Management

Path source = Path.of("original.txt");
Path dest = Path.of("copy.txt");
Path dir = Path.of("new_directory");

// Copy (overwrite option)
Files.copy(source, dest, StandardCopyOption.REPLACE_EXISTING);

// Move (also works as rename)
Files.move(source, Path.of("renamed.txt"), StandardCopyOption.REPLACE_EXISTING);

// Create directory
Files.createDirectory(dir); // fails if parent doesn't exist
Files.createDirectories(Path.of("a/b/c")); // creates all intermediate dirs

// Delete
Files.delete(dest); // throws if not found
Files.deleteIfExists(Path.of("maybe.txt")); // silently ignored if not found

// Temporary files
Path temp = Files.createTempFile("prefix_", "_suffix.txt");
Path tempDir = Files.createTempDirectory("myapp_");

File Metadata

Path p = Path.of("data.txt");

System.out.println(Files.exists(p)); // existence check
System.out.println(Files.isRegularFile(p)); // is a regular file
System.out.println(Files.isDirectory(p)); // is a directory
System.out.println(Files.isReadable(p)); // readable
System.out.println(Files.size(p)); // size in bytes
System.out.println(Files.getLastModifiedTime(p)); // last modified time
System.out.println(Files.isHidden(p)); // hidden file

3. Directory Traversal

Files.list — Direct Children Only

Path dir = Path.of(".");

// List .java files in the current directory (non-recursive)
try (Stream<Path> entries = Files.list(dir)) {
entries
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}

Files.walk — Recursive Traversal

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

// All .java files recursively (no depth limit)
try (Stream<Path> walk = Files.walk(root)) {
walk
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}

// Limit depth
try (Stream<Path> walk = Files.walk(root, 2)) { // at most 2 levels deep
walk.forEach(System.out::println);
}

Files.find — Traversal with Condition

try (Stream<Path> found = Files.find(
Path.of("src"),
Integer.MAX_VALUE, // max depth
(path, attrs) -> attrs.isRegularFile() && path.toString().endsWith(".java")
)) {
found.forEach(System.out::println);
}

4. Practical Example: File Utility Methods

import java.nio.file.*;
import java.io.IOException;
import java.util.*;
import java.util.stream.*;

public class FileUtils {

// Sum of all file sizes in a directory
public static long totalSize(Path dir) throws IOException {
try (Stream<Path> walk = Files.walk(dir)) {
return walk
.filter(Files::isRegularFile)
.mapToLong(p -> {
try { return Files.size(p); }
catch (IOException e) { return 0L; }
})
.sum();
}
}

// Count files by extension
public static Map<String, Long> countByExtension(Path dir) throws IOException {
try (Stream<Path> walk = Files.walk(dir)) {
return walk
.filter(Files::isRegularFile)
.collect(Collectors.groupingBy(
p -> {
String name = p.getFileName().toString();
int dot = name.lastIndexOf('.');
return dot == -1 ? "(none)" : name.substring(dot);
},
Collectors.counting()
));
}
}

// Find text files containing a keyword
public static List<Path> findContaining(Path dir, String keyword) throws IOException {
try (Stream<Path> walk = Files.walk(dir)) {
return walk
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".txt"))
.filter(p -> {
try {
return Files.readString(p).contains(keyword);
} catch (IOException e) { return false; }
})
.collect(Collectors.toList());
}
}

public static void main(String[] args) throws IOException {
Path dir = Path.of(".");

System.out.printf("Total size: %,d bytes%n", totalSize(dir));

System.out.println("Files by extension:");
countByExtension(dir).entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.forEach(e -> System.out.printf(" %-10s : %d files%n", e.getKey(), e.getValue()));
}
}

Pro Tips

Always close streams from Files.list/walk/lines: These methods return Stream objects that hold open file handles. Use try-with-resources to prevent handle leaks.

// Dangerous: stream not closed
Files.walk(dir).forEach(System.out::println);

// Safe: automatically closed
try (Stream<Path> s = Files.walk(dir)) {
s.forEach(System.out::println);
}

Path.of() vs Paths.get(): Prefer Path.of() in Java 11+. They are functionally identical, but Path.of() is more concise and is the preferred static factory method on the Path interface.

Files.copy vs manual copy: For simple file copying, Files.copy() is dramatically simpler and often faster than manual InputStream/OutputStream copying. Only resort to manual copying when you need to transform content during the copy.