Java Tutorial
šŸ”

NIO.2 (java.nio)

NIO.2 (java.nio)

NIO.2, introduced in Java 7 as java.nio.file, is the API this entire section is built on — Path, covered in File & Path Basics, and the Files methods for reading and writing content, covered in the two articles that followed. This article covers the rest of what NIO.2 offers: copying and moving files, listing and recursively walking directory trees, and briefly, watching a directory for changes.

What Does NIO.2 Add Beyond Basic Reading and Writing?

Copying a file with the older java.io streams meant manually looping over a buffer until the source was exhausted. Files.copy() replaces that entire loop with one call, and Files.move() handles renaming or relocating a file the same way.

1// File: BeforeNio2Copy.java 2import java.io.*; 3import java.nio.file.*; 4 5public class BeforeNio2Copy { 6 public static void main(String[] args) throws IOException { 7 Path source = Files.createTempFile("source", ".txt"); 8 Files.writeString(source, "important data"); 9 Path destination = source.resolveSibling("destination.txt"); 10 11 try (InputStream in = new FileInputStream(source.toFile()); 12 OutputStream out = new FileOutputStream(destination.toFile())) { 13 byte[] buffer = new byte[1024]; 14 int bytesRead; 15 while ((bytesRead = in.read(buffer)) != -1) { 16 out.write(buffer, 0, bytesRead); 17 } 18 } 19 20 System.out.println(Files.readString(destination)); 21 22 Files.delete(source); 23 Files.delete(destination); 24 } 25}
Output:
important data
1// File: AfterNio2Copy.java 2import java.io.IOException; 3import java.nio.file.*; 4 5public class AfterNio2Copy { 6 public static void main(String[] args) throws IOException { 7 Path source = Files.createTempFile("source", ".txt"); 8 Files.writeString(source, "important data"); 9 Path destination = source.resolveSibling("destination.txt"); 10 11 Files.copy(source, destination); 12 13 System.out.println(Files.readString(destination)); 14 15 Files.delete(source); 16 Files.delete(destination); 17 } 18}
Output:
important data
1// File: FilesMoveExample.java 2import java.io.IOException; 3import java.nio.file.*; 4 5public class FilesMoveExample { 6 public static void main(String[] args) throws IOException { 7 Path original = Files.createTempFile("move-demo", ".txt"); 8 Files.writeString(original, "relocatable content"); 9 10 Path renamed = original.resolveSibling("renamed.txt"); 11 Files.move(original, renamed); 12 13 System.out.println("Original exists: " + Files.exists(original)); 14 System.out.println("Renamed exists: " + Files.exists(renamed)); 15 16 Files.delete(renamed); 17 } 18}
Output:
Original exists: false
Renamed exists: true

How Directory Traversal Works Internally

Files.list() returns a Stream<Path> of a directory's direct children only, while Files.walk() recursively visits an entire tree — both return a stream backed by an open directory handle and must be used inside try-with-resources, exactly like Files.lines() from this section's Reading Files article.

One sentence before the diagram: Files.walk() performs a depth-first traversal, descending into each subdirectory fully before moving to the next sibling, and it always includes the starting directory itself as the very first entry.

root/
ā”œā”€ā”€ a.txt
ā”œā”€ā”€ b.txt
└── sub/
    └── c.txt

Files.list(root)  -->  [a.txt, b.txt, sub]              (direct children only)

Files.walk(root)  -->  [root, a.txt, b.txt, sub, sub/c.txt]   (root itself,
                        then every descendant, depth-first)
1// File: DirectoryListingExample.java 2import java.io.IOException; 3import java.nio.file.*; 4import java.util.*; 5import java.util.stream.*; 6 7public class DirectoryListingExample { 8 public static void main(String[] args) throws IOException { 9 Path root = Files.createTempDirectory("listing-demo"); 10 Files.createFile(root.resolve("a.txt")); 11 Files.createFile(root.resolve("b.txt")); 12 Path subDir = Files.createDirectory(root.resolve("sub")); 13 Files.createFile(subDir.resolve("c.txt")); 14 15 try (Stream<Path> topLevel = Files.list(root)) { 16 List<String> names = topLevel.map(p -> p.getFileName().toString()).sorted().collect(Collectors.toList()); 17 System.out.println("Top level: " + names); 18 } 19 20 try (Stream<Path> allFiles = Files.walk(root)) { 21 long fileCount = allFiles.filter(Files::isRegularFile).count(); 22 System.out.println("Total files: " + fileCount); 23 } 24 25 Files.delete(subDir.resolve("c.txt")); 26 Files.delete(subDir); 27 Files.delete(root.resolve("a.txt")); 28 Files.delete(root.resolve("b.txt")); 29 Files.delete(root); 30 } 31}
Output:
Top level: [a.txt, b.txt, sub]
Total files: 3

Files.list() sees sub as one entry and does not look inside it. Files.walk() descends into sub as well, which is why filtering for regular files across the whole tree finds three — a.txt, b.txt, and sub/c.txt — not just the two at the top level.

