☕ Java

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

java.io.File represents a file or directory as an abstract pathname — a string that may or may not correspond to an actual file system entry. File objects are immutable (the path string cannot change after construction) but the file system entity they refer to can change at any time. A File object is not an open file; it is a handle to a path. Opening the file for reading or writing requires creating an InputStream, OutputStream, or Reader/Writer with the File as the source. File construction accepts absolute paths ("/home/user/data.txt"), relative paths ("data.txt", "subdir/data.txt"), parent/child combinations (new File("/home/user", "data.txt") or new File(parentFile, "data.txt")), and URI (new File(URI)). Path separators are platform-specific: File.separator is "/" on Unix/macOS and "\" on Windows. However, "/" works on Windows in most contexts, making forward slashes reasonably portable in practice. File.separatorChar and File.pathSeparator (the separator between paths in a list, ":" on Unix, ";" on Windows) are useful for constructing paths programmatically. The query methods tell you about the current state of the file system entry: exists() tests existence, isFile() tests that it is a regular file (not a directory or symlink), isDirectory() tests that it is a directory, length() returns the file size in bytes, lastModified() returns the last modification time as a long (milliseconds since epoch), canRead(), canWrite(), canExecute() test permissions. All these methods return false or 0 rather than throwing exceptions when the file does not exist or when an I/O error occurs — making it impossible to distinguish "file does not exist" from "I/O error during the check" when these methods return false. Mutation methods create and delete files and directories: createNewFile() creates the file and returns true if it was created, false if it already exists; delete() deletes the file or empty directory and returns true on success, false otherwise (whether because it didn't exist, wasn't empty, or a permissions error); mkdir() creates a directory; mkdirs() creates the directory and all missing parent directories; renameTo(File dest) renames or moves the file. All of these return boolean instead of throwing exceptions, making silent failure the default. list() and listFiles() return the contents of a directory. list() returns String[] of names; listFiles() returns File[] of child File objects. Both return null (not an empty array) if the path is not a directory or if an I/O error occurs — a null check is mandatory. Both load the entire directory listing into memory, which is a problem for directories with millions of entries. Neither supports filtering directly in list() without a FilenameFilter or FileFilter argument. The NIO.2 equivalents (Files.list(), Files.walk()) return lazy Stream<Path> and are dramatically superior for large directories.
Java
// ── 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

The java.nio.file package, specifically the Path interface and Files utility class, supersedes java.io.File for all new code. Path represents a file system path with proper exception-throwing semantics, symbolic link awareness, and integration with the Java file system provider model. Files contains over 60 static methods for all file system operations, all of which throw IOException (or its subclasses like NoSuchFileException, FileAlreadyExistsException, AccessDeniedException) rather than returning boolean. Path is created via Path.of(String first, String... more) (Java 11+) or Paths.get(String first, String... more) (Java 7+). Multiple path segments are joined with the platform separator: Path.of("/home", "user", "file.txt") produces /home/user/file.txt on Unix. Path.resolve(String or Path) appends a relative path to the current path. Path.relativize(Path other) computes the relative path from the current path to other. Path.normalize() removes ".." and "." components. Path.toAbsolutePath() makes the path absolute using the current working directory. The Files class provides methods that fall into five categories. Existence and metadata queries: Files.exists(Path, LinkOption...), Files.isRegularFile(), Files.isDirectory(), Files.size(), Files.getLastModifiedTime(), Files.getAttribute(), Files.readAttributes() (returns BasicFileAttributes, PosixFileAttributes, or DosFileAttributes). Creation and deletion: Files.createFile() (throws FileAlreadyExistsException if exists — correct behavior), Files.createDirectory(), Files.createDirectories(), Files.delete() (throws NoSuchFileException if not found), Files.deleteIfExists(), Files.createTempFile(), Files.createTempDirectory(). Copy and move: Files.copy() and Files.move() with StandardCopyOption options (REPLACE_EXISTING, COPY_ATTRIBUTES, ATOMIC_MOVE). Reading and writing content: Files.readAllBytes(), Files.readString(), Files.readAllLines(), Files.write(), Files.writeString(). Directory listing and traversal: Files.list() (single directory), Files.walk() (recursive, lazy stream), Files.find() (recursive with predicate), Files.walkFileTree() (full visitor API). Files.move() with StandardCopyOption.ATOMIC_MOVE performs an atomic rename-replace, which is the correct implementation of the write-to-temp-then-rename pattern for crash-safe file writes: write data to a temporary file, fsync the temp file, then atomically rename it to the destination. If the process crashes during the write, the destination file remains in its previous state — there is no partial write window. ATOMIC_MOVE throws AtomicMoveNotSupportedException if the source and destination are on different file systems (which would require a copy, which is inherently non-atomic).
Java
// ── 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(Path start, int maxDepth, FileVisitOption... options) returns a lazy Stream<Path> of all files and directories in the subtree rooted at start, in depth-first order. The stream is lazy — it reads directory entries on demand rather than loading the entire tree into memory — making it suitable for very large directory trees. The stream must be closed when done (use try-with-resources); not closing it leaks a DirectoryStream. Files.walk() follows symbolic links only if FileVisitOption.FOLLOW_LINKS is specified; without it, symbolic links are yielded as Path entries but not followed. When FOLLOW_LINKS is specified and a cycle is detected (a symlink that creates a circular reference), FileSystemLoopException is thrown. Files.find(Path start, int maxDepth, BiPredicate<Path, BasicFileAttributes> matcher, FileVisitOption...) combines walking with attribute-based filtering in a single efficient pass — reading directory entries and filtering in one operation, avoiding a separate call to Files.readAttributes() for each entry. This is more efficient than Files.walk().filter() when filtering by size, modification time, or file type, because find() passes the attributes that were already read during directory traversal. Files.walkFileTree(Path start, FileVisitor<? super Path> visitor) provides the full visitor pattern with callbacks for entering a directory (preVisitDirectory), visiting a file (visitFile), handling a file visit failure (visitFileFailed), and leaving a directory (postVisitDirectory). Each callback returns a FileVisitResult that controls traversal: CONTINUE, SKIP_SUBTREE (skip this directory's contents), SKIP_SIBLINGS (skip remaining entries at this level), or TERMINATE. walkFileTree is appropriate when you need precise control over traversal order, need to delete directory trees (must delete files before directories — postVisitDirectory), or need to handle individual errors without aborting the entire walk. WatchService monitors a directory for file system events (CREATE, MODIFY, DELETE) using OS-level notifications (inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows) rather than polling. A WatchKey is returned for each registered directory; take() blocks until an event occurs; pollEvents() retrieves the events without blocking. Each WatchEvent has a kind() (ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY) and a context() (the file name relative to the watched directory). WatchService is the correct implementation of build-tool file watching, live configuration reload, and incremental processing pipelines — polling via scheduled file existence/modification checks is always inferior.
Java
// ── 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

