☕ Java

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.

Construction, Path Semantics, and Navigation Methods

File has four constructors: File(String pathname) creates a File from a path string; File(String parent, String child) creates a File from parent path and child name; File(File parent, String child) creates from a parent File and child name; File(URI uri) creates from a file:// URI. All forms accept both absolute and relative paths. Relative paths are interpreted relative to the current working directory, which is the value of System.getProperty("user.dir") at the time the JVM was started — it cannot be changed by Java code on most platforms. The path separator (/ or \) is platform-specific, but Java accepts / on Windows for most operations. The naming methods expose the components of the path string. getName() returns only the final component (the filename or last directory name). getParent() returns the parent path string, or null if the path has no parent component (a bare filename with no directory). getPath() returns the path string as passed to the constructor (possibly relative). getAbsolutePath() returns the absolute path by resolving the path against the current working directory if relative — it does not access the file system and does not resolve symbolic links. getCanonicalPath() resolves the absolute path AND resolves all symbolic links and ./ and ../ components, accessing the file system; it throws IOException and is the only naming method that can fail. Path navigation is done via resolve-like operations on File objects. new File(file, "subdir") creates a child path; getParentFile() returns the parent as a File object. There is no equivalent of Path.resolve() or Path.relativize() — you must construct child paths manually. The isAbsolute() method tests whether the path is absolute. On Unix, absolute paths start with /. On Windows, absolute paths start with a drive letter and colon (C:) or a UNC path (\\server\share). A path that starts with on Windows (like WindowsSystem32) is drive-relative but not truly absolute according to isAbsolute() — it refers to the root of the current drive. This Windows-specific nuance frequently surprises developers writing cross-platform code. Comparing File objects: equals() compares path strings using the rules of the underlying OS — case-insensitive on Windows (using String.equalsIgnoreCase()), case-sensitive on Unix. compareTo() follows the same convention. Two File objects referring to the same physical file through different paths (one absolute, one relative; or one with and one without a trailing slash; or one through a symlink) are not equal. Files.isSameFile(Path, Path) in NIO.2 compares the actual underlying file system entry, not the path strings.
Java
// ── All four constructors ────────────────────────────────────────────
File f1 = new File("/home/user/data.txt");                     // absolute path
File f2 = new File("data.txt");                                // relative to CWD
File f3 = new File("subdir", "data.txt");                      // parent string + child
File f4 = new File(new File("/home/user"), "data.txt");        // parent File + child
File f5 = new File(URI.create("file:///home/user/data.txt"));  // from URI

// All path separators:
System.out.println(File.separator);      // "/" on Unix, "\" on Windows
System.out.println(File.separatorChar);  // '/' or '\'
System.out.println(File.pathSeparator);  // ":" on Unix, ";" on Windows (for PATH lists)

// Forward slash works on Windows too:
File winFile = new File("C:/Users/user/data.txt");  // works on Windows

// ── Naming methods ─────────────────────────────────────────────────────
File f = new File("/home/user/documents/report.txt");

System.out.println(f.getName());           // report.txt
System.out.println(f.getParent());         // /home/user/documents
System.out.println(f.getParentFile());     // File for /home/user/documents
System.out.println(f.getPath());           // /home/user/documents/report.txt (as constructed)
System.out.println(f.isAbsolute());        // true
System.out.println(f.getAbsolutePath());   // same (already absolute)

// Relative path — getAbsolutePath() prepends CWD:
File rel = new File("data.txt");
System.out.println(rel.getPath());          // data.txt
System.out.println(rel.getAbsolutePath()); // /current/working/dir/data.txt
System.out.println(rel.isAbsolute());       // false

// getCanonicalPath — filesystem access, resolves symlinks:
try {
    File symlink = new File("/home/user/link");  // symlink → /data/actual
    System.out.println(symlink.getAbsolutePath());  // /home/user/link (path string)
    System.out.println(symlink.getCanonicalPath()); // /data/actual (resolved)
} catch (IOException e) { /* I/O error — only method that can throw */ }

// With ../ and ./:
try {
    File dotdot = new File("/home/user/../user/./documents");
    System.out.println(dotdot.getAbsolutePath());  // /home/user/../user/./documents
    System.out.println(dotdot.getCanonicalPath()); // /home/user/documents
} catch (IOException e) { }