Files.walk()'s results always include the starting directory itself as the first entry. Forgetting this is one of the most common off-box errors when counting files in a tree.

Real-World Example

A backup utility walks a source directory tree, finds every .txt file regardless of how deeply nested it is, and copies each one into a flat backup directory — exactly the depth-first traversal this article's internals section describes, put to practical use.

1// File: TextFileBackup.java 2import java.io.IOException; 3import java.nio.file.*; 4import java.util.*; 5import java.util.stream.*; 6 7public class TextFileBackup { 8 9 public int backup(Path sourceDir, Path backupDir) throws IOException { 10 Files.createDirectories(backupDir); 11 12 List<Path> textFiles; 13 try (Stream<Path> files = Files.walk(sourceDir)) { 14 textFiles = files 15 .filter(Files::isRegularFile) 16 .filter(p -> p.toString().endsWith(".txt")) 17 .collect(Collectors.toList()); 18 } 19 20 for (Path file : textFiles) { 21 Path target = backupDir.resolve(file.getFileName()); 22 Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING); 23 } 24 25 return textFiles.size(); 26 } 27}
1// File: TextFileBackupDemo.java 2import java.io.IOException; 3import java.nio.file.*; 4 5public class TextFileBackupDemo { 6 public static void main(String[] args) throws IOException { 7 Path sourceDir = Files.createTempDirectory("backup-source"); 8 Files.writeString(sourceDir.resolve("notes.txt"), "meeting notes"); 9 Files.writeString(sourceDir.resolve("photo.jpg"), "not really a jpg"); 10 Path subDir = Files.createDirectory(sourceDir.resolve("archive")); 11 Files.writeString(subDir.resolve("old-notes.txt"), "older notes"); 12 13 Path backupDir = sourceDir.resolveSibling("backup-target"); 14 15 TextFileBackup backup = new TextFileBackup(); 16 int count = backup.backup(sourceDir, backupDir); 17 18 System.out.println("Files backed up: " + count); 19 System.out.println("Backup contains notes.txt: " + Files.exists(backupDir.resolve("notes.txt"))); 20 System.out.println("Backup contains old-notes.txt: " + Files.exists(backupDir.resolve("old-notes.txt"))); 21 System.out.println("Backup contains photo.jpg: " + Files.exists(backupDir.resolve("photo.jpg"))); 22 23 Files.delete(sourceDir.resolve("notes.txt")); 24 Files.delete(sourceDir.resolve("photo.jpg")); 25 Files.delete(subDir.resolve("old-notes.txt")); 26 Files.delete(subDir); 27 Files.delete(sourceDir); 28 Files.delete(backupDir.resolve("notes.txt")); 29 Files.delete(backupDir.resolve("old-notes.txt")); 30 Files.delete(backupDir); 31 } 32}
Output:
Files backed up: 2
Backup contains notes.txt: true
Backup contains old-notes.txt: true
Backup contains photo.jpg: false

A mistake that appears often in fresher pull requests is forgetting that Files.walk(), like Files.list() and Files.lines(), returns a stream backed by an open directory handle. Collecting the filtered results into a plain List first, exactly as backup does here, closes that handle promptly instead of holding it open for the entire copy operation that follows.

Best Practices

Always use try-with-resources around Files.list() and Files.walk(), exactly as around Files.lines() — all three hold an open resource that needs to be closed promptly.

Prefer Files.copy() and Files.move() over a manual buffered-stream copy loop for anything that is not a specialized transformation of the data along the way.

Pass StandardCopyOption.REPLACE_EXISTING explicitly whenever overwriting the destination is the intended behavior, rather than letting the call fail unexpectedly when the destination already exists.

Reach for Files.walk() only when recursion into subdirectories is actually needed — Files.list() is simpler and clearer for a single directory level.

Common Mistakes

Calling Files.copy() on a destination that already exists, without REPLACE_EXISTING, throws rather than silently overwriting it.

1// File: CopyExistsMistake.java 2import java.io.IOException; 3import java.nio.file.*; 4 5public class CopyExistsMistake { 6 public static void main(String[] args) throws IOException { 7 Path source = Files.createTempFile("copy-source", ".txt"); 8 Path destination = Files.createTempFile("copy-destination", ".txt"); 9 10 try { 11 Files.copy(source, destination); 12 } catch (FileAlreadyExistsException e) { 13 System.out.println("Caught: " + e.getClass().getSimpleName()); 14 } 15 16 Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING); 17 System.out.println("Copied with REPLACE_EXISTING"); 18 19 Files.delete(source); 20 Files.delete(destination); 21 } 22}
Output:
Caught: FileAlreadyExistsException
Copied with REPLACE_EXISTING

Forgetting that Files.walk() includes the starting directory itself in its results, not just its descendants, is a second, easy off-by-one mistake.

