☕ 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

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 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.