☕ 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

I/O Basics
Java I/O is built on a small set of abstract concepts that underlie every I/O operation in the language: streams, readers, writers, channels, and buffers. A stream is a sequential flow of data — bytes moving from a source to a destination one at a time or in chunks. Java organizes I/O around two fundamental distinctions: byte I/O (reading and writing raw bytes, the universal representation that everything ultimately reduces to) and character I/O (reading and writing text encoded in a specific character set, with automatic encoding and decoding). The original java.io package, introduced in Java 1.0, provides stream-based I/O through four abstract base classes: InputStream, OutputStream, Reader, and Writer. The java.nio package, introduced in Java 1.4, adds a channel-and-buffer model for non-blocking and memory-mapped I/O. The java.nio.file package, introduced in Java 7 as part of NIO.2, provides a modern, comprehensive file system API that supersedes much of java.io.File. This entry covers the conceptual model of streams and their abstract base classes, the decorator pattern that underlies Java I/O class hierarchy, the source-processor-sink taxonomy of stream classes, blocking versus non-blocking I/O, buffering and why it is almost always necessary, the standard I/O streams (System.in, System.out, System.err), and the resource management contract that every I/O class must satisfy.
Byte Streams
Byte streams are the fundamental I/O abstraction in Java for reading and writing raw binary data. InputStream and OutputStream are the abstract base classes for all byte-oriented I/O, and their concrete subclasses cover every byte-level data source and destination: files, byte arrays in memory, network sockets, pipes between threads, and process standard streams. The critical read() contract — returning an int from 0 to 255 for valid bytes and -1 for end-of-stream — is the foundation of all stream-based binary processing. Byte streams do not perform character encoding or decoding; every byte is passed through as-is, making them correct for binary formats (images, audio, archives, serialized data, protocol buffers), and incorrect for text unless the encoding is explicitly managed. This entry covers the complete InputStream and OutputStream APIs, every major concrete byte stream class and its use case, DataInputStream and DataOutputStream for structured binary I/O, the mark/reset mechanism, available() and its correct interpretation, skipping and transferTo, and ObjectInputStream and ObjectOutputStream for Java serialization.
Character Streams
Character streams, represented by the Reader and Writer abstract base classes, handle text data by abstracting away the encoding and decoding between Java's internal char/String representation (UTF-16) and the byte encoding used in files and network connections. Where byte streams treat data as raw octets, character streams treat data as Unicode characters, handling multi-byte sequences transparently according to a specified Charset. InputStreamReader and OutputStreamWriter are the bridge classes that connect byte streams to character streams, applying charset encoding on write and decoding on read. BufferedReader adds line-at-a-time reading via readLine() and multi-character buffering. PrintWriter adds print/println/printf formatting output. StringReader and StringWriter enable in-memory character stream operations on String data. This entry covers the complete Reader and Writer APIs, charset handling and the consequences of using the wrong charset, the complete class hierarchy of character streams with the use case for each, BufferedReader.readLine() semantics and the lines() stream, the bridge classes in depth, character encoding best practices, and the interaction between character streams and Java's String.lines() and Files.readString()/writeString() alternatives.
File Class
The java.io.File class is Java's original file system abstraction, present since Java 1.0. A File object represents an abstract pathname — a string denoting a file or directory that may or may not exist on the file system. File objects are immutable: once constructed, the path string they represent never changes. The class provides a comprehensive set of methods for path manipulation, file system queries, directory operations, and file creation and deletion. File served as the primary file system API for 17 years until NIO.2's Path and Files classes superseded it in Java 7. Understanding File is essential for reading existing Java codebases, working with older APIs that accept File parameters, and understanding why NIO.2 was designed the way it was. This entry covers the complete File API in depth: all constructor forms and path semantics, every query and mutation method with its exact return and failure semantics, the listFiles() filtering API, path resolution and relative path handling, platform-specific behavior differences, the interoperability bridge between File and Path, and a precise catalog of File's deficiencies that motivated NIO.2.