1// File: WalkIncludesRootMistake.java 2import java.io.IOException; 3import java.nio.file.*; 4import java.util.stream.*; 5 6public class WalkIncludesRootMistake { 7 public static void main(String[] args) throws IOException { 8 Path root = Files.createTempDirectory("walk-root-demo"); 9 Files.createFile(root.resolve("one.txt")); 10 Files.createFile(root.resolve("two.txt")); 11 12 try (Stream<Path> all = Files.walk(root)) { 13 System.out.println("Total entries: " + all.count()); 14 } 15 16 Files.delete(root.resolve("one.txt")); 17 Files.delete(root.resolve("two.txt")); 18 Files.delete(root); 19 } 20}
Output:
Total entries: 3

Two files were created, but Files.walk() reports three entries, since the root directory itself counts as the first entry in its results — filtering with Files::isRegularFile, as this article's directory listing example does, is what excludes it when only actual files are wanted.

Interview Questions

Q1. What is NIO.2, and which Java version introduced it?

NIO.2 is the java.nio.file package — Path, Files, and related classes — introduced in Java 7. It replaced most of the practical need for java.io.File, offering exception-based error handling and richer operations like copying, moving, and recursively walking a directory tree. Interviewers listen for whether you can name the version and the exception-handling improvement, not just recognize the package name.

Q2. What is the difference between Files.list() and Files.walk()?

Files.list() returns a stream of a directory's direct children only, with no recursion. Files.walk() recursively visits every file and directory in the entire tree rooted at the given path, including the root itself. The nuance being tested is that last detail — many candidates forget the root is included.

Q3. What happens if Files.copy() is called with a destination that already exists?

It throws FileAlreadyExistsException, unless StandardCopyOption.REPLACE_EXISTING is passed explicitly, in which case the destination is overwritten. A strong answer names the exact exception type, not just "it throws something."

Q4. Why must Files.list() and Files.walk() be used inside try-with-resources?

Both return a Stream<Path> backed by an open directory-reading resource, exactly like Files.lines() — failing to close it, whether through try-with-resources or an explicit close() call, leaves that resource open longer than necessary. This tests whether you generalize the resource-management pattern across all three stream-returning Files methods, not just memorize it for one.

Q5. Does Files.walk() include the starting directory itself in its results?

Yes, as demonstrated in this article's Common Mistakes section — the starting directory is always the first entry in the resulting stream, before any of its descendants. This is a very commonly asked "gotcha" question specifically because it trips up so many production off-by-one bugs.

Q6. What is the difference between Files.move() and copying followed by deleting the original?

When the source and destination are on the same filesystem, Files.move() is typically an atomic rename with no actual data copy involved. A manual copy-then-delete always physically duplicates the bytes and is not atomic — a failure between the two steps could leave both a copy and the original, or neither, depending on where it failed. Product-company interviewers listen for the word "atomic" specifically, since it signals you understand the crash-safety implication.

Q7. What is WatchService used for?

It asynchronously monitors one or more directories for filesystem change events — file creation, deletion, or modification — letting a program react to changes without repeatedly polling the directory's contents itself. The nuance interviewers want to hear is that it is event-driven, not a polling loop written by hand.

FAQs

Does Files.copy() work for copying an entire directory tree at once?

No, not by itself. Calling Files.copy() on a directory only creates an empty directory at the destination — recursively copying a directory's contents requires walking the tree and copying each file individually, exactly as this article's real-world example does for .txt files specifically.

Is Files.walk() guaranteed to return entries in a specific order?

No, the order is not specified by the API and can vary by filesystem and platform — code that depends on a particular order should sort the results explicitly, as DirectoryListingExample does above.

Can Files.move() be used to rename a file within the same directory?

Yes. Renaming is simply a move where the parent directory does not change — Files.move(path, path.resolveSibling("new-name.txt")) renames a file in place.

Does WatchService require polling in a loop to detect changes?

Yes, in the sense that a typical usage pattern repeatedly calls take() (which blocks until an event arrives) or poll() (which returns immediately) in a loop to retrieve queued watch events — it is not a callback-based API.

What is the difference between Files.list() and Files.newDirectoryStream()?

Both list a single directory level without recursion. Files.newDirectoryStream() is the original NIO.2 API, returning an Iterable<Path> used in a for-each loop. Files.list() returns a Stream<Path> directly, and is generally preferred in modern code for integrating naturally with the Streams API.

Can Files.walk() be limited to a maximum depth?

Yes, Files.walk(Path, int maxDepth) accepts an explicit maximum recursion depth, useful when only a few levels of nesting need to be visited rather than the entire tree.

Does copying a file preserve its last-modified timestamp?

Not by default. Files.copy() without StandardCopyOption.COPY_ATTRIBUTES gives the copy a new last-modified time reflecting when the copy happened — passing COPY_ATTRIBUTES explicitly preserves the original file's timestamps and other attributes.

Summary

NIO.2 rounds out this section's coverage of java.nio.file — Files.copy() and Files.move() replace manual stream-copy loops entirely, and Files.list() and Files.walk() bring directory traversal into the same Stream-based model the Streams API already covers, at the cost of needing the same try-with-resources discipline as Files.lines().

The habit worth carrying forward from this article's backup utility is remembering that Files.walk()'s results always include the starting directory itself, and closing its stream promptly by collecting filtered results into a list before acting on them, rather than performing further file operations while the directory handle is still open.

What to Read Next