// ── Path navigation without Path.resolve() ────────────────────────────
File base   = new File("/home/user");
File child  = new File(base, "documents");       // /home/user/documents
File grandchild = new File(child, "report.txt"); // /home/user/documents/report.txt

System.out.println(grandchild.getParentFile().equals(child));  // true
System.out.println(grandchild.getParent());   // /home/user/documents

// ── equals() — path string comparison, not file system identity ──────
File a = new File("/home/user/./data.txt");
File b = new File("/home/user/data.txt");
System.out.println(a.equals(b));   // false — different path strings, same file
// Use Files.isSameFile() for true identity comparison:
System.out.println(Files.isSameFile(a.toPath(), b.toPath()));   // true

// Case sensitivity:
File upper = new File("/tmp/FILE.txt");
File lower = new File("/tmp/file.txt");
System.out.println(upper.equals(lower));  // false on Unix (case-sensitive)
                                          // true on Windows (case-insensitive)

// ── toPath() — bridge to NIO.2 ────────────────────────────────────────
File legacyFile = new File("/home/user/data.txt");
Path modernPath = legacyFile.toPath();            // convert to Path
File backToFile = modernPath.toFile();            // convert back

// Prefer Path.of() for new code:
Path path = Path.of("/home/user/data.txt");       // equivalent, modern

Query Methods, Mutation Methods, and the Boolean-Return Problem

