File Handling
File handling in Java spans two generations of API: the legacy java.io.File class introduced in Java 1.0, and the modern java.nio.file package (NIO.2) introduced in Java 7 with its Path interface, Files utility class, and FileSystem abstraction. The File class represents a file or directory path as an abstract pathname and provides methods for querying metadata, listing directory contents, creating and deleting files, and basic path manipulation. Its limitations — no symbolic link support, inconsistent error reporting (methods return boolean instead of throwing exceptions), no atomic operations, limited metadata access, and performance issues for large directory traversals — motivated the complete redesign in NIO.2. The Path interface and Files class cover all functionality of File with better exception handling, symbolic link support, atomic operations, rich metadata via BasicFileAttributes, efficient directory walking with Files.walk() and Files.walkFileTree(), file watching with WatchService, and a provider model for custom file system implementations. This entry covers the complete File API and its limitations, the NIO.2 Path and Files APIs, directory traversal strategies, file watching, temporary files, and best practices for cross-platform path handling.
java.io.File — The Legacy API and Its Limitations
// ── File construction — all forms ────────────────────────────────────
File absolute = new File("/home/user/documents/report.txt");
File relative = new File("data/config.json"); // relative to CWD
File fromParent = new File("/home/user", "documents/report.txt");
File fromFile = new File(new File("/home/user"), "documents");
File fromUri = new File(URI.create("file:///tmp/test.txt"));
// Platform-safe path construction:
String sep = File.separator; // "/" on Unix, "\" on Windows
File safe = new File("parent" + sep + "child" + sep + "file.txt");
// ── Query methods ─────────────────────────────────────────────────────
File f = new File("/home/user/data.txt");
System.out.println(f.exists()); // true if any file system entry at this path
System.out.println(f.isFile()); // true if regular file (not directory, not symlink target)
System.out.println(f.isDirectory()); // true if directory
System.out.println(f.length()); // bytes; 0 if doesn't exist or is directory
System.out.println(f.lastModified()); // ms since epoch; 0 if doesn't exist
System.out.println(f.canRead()); // permission check + existence
System.out.println(f.canWrite());
System.out.println(f.canExecute());
System.out.println(f.isHidden()); // starts with "." on Unix; hidden attribute on Windows
System.out.println(f.isAbsolute()); // path is absolute vs relative
System.out.println(f.getAbsolutePath()); // absolute path string
System.out.println(f.getCanonicalPath()); // resolves symlinks and ".." — throws IOException
// The boolean-return failure mode — cannot distinguish errors:
File missing = new File("/nonexistent/path/file.txt");
System.out.println(missing.exists()); // false — but WHY? No permission? Doesn't exist?
System.out.println(missing.delete()); // false — but WHY? Doesn't exist? Not empty? No permission?
// ── Mutation methods — always check return values ─────────────────────
File newFile = new File("/tmp/test.txt");
boolean created = newFile.createNewFile(); // false if already exists — NOT an error
if (!created && !newFile.exists()) {
throw new IOException("Failed to create file: " + newFile);
}
File dir = new File("/tmp/newdir/subdir");
if (!dir.mkdirs()) { // creates /tmp/newdir AND /tmp/newdir/subdir
if (!dir.exists()) throw new IOException("Could not create directory: " + dir);
}
boolean deleted = newFile.delete(); // returns false silently on failure
if (!deleted) {
System.err.println("Delete failed — file may not exist or may be locked");
}
// renameTo — not atomic on some platforms, fails silently:
File source = new File("/tmp/old.txt");
File dest = new File("/tmp/new.txt");
if (!source.renameTo(dest)) {
// Fails across filesystems on many platforms — copy-then-delete needed
System.err.println("Rename failed");
}
// ── Directory listing ─────────────────────────────────────────────────
File dir2 = new File("/home/user");
String[] names = dir2.list(); // null if not a directory or I/O error — always check!
if (names == null) throw new IOException("Cannot list: " + dir2);
Arrays.sort(names); // list() does NOT guarantee order
for (String name : names) System.out.println(name);
// With filter:
String[] javaFiles = dir2.list((d, n) -> n.endsWith(".java"));
File[] javaFileObjs = dir2.listFiles(f2 -> f2.isFile() && f2.getName().endsWith(".java"));
// listFiles() also returns null on error — always check:
File[] children = dir2.listFiles();
if (children == null) throw new IOException("Cannot list: " + dir2);NIO.2 — Path, Files, and the Modern File System API
// ── Path construction and manipulation ───────────────────────────────
Path p1 = Path.of("/home/user/documents/report.txt");
Path p2 = Path.of("data", "config", "settings.json"); // joined with File.separator
// Path manipulation:
System.out.println(p1.getFileName()); // report.txt
System.out.println(p1.getParent()); // /home/user/documents
System.out.println(p1.getRoot()); // /
System.out.println(p1.getNameCount()); // 4 (home, user, documents, report.txt)
System.out.println(p1.getName(2)); // documents
System.out.println(p1.subpath(1, 3)); // user/documents
// Resolution and relativization:
Path base = Path.of("/home/user");
Path resolved = base.resolve("documents/file.txt"); // /home/user/documents/file.txt
Path sibling = p1.resolveSibling("other.txt"); // /home/user/documents/other.txt
Path relative = base.relativize(resolved); // documents/file.txt
// Normalization:
Path messy = Path.of("/home/user/../user/./documents/./report.txt");
System.out.println(messy.normalize()); // /home/user/documents/report.txt
// Convert to/from File:
File asFile = p1.toFile();
Path fromFile = asFile.toPath();
// ── Files — query, create, delete ────────────────────────────────────
Path target = Path.of("/tmp/example.txt");
// Existence and metadata (proper exceptions, not silent booleans):
boolean exists = Files.exists(target); // follows symlinks by default
boolean exists2 = Files.exists(target, LinkOption.NOFOLLOW_LINKS); // don't follow
long size = Files.size(target); // throws NoSuchFileException if missing
FileTime mtime = Files.getLastModifiedTime(target); // throws NoSuchFileException
// Create (throws FileAlreadyExistsException if exists — correct, not silent):
try {
Files.createFile(target);
} catch (FileAlreadyExistsException e) {
System.out.println("Already exists: " + e.getFile());
}
Files.createDirectories(Path.of("/tmp/a/b/c")); // creates all missing parents
Files.createTempFile(Path.of("/tmp"), "prefix-", ".tmp"); // /tmp/prefix-XXXX.tmp
// Delete (throws NoSuchFileException if not found — correct):
try {
Files.delete(target);
} catch (NoSuchFileException e) {
System.out.println("Did not exist: " + e.getFile());
}
Files.deleteIfExists(target); // no exception if not found — idempotent
// ── Files.copy and Files.move ────────────────────────────────────────
Path src = Path.of("/tmp/source.txt");
Path dst = Path.of("/tmp/dest.txt");
Path dst2 = Path.of("/tmp/moved.txt");
Files.copy(src, dst); // throws FileAlreadyExistsException if dst exists
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
// Atomic move — crash-safe write pattern:
Path tempFile = Files.createTempFile(dst2.getParent(), ".tmp-", ".dat");
try {
Files.write(tempFile, data); // write to temp
// fsync for durability (requires FileChannel.force() — not in Files API)
try (FileChannel ch = FileChannel.open(tempFile, StandardOpenOption.WRITE)) {
ch.force(true); // flush to disk hardware
}
Files.move(tempFile, dst2,
StandardCopyOption.ATOMIC_MOVE, // atomic on same filesystem
StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException e) {
// Cross-filesystem move — cannot be atomic
Files.copy(tempFile, dst2, StandardCopyOption.REPLACE_EXISTING);
Files.delete(tempFile);
}
// ── Rich metadata via BasicFileAttributes ─────────────────────────────
BasicFileAttributes attrs = Files.readAttributes(target, BasicFileAttributes.class);
System.out.println("Size: " + attrs.size());
System.out.println("Created: " + attrs.creationTime());
System.out.println("Modified: " + attrs.lastModifiedTime());
System.out.println("Is file: " + attrs.isRegularFile());
System.out.println("Is dir: " + attrs.isDirectory());
System.out.println("Is symlink: " + attrs.isSymbolicLink());
// POSIX attributes (Unix/macOS):
PosixFileAttributes posix = Files.readAttributes(target, PosixFileAttributes.class);
System.out.println("Owner: " + posix.owner().getName());
System.out.println("Group: " + posix.group().getName());
System.out.println("Permissions: " + PosixFilePermissions.toString(posix.permissions()));
// e.g., "rwxr-xr--"Directory Traversal, File Watching, and Temporary Files
// ── Files.walk — lazy recursive traversal ────────────────────────────
Path root = Path.of("/home/user/project");
// Find all .java files, sorted:
try (Stream<Path> walk = Files.walk(root)) {
List<Path> javaFiles = walk
.filter(p -> p.toString().endsWith(".java"))
.sorted()
.collect(Collectors.toList());
javaFiles.forEach(System.out::println);
} // MUST close — else DirectoryStream leaks
// Size of entire directory tree:
try (Stream<Path> walk = Files.walk(root)) {
long totalBytes = walk
.filter(Files::isRegularFile)
.mapToLong(p -> { try { return Files.size(p); } catch (IOException e) { return 0; } })
.sum();
System.out.printf("Total: %.2f MB%n", totalBytes / 1e6);
}
// ── Files.find — efficient filtered walk ─────────────────────────────
// Find files modified in the last 24 hours, larger than 1MB:
FileTime cutoff = FileTime.from(Instant.now().minusSeconds(86400));
try (Stream<Path> found = Files.find(root, Integer.MAX_VALUE,
(path, attrs) -> attrs.isRegularFile()
&& attrs.size() > 1_000_000
&& attrs.lastModifiedTime().compareTo(cutoff) > 0)) {
found.forEach(p -> System.out.println(p + " (" + getSize(p) + " bytes)"));
}
// ── Files.walkFileTree — delete directory tree ────────────────────────
void deleteTree(Path dir) throws IOException {
Files.walkFileTree(dir, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file); // delete file
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir2, IOException exc)
throws IOException {
if (exc != null) throw exc;
Files.delete(dir2); // delete directory AFTER its contents are deleted
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Could not delete: " + file + " — " + exc.getMessage());
return FileVisitResult.CONTINUE; // skip and continue
}
});
}
// ── WatchService — OS-level file system event monitoring ──────────────
Path watchDir = Path.of("/home/user/config");
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
// Register for create, modify, delete events:
watchDir.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
System.out.println("Watching: " + watchDir);
// Event loop — runs until interrupted:
while (true) {
WatchKey key = watcher.take(); // blocks until an event occurs
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
// OVERFLOW means events were lost — handle gracefully:
if (kind == StandardWatchEventKinds.OVERFLOW) {
System.err.println("Events lost — rescanning directory");
continue;
}
@SuppressWarnings("unchecked")
WatchEvent<Path> pathEvent = (WatchEvent<Path>) event;
Path changed = watchDir.resolve(pathEvent.context());
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
System.out.println("Created: " + changed);
reloadConfig(changed);
} else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
System.out.println("Modified: " + changed);
reloadConfig(changed);
} else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
System.out.println("Deleted: " + changed);
}
}
// Must reset the key to receive further events:
boolean valid = key.reset();
if (!valid) {
System.out.println("Watch directory no longer accessible");
break;
}
}
}
// ── Temporary files and directories ──────────────────────────────────
// Temporary file in system temp dir:
Path tempFile = Files.createTempFile("prefix-", ".tmp");
// /tmp/prefix-8572345234987.tmp (on Unix)
// Temporary file in specific directory:
Path tempInDir = Files.createTempFile(Path.of("/home/user/work"), "temp-", ".json");
// Temporary directory:
Path tempDir = Files.createTempDirectory("myapp-");
// Always delete temp files — JVM does NOT automatically delete them:
try {
doWorkWith(tempFile);
} finally {
Files.deleteIfExists(tempFile); // or register with deleteOnExit:
}
// Register for deletion on JVM exit (best-effort — not called on kill -9):
tempFile.toFile().deleteOnExit(); // uses File.deleteOnExit() under the hood