Related Topics in Java I/O

BufferedReader
BufferedReader wraps any Reader with an in-memory character buffer, dramatically reducing system calls for character-by-character or line-by-line reading. Its defining method is readLine(), which reads a complete line of text terminated by \n, \r, or \r\n and returns it without the terminator, or returns null at end-of-file. Beyond buffering, BufferedReader also exposes a lines() method (Java 8+) that returns a lazy Stream<String> of lines, enabling the full Stream API for file processing without loading the entire file into memory. BufferedReader supports mark/reset with a caller-specified readAheadLimit. It is obtained either by wrapping a Reader (new BufferedReader(new FileReader(...))) or directly from Files.newBufferedReader(path, charset), which is the preferred idiom in modern Java. This entry covers construction and buffer sizing, all read methods and their contracts, readLine() edge cases (empty lines, last line without terminator), the lines() stream and its relationship to reader lifecycle, mark/reset semantics with readAheadLimit, and the use of BufferedReader as a base for protocol parsing.
BufferedWriter
BufferedWriter wraps any Writer with an in-memory character buffer, reducing system calls by accumulating characters until the buffer fills, flush() is called, or close() is called. It adds two capabilities not present in Writer: newLine(), which writes the platform-specific line separator, and an optimized write(String, int, int) that avoids creating a char[] copy by writing directly from the String. BufferedWriter is the standard output partner to BufferedReader — together they provide efficient line-by-line text file processing. It is constructed either by wrapping a Writer (new BufferedWriter(new FileWriter(...))) or via Files.newBufferedWriter(path, charset, options), the modern idiomatic alternative. Like all buffered streams, correct usage requires try-with-resources to guarantee that buffered data is flushed and the file is closed even when exceptions occur. This entry covers construction and buffer sizing, all write methods and their interaction with the buffer, newLine() and its platform behavior, flush semantics including when explicit flush is necessary, the difference between close() and flush(), and performance patterns for high-throughput text writing.
PrintWriter
PrintWriter is a character-based output class that wraps any Writer or OutputStream and adds convenience methods for printing formatted representations of all Java primitive types, strings, and objects. Its defining characteristic is that none of its print(), println(), and printf() methods throw checked IOException — errors are silently swallowed and can only be detected after the fact by calling checkError(). This makes PrintWriter easy to use interactively and in situations where I/O failure is genuinely unrecoverable (writing to System.out, generating diagnostic output), but makes it dangerous for critical data writing where exceptions must be caught and handled. PrintWriter can auto-flush on println(), printf(), and format() calls when constructed with autoFlush=true, which is useful for interactive console output and network protocol streams. Its printf() and format() methods delegate to java.util.Formatter, enabling C-style formatted output with full locale awareness. This entry covers all constructor variants and their autoFlush and buffering behavior, every print/println/printf method, the checkError() error detection model, the difference between PrintWriter and PrintStream, charset handling, and when PrintWriter is the right choice versus BufferedWriter.
DataInputStream
DataInputStream wraps any InputStream and adds methods for reading Java primitive types in a machine-independent binary format. It reads boolean, byte, short, int, long, float, double, and char values from the underlying stream using fixed byte widths and big-endian byte order. The big-endian, fixed-width encoding is identical to the format written by DataOutputStream, making the two classes the natural pair for serializing and deserializing primitive data across files, network connections, or inter-process pipes. DataInputStream also provides readFully(), which blocks until exactly the specified number of bytes have been read — filling a buffer completely rather than returning a partial read as InputStream.read() may do. readUTF() reads a string encoded in a modified UTF-8 format (a two-byte length prefix followed by the encoded string bytes) that was written by DataOutputStream.writeUTF(). DataInputStream is unbuffered, so it should always be wrapped inside a BufferedInputStream for performance. This entry covers the full method API, the big-endian byte order contract, readFully() vs read() semantics, the modified UTF-8 format and its limitations, end-of-file detection, and composition patterns for binary protocol parsing.