File provides a complete set of file system query methods that return metadata about the file system entry at the path. exists() tests whether anything (file, directory, symlink) exists at the path. isFile() tests that the entry is a regular file — returns false for directories, symlinks, device files, and pipes. isDirectory() tests that the entry is a directory. length() returns the size in bytes for a regular file, 0 for directories (not the directory's content size), and 0 for non-existent files. lastModified() returns the last modification time as milliseconds since the Unix epoch, or 0 if the file does not exist. canRead(), canWrite(), canExecute() test the JVM process's access to the file based on OS permissions. The critical limitation of all these methods is that they return false or 0 on any failure: file does not exist, permission denied, I/O error, filesystem not mounted. There is no mechanism to distinguish these cases. In NIO.2, Files.exists() returns false only for non-existence and for IOExceptions (including permission denied); the other query methods throw the appropriate IOException. For the same ambiguity problem with canRead()/canWrite(), NIO.2 uses Files.isReadable()/Files.isWritable() which also just return boolean, but are honest about following the same convention — the difference is that NIO.2 lets you read the actual attributes when you need precision. The mutation methods create and delete files and directories and all return boolean: createNewFile() returns true if the file was created, false if it already existed, and throws IOException on I/O errors. delete() returns true if the file was deleted, false if it wasn't (because it didn't exist, wasn't empty, was locked, or permissions denied). mkdir() creates a single directory; mkdirs() creates the directory and all missing parents. renameTo() renames or moves the file; it works atomically on the same filesystem on Unix but may fail silently on Windows when the target is locked or across filesystems on any platform. setLastModified(long time) sets the modification time. setReadOnly() makes the file read-only. setWritable(boolean writable), setReadable(boolean readable), setExecutable(boolean executable) — Java 6 additions — control permissions. These are all best-effort and return boolean. The static getTempDir() (via System.getProperty("java.io.tmpdir")), listRoots() (returns the filesystem roots — drive letters on Windows, just "/" on Unix), and getFreeSpace()/getTotalSpace()/getUsableSpace() provide system-level information. createTempFile(String prefix, String suffix) and createTempFile(String prefix, String suffix, File directory) are static factory methods that create unique temporary files in the system temp directory or a specified directory.
Java
// ── Complete query method reference ──────────────────────────────────
File f = new File("/home/user/example.txt");

// Existence — all return false for any failure (existence, permissions, I/O error):
System.out.println(f.exists());          // any type of file system entry
System.out.println(f.isFile());          // regular file only
System.out.println(f.isDirectory());     // directory only
System.out.println(f.isHidden());        // starts with '.' on Unix; hidden attr on Windows
System.out.println(f.isAbsolute());      // path is absolute

// Size and timing — return 0 if doesn't exist or on error:
System.out.println(f.length());          // bytes; 0 for directories
System.out.println(f.lastModified());    // ms since epoch; 0 if doesn't exist

// Permission checks — may not reflect actual OS permissions accurately on all platforms:
System.out.println(f.canRead());
System.out.println(f.canWrite());
System.out.println(f.canExecute());

// ── Mutation methods — check return values, they're your only error signal ──
// createNewFile: creates if absent, false if exists, IOException on error:
File newFile = new File("/tmp/atomic-test.txt");
try {
    boolean createdNow = newFile.createNewFile();
    if (createdNow) {
        System.out.println("Created new file");
    } else {
        System.out.println("File already existed");
    }
} catch (IOException e) {
    System.err.println("I/O error: " + e.getMessage());
}

// delete: returns false silently on any failure (doesn't exist, not empty, locked):
boolean deleted = newFile.delete();
if (!deleted) {
    // Distinguish reason manually — File gives no clue:
    if (!newFile.exists()) {
        System.out.println("Was already gone");
    } else {
        System.out.println("Delete failed: locked? not empty? permissions?");
        // NIO.2 Files.delete() would tell you which one
    }
}

// mkdir vs mkdirs:
File single  = new File("/tmp/single");
File nested  = new File("/tmp/a/b/c/d");

boolean singleMade = single.mkdir();    // fails if /tmp doesn't exist? No, /tmp always exists
boolean nestedMade = nested.mkdirs();   // creates /tmp/a, /tmp/a/b, /tmp/a/b/c, /tmp/a/b/c/d
System.out.println("Single: " + singleMade);   // true
System.out.println("Nested: " + nestedMade);   // true

// renameTo: non-atomic on Windows, may fail across filesystems:
File source = new File("/tmp/original.txt");
File dest   = new File("/tmp/renamed.txt");
source.createNewFile();
boolean renamed = source.renameTo(dest);
if (!renamed) {
    System.err.println("renameTo failed — use Files.move() for reliable semantics");
}

// ── setters — permission and timestamp control ─────────────────────────
File controlled = new File("/tmp/controlled.txt");
controlled.createNewFile();

controlled.setReadOnly();                         // make read-only
controlled.setWritable(true);                     // re-enable writing
controlled.setWritable(false, false);             // false for owner only too
controlled.setExecutable(true, false);            // executable for all users
controlled.setLastModified(System.currentTimeMillis() - 86_400_000L);  // set to yesterday

// ── Static methods ────────────────────────────────────────────────────
// Filesystem roots:
for (File root : File.listRoots()) {
    System.out.printf("Root: %s total=%.1fGB free=%.1fGB usable=%.1fGB%n",
        root,
        root.getTotalSpace()  / 1e9,
        root.getFreeSpace()   / 1e9,
        root.getUsableSpace() / 1e9   // may differ from freeSpace due to quotas
    );
}
// On Unix:   Root: /  total=500.0GB free=350.0GB usable=340.0GB
// On Windows: Root: C:  ... D:  ...

// Temp directory:
System.out.println(System.getProperty("java.io.tmpdir"));  // /tmp on Unix, C:Temp on Windows

// Static createTempFile:
File tmpFile = File.createTempFile("myapp-", ".tmp");
// Creates /tmp/myapp-1234567890.tmp (unique name guaranteed)
System.out.println("Temp: " + tmpFile.getAbsolutePath());
tmpFile.deleteOnExit();   // register for cleanup (best-effort)

File tmpInDir = File.createTempFile("cache-", ".dat", new File("/home/user/cache"));

Directory Listing, FileFilter, FilenameFilter, and File Deficiencies

File provides three directory listing methods. list() returns String[] of names of direct children (not recursive). listFiles() returns File[] of child File objects. Both return null if the path is not a directory or if an I/O error occurs — null checking is mandatory and easily overlooked. Filtering is provided by passing a FilenameFilter (a functional interface accepting a File directory and String name) or a FileFilter (accepting a File) to list() or listFiles(). Neither accepts lambda expressions directly in Java 6, but since both are functional interfaces, lambda syntax works in Java 8+. A critical behavioral detail: list() and listFiles() do not guarantee any ordering of results. On most file systems the order is inode or creation order, which is neither alphabetical nor predictable across platforms. Code that depends on list() returning files in any particular order must sort the result explicitly. NTFS returns entries in Unicode alphabetical order; ext4 returns them in hash table order; FAT32 returns them in creation order. This ordering difference is a frequent source of test failures when a test written on macOS (which sorts) is run on Linux (which doesn't). File has no recursive listing capability — listing all files in a directory tree requires writing a recursive method or using an external library. NIO.2's Files.walk() solves this with a lazy Stream<Path>. The deficiencies of File that motivated NIO.2 are precise and important to understand: no symbolic link support (there is no isSymbolicLink(), and all symlink-following behavior is implicit), boolean-returning mutation methods with no exception on failure, no atomic operations (no create-or-fail, no move-with-failure-semantics), no directory stream for large directories (listFiles() loads everything into an array), no file attribute support beyond the basic legacy attributes, no file watching, no file store information per file (which disk is it on?), and no pluggable provider model (cannot implement a custom FileSystem). NIO.2 addresses all of these deficiencies. New code should use NIO.2; File should only be used when interfacing with legacy APIs that require it, using toPath() to convert immediately.
Java
// ── list() and listFiles() — null safety is mandatory ────────────────
File dir = new File("/home/user");

// list() returns String[] — null on error or non-directory:
String[] names = dir.list();
if (names == null) throw new IOException("Cannot list: " + dir);
// Sort explicitly — order is NOT guaranteed:
Arrays.sort(names);
for (String name : names) System.out.println(name);

// listFiles() returns File[] — null on error or non-directory:
File[] files = dir.listFiles();
if (files == null) throw new IOException("Cannot list: " + dir);
Arrays.sort(files, Comparator.comparing(File::getName));  // sort by name

// ── FilenameFilter and FileFilter — lambda-friendly in Java 8+ ────────
// FilenameFilter: accepts (File parent, String filename):
FilenameFilter javaFilter = (parent, name) -> name.endsWith(".java");
String[] javaNames = dir.list(javaFilter);

// FileFilter: accepts (File pathname):
FileFilter largeFileFilter = f -> f.isFile() && f.length() > 1_000_000;
File[] largeFiles = dir.listFiles(largeFileFilter);

// Combined: list only large Java files:
File[] largeJava = dir.listFiles(
    f -> f.isFile() && f.getName().endsWith(".java") && f.length() > 100_000
);

// ── Recursive listing — manually (no built-in support in File) ────────
List<File> findAllJavaFiles(File directory) {
    List<File> result = new ArrayList<>();
    File[] children = directory.listFiles();
    if (children == null) return result;   // not a directory or I/O error

    for (File child : children) {
        if (child.isDirectory()) {
            result.addAll(findAllJavaFiles(child));   // recurse
        } else if (child.getName().endsWith(".java")) {
            result.add(child);
        }
    }
    return result;
}

// NIO.2 equivalent — lazy, handles large trees, fewer lines:
List<Path> javaFiles = Files.walk(dir.toPath())
    .filter(p -> p.toString().endsWith(".java"))
    .collect(Collectors.toList());

// ── File deficiencies catalog — why NIO.2 was needed ─────────────────
// 1. No symbolic link detection:
File symlink = new File("/home/user/link");
symlink.isFile();        // true if symlink points to a file — cannot distinguish!
// NIO.2: Files.isSymbolicLink(path) → correct answer

// 2. Boolean returns — no exception on failure:
File file = new File("/read-only-dir/file.txt");
file.delete();   // false — is it permissions? Doesn't exist? Locked? Unknown.
// NIO.2: Files.delete(path) → throws AccessDeniedException, NoSuchFileException etc.

// 3. No atomic create-or-fail:
// createNewFile() returns false if exists — that's OK, but...
// Between exists() check and createNewFile(), another process could create it
// NIO.2: Files.createFile() throws FileAlreadyExistsException — atomic

// 4. No atomic move across potential rename race:
file.renameTo(new File("/other/path/file.txt"));  // fails silently across filesystems
// NIO.2: Files.move(src, dst, ATOMIC_MOVE) → throws if not atomic

// 5. No large directory streaming:
File[] all = hugeDirWithMillions.listFiles();   // loads ALL entries into array → OOM
// NIO.2: Files.list(path) → lazy Stream<Path>

// 6. No rich attributes:
file.lastModified();    // ms since epoch — that's all
// NIO.2: Files.readAttributes(path, BasicFileAttributes.class) → full metadata

// 7. No file watching:
// Must poll using lastModified() comparisons — wastes CPU, misses events
// NIO.2: WatchService → OS-level push notifications

// ── The bridge: always convert legacy File to Path immediately ────────
void processLegacyApi(File file) {
    Path path = file.toPath();     // immediately convert
    // Now use all NIO.2 capabilities:
    try {
        BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
        System.out.println("Size: " + attrs.size());
        System.out.println("Is symlink: " + attrs.isSymbolicLink());
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
}

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.