33. Files and Paths APIs
Table of Contents
- 33.1 Legacy File and NIO Path Creation-and-Conversion
- 33.1.1 Creating a File Legacy
- 33.1.2 Creating a Path NIO-v2
- 33.1.3 Absolute vs Relative What Relative Means
- 33.1.4 Joining--Building-Paths
- 33.1.5 Converting Between File and Path
- 33.1.6 URI Conversion When-Needed
- 33.1.7 Canonical vs Absolute vs Normalized Core-Differences
- 33.1.8 Quick Comparison Table Creation--Conversion
- 33.2 Managing Files and Directories Create-Copy-Move-Replace-Compare-Delete
- 33.2.1 Mental Model Path-Locator-vs-Operations
- 33.2.2 Creating Files and Directories
- 33.2.3 Copying Files and Directories
- 33.2.4 Moving--Renaming-and-Replacing
- 33.2.5 Comparing Paths and Files
- 33.2.6 Deleting Files and Directories
- 33.2.7 Recursively Copying--Deleting-Directory-Trees NIO-Pattern
- 33.2.8 Summary Checklist
This section focuses on how to create filesystem locators using the legacy java.io.File API and the modern java.nio.file.Path API: how to convert between them and understanding overloads, defaults, and common pitfalls.
33.1 Legacy File and NIO Path: Creation and Conversion
33.1.1 Creating a File (Legacy)
A File instance represents a filesystem pathname (absolute or relative).
Creating one does not access the filesystem and does not throw IOException.
Core constructors (most common):
new File(String pathname)new File(String parent, String child)new File(File parent, String child)new File(URI uri)(typicallyfile:...)
import java.io.File;
import java.net.URI;
File f1 = new File("data.txt"); // relative
File f2 = new File("/tmp", "data.txt"); // parent + child
File f3 = new File(new File("/tmp"), "data.txt");
File f4 = new File(URI.create("file:///tmp/data.txt"));
Note
new File(...)never opens the file.- Existence/permissions are checked only when you call methods like
exists(),length(), or when you open a stream/channel.
33.1.2 Creating a Path (NIO v.2)
A Path is also just a locator.
Like File, creating a Path does not access the filesystem.
Core factories:
Path.of(String first, String... more)(Java 11+)Paths.get(String first, String... more)(older style; still valid)Path.of(URI uri)(e.g.,file:///...)
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
Path p1 = Path.of("data.txt"); // relative
Path p2 = Path.of("/tmp", "data.txt"); // parent + child
Path p3 = Paths.get("data.txt"); // legacy factory style
Path p4 = Path.of(URI.create("file:///tmp/data.txt"));
Note
Path.of(...)andPaths.get(...)are equivalent for the default filesystem.- Prefer
Path.ofin modern code.
33.1.3 Absolute vs Relative: What “Relative” Means
Both File and Path can be created as relative paths.
Relative paths are resolved against the process working directory (typically System.getProperty("user.dir")).
import java.io.File;
import java.nio.file.Path;
File rf = new File("data.txt");
Path rp = Path.of("data.txt");
System.out.println(rf.isAbsolute()); // false
System.out.println(rp.isAbsolute()); // false
System.out.println(rf.getAbsolutePath());
System.out.println(rp.toAbsolutePath());
Note
Relative paths are a common source of “works on my machine” bugs because user.dir depends on how/where the JVM was launched.
33.1.4 Joining / Building Paths
- Legacy
Fileuses constructors (parent + child). - NIO uses
resolveand related methods.
| Task | Legacy (File) | NIO (Path) |
|---|---|---|
| Join parent + child | new File(parent, child) |
parent.resolve(child) |
| Join many segments | Repeated constructors | Path.of(a, b, c) or chained resolve() |
import java.io.File;
import java.nio.file.Path;
File f = new File("/tmp", "a.txt");
Path base = Path.of("/tmp");
Path p = base.resolve("a.txt"); // /tmp/a.txt
Path p2 = base.resolve("dir").resolve("a.txt"); // /tmp/dir/a.txt
33.1.4.1 resolve()
Combines paths in a filesystem-aware way.
- Relative paths are appended
- Absolute argument replaces base path
Note
Path.resolve(...) has a rule: if the argument is absolute, it returns the argument and discards the base (you cannot combine two absolute paths using resolve).
33.1.4.2 relativize()
Path.relativize computes a relative path from one path to another. The resulting path, when resolved against the source path, yields the target path.
In other words:
- It answers the question: “How do I go from path A to path B?”
- The result is always a relative path
- No filesystem access occurs
Fundamental Rules
relativize has strict preconditions. Violating them throws an exception.
| Rule | Explanation |
|---|---|
| Both paths must be absolute | or both relative |
| Both paths must belong to the same filesystem | same provider |
| Root components must match | same root (on Windows, same drive) |
| Result is never absolute | always relative |
Note
If one path is absolute and the other relative, IllegalArgumentException is thrown.
Simple Relative Example:
Both paths are relative, so relativization is allowed.
Path p1 = Path.of("docs/manual");
Path p2 = Path.of("docs/images/logo.png");
Path relative = p1.relativize(p2);
System.out.println(relative);
../images/logo.png
Interpretation: from docs/manual, go up one level, then into images/logo.png.
Absolute Paths Example:
Absolute paths work exactly the same way.
Path base = Path.of("/home/user/projects");
Path target = Path.of("/home/user/docs/readme.txt");
Path relative = base.relativize(target);
System.out.println(relative);
../docs/readme.txt
Using resolve to Verify the Result
A key property of relativize is this identity:
base.resolve(base.relativize(target)).equals(target)
Path base = Path.of("/a/b/c");
Path target = Path.of("/a/d/e");
Path r = base.relativize(target);
System.out.println(r); // ../../d/e
System.out.println(base.resolve(r)); // /a/d/e
Example: Mixing Absolute and Relative Paths (ERROR CASE)
This is one of the most common mistakes.
Path abs = Path.of("/a/b");
Path rel = Path.of("c/d");
abs.relativize(rel); // throws exception
Exception in thread "main" java.lang.IllegalArgumentException
Note
relativize does NOT attempt to convert paths to absolute automatically.
Example: Different Roots (Windows-Specific Trap)
On Windows, paths with different drive letters cannot be relativized.
Path p1 = Path.of("C:\\data\\a");
Path p2 = Path.of("D:\\data\\b");
p1.relativize(p2); // IllegalArgumentException
Note
On Unix-like systems, the root is always /, so this issue does not occur.
33.1.5 Converting Between File and Path
Conversion is straightforward and lossless for normal local filesystem paths.
| Conversion | How |
|---|---|
| File → Path | file.toPath() |
| Path → File | path.toFile() |
import java.io.File;
import java.nio.file.Path;
File f = new File("data.txt");
Path p = f.toPath();
File back = p.toFile();
Note
Conversion does not validate existence. It only converts representations.
33.1.6 URI Conversion (When Needed)
URIs are useful when paths must be represented in a standard, absolute form (e.g., interoperating with networked resources or configuration).
Both APIs support URI conversion.
| Direction | Legacy (File) | NIO (Path) |
|---|---|---|
| From URI | new File(uri) |
Path.of(uri) |
| To URI | file.toURI() |
path.toUri() |
import java.io.File;
import java.net.URI;
import java.nio.file.Path;
File f = new File("/tmp/data.txt");
URI u1 = f.toURI();
Path p = Path.of("/tmp/data.txt");
URI u2 = p.toUri();
Path pFromUri = Path.of(u2);
File fFromUri = new File(u1);
Note
new File(URI) requires a file: URI and throws IllegalArgumentException if the URI is not hierarchical or not a file URI.
33.1.7 Canonical vs Absolute vs Normalized (Core Differences)
These terms are often mixed up. They are not the same.
| Concept | Legacy (File) | NIO (Path) | Touches filesystem |
|---|---|---|---|
| Absolute | getAbsoluteFile() |
toAbsolutePath() |
No |
| Normalized | (no pure normalize, use canonical)* | normalize() |
normalize(): No |
| Canonical / Real | getCanonicalFile() |
toRealPath() |
Yes |
Note
File.getCanonicalFile() and Path.toRealPath() may resolve symlinks and require the path to exist, so they can throw IOException.
File does not provide a method for purely syntactic normalization: historically many developers used getCanonicalFile(), but this accesses the filesystem and can fail.
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
File f = new File("a/../data.txt");
System.out.println(f.getAbsolutePath()); // absolute, may still contain ".."
try {
System.out.println(f.getCanonicalPath()); // resolves "..", may touch filesystem
} catch (IOException e) {
System.out.println("Canonical failed: " + e.getMessage());
}
Path p = Path.of("a/../data.txt");
System.out.println(p.toAbsolutePath()); // absolute, may still contain ".."
System.out.println(p.normalize()); // purely syntactic
try {
System.out.println(p.toRealPath()); // resolves symlinks, requires existence
} catch (IOException e) {
System.out.println("RealPath failed: " + e.getMessage());
}
33.1.7.1 normalize()
Removes redundant name elements like . and ...
- Purely syntactic
- Does not check if path exists
Note
normalize() is purely syntactic, does not check existence, and can produce invalid paths if misused.
33.1.8 Quick Comparison Table (Creation + Conversion)
| Need | Legacy (File) | NIO (Path) | Preferred today |
|---|---|---|---|
| Create from string | new File("x") |
Path.of("x") |
Path |
| Parent + child | new File(p, c) |
Path.of(p, c) or resolve() |
Path |
| Convert between APIs | toPath() |
toFile() |
Path-centric |
| Normalize | getCanonicalFile() (filesystem-based) |
normalize() (syntactic only) |
Path |
| Resolve symlinks | Canonical | toRealPath() |
Path |
33.2 Managing Files and Directories: Create, Copy, Move, Replace, Compare, Delete (Legacy vs NIO)
This section covers the operations you perform on filesystem entries (files/directories): creating, copying, moving/renaming, replacing, comparing, and deleting.
It contrasts legacy java.io.File (and related legacy helpers) with modern java.nio.file (NIO.2).
33.2.1 Mental Model: “Path/Locator” vs “Operations”
Both APIs use objects that represent a path, but operations differ:
- Legacy:
Fileis both a path wrapper and an operations API (mixed responsibility) - NIO:
Pathis the path;Filesperforms operations (separation of concerns)
| Responsibility | Legacy | NIO |
|---|---|---|
| Path representation | File |
Path |
| Filesystem operations | File |
Files |
| Rich error reporting | Weak (booleans) | Strong (exceptions) |
Note
legacy methods often return boolean (silent failure), while NIO throws IOException with cause.
33.2.2 Creating Files and Directories
Creating is where the old API is most awkward and the NIO API is most expressive.
| Task | Legacy approach | NIO approach | Notes |
|---|---|---|---|
| Create empty file | open+close stream | Files.createFile |
NIO fails if exists |
| Create one directory | mkdir |
Files.createDirectory |
Parent must exist |
| Create directories recursively | mkdirs |
Files.createDirectories |
Creates parents |
33.2.2.1 Create a File
Legacy has no “create empty file” method, so you typically create a file by opening an output stream (side effect).
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
File f = new File("created-legacy.txt");
try (FileOutputStream out = new FileOutputStream(f)) {
// file is created (or truncated) as a side effect
}
NIO provides an explicit creation method.
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;
Path p = Path.of("created-nio.txt");
Files.createFile(p);
Note
Files.createFile throws FileAlreadyExistsException if the entry exists.
33.2.2.2 Create Directories
import java.io.File;
File dir1 = new File("a/b");
boolean ok1 = dir1.mkdir(); // fails if parent "a" does not exist
boolean ok2 = dir1.mkdirs(); // creates parents
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;
Path d = Path.of("a/b");
Files.createDirectory(d); // parent must exist
Files.createDirectories(d); // creates parents, ok if already exists
Note
Legacy mkdir()/mkdirs() return false on failure without telling why. NIO throws IOException.
33.2.3 Copying Files and Directories
Legacy copy is usually manual stream-copy (or external libs). NIO has a single, explicit operation.
| Capability | Legacy | NIO |
|---|---|---|
| Copy file contents | Manual streams | Files.copy |
| Copy into existing target | Manual | REPLACE_EXISTING option |
| Copy directory tree | Manual recursion | Manual recursion (but better tools: Files.walk + Files.copy) |
33.2.3.1 Copy a File (NIO)
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.io.IOException;
Path src = Path.of("src.txt");
Path dst = Path.of("dst.txt");
Files.copy(src, dst); // fails if dst exists
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
Note
Files.copy throws FileAlreadyExistsException if the target exists and you did not use REPLACE_EXISTING.
33.2.3.2 Manual Copy (Legacy, Stream-Based)
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
try (FileInputStream in = new FileInputStream("src.bin");
FileOutputStream out = new FileOutputStream("dst.bin")) {
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n);
}
}
Note
Remember read(byte[]) returns the number of bytes read; you must write only that count, not the full buffer.
33.2.4 Moving / Renaming and Replacing
In both APIs, rename/move is “metadata-level” when possible, but can behave like copy+delete across filesystems. NIO makes this explicit via options.
| Operation | Legacy | NIO |
|---|---|---|
| Rename/move | File.renameTo |
Files.move |
| Replace existing | Unreliable | REPLACE_EXISTING |
| Atomic move | Not supported | ATOMIC_MOVE (if supported) |
33.2.4.1 Legacy Rename (Common Pitfall)
import java.io.File;
File from = new File("old.txt");
File to = new File("new.txt");
boolean ok = from.renameTo(to); // may fail silently
System.out.println(ok);
Note
renameTois notoriously platform-dependent and returns onlyboolean.- It may fail because target exists, file is open, permissions, or cross-filesystem move.
33.2.4.2 NIO Move (Preferred)
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.io.IOException;
Path from = Path.of("old.txt");
Path to = Path.of("new.txt");
Files.move(from, to); // fails if target exists
Files.move(from, to, StandardCopyOption.REPLACE_EXISTING);
Note
Files.move throws FileAlreadyExistsException when the target exists and REPLACE_EXISTING is not specified.
33.2.5 Comparing Paths and Files
Comparing locators can mean: string/path equality, normalized/canonical equality, or “same file on disk”.
The APIs differ here significantly.
| Comparison goal | Legacy | NIO |
|---|---|---|
| Same path text | File.equals |
Path.equals |
| Normalize path | getCanonicalFile |
normalize |
| Same file/resource on disk | weak (canonical heuristic) | Files.isSameFile |
33.2.5.1 Equality vs Same File
Two different path strings can refer to the same file.
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;
Path p1 = Path.of("a/../data.txt");
Path p2 = Path.of("data.txt");
System.out.println(p1.equals(p2)); // false (different path text)
System.out.println(p1.normalize().equals(p2.normalize())); // might still be false if relative
try {
System.out.println(Files.isSameFile(p1, p2)); // may be true, may throw if not accessible
} catch (IOException e) {
System.out.println("isSameFile failed: " + e.getMessage());
}
Note
Files.isSameFile may access the filesystem and can throw IOException (permission issues, missing files, etc.).
33.2.6 Deleting Files and Directories
Deletion is simple in concept but has important edge cases: non-empty directories, missing targets, and error reporting differences.
| Task | Legacy | NIO | Behavior if missing |
|---|---|---|---|
| Delete file/dir | File.delete |
Files.delete |
Legacy false, NIO exception |
| Delete if exists | No direct (check+delete) | Files.deleteIfExists |
returns boolean |
| Delete non-empty dir | Manual recursion | Manual recursion (walk) | Both require recursion |
33.2.6.1 Legacy Delete
import java.io.File;
File f = new File("x.txt");
boolean ok = f.delete(); // false if not deleted
System.out.println(ok);
Note
Legacy delete() fails (returns false) for a non-empty directory and often provides no reason.
33.2.6.2 NIO Delete and Delete-If-Exists
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.NoSuchFileException;
import java.nio.file.DirectoryNotEmptyException;
import java.io.IOException;
Path p = Path.of("x.txt");
try {
Files.delete(p);
} catch (NoSuchFileException e) {
System.out.println("Missing: " + e.getFile());
} catch (DirectoryNotEmptyException e) {
System.out.println("Directory not empty: " + e.getFile());
} catch (IOException e) {
System.out.println("Delete failed: " + e.getMessage());
}
boolean deleted = Files.deleteIfExists(p);
System.out.println(deleted);
Note
Certification tip: Files.delete throws NoSuchFileException if missing, while deleteIfExists returns false.
33.2.7 Recursively Copying / Deleting Directory Trees (NIO Pattern)
NIO doesn’t provide a single “copyTree/deleteTree” method, but the standard approach uses Files.walk or Files.walkFileTree.
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
Path root = Path.of("dirToDelete");
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (exc != null) throw exc;
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
Note
Deleting a directory tree requires deleting files first, then directories (post-order). This is a common reasoning question.
33.2.8 Summary Checklist
- Prefer
Files.createFile/createDirectory/createDirectoriesover legacy workarounds File.renameTois unreliable; preferFiles.movewith optionsFiles.copy/movethrowFileAlreadyExistsExceptionunlessREPLACE_EXISTINGis usedFiles.deletethrows;Files.deleteIfExistsreturns booleanFiles.isSameFilecan throwIOExceptionand may touch the filesystem- Non-empty directory deletion requires recursion (both APIs)