File Handling

Java's Path and Files APIs give you a clean, modern way to create, read, write, inspect, move, and delete files and directories, replacing much of the older java.io.File-based approach.

File handling in modern Java centers on two cooperating types: java.nio.file.Path, which represents a location in a file system (without necessarily requiring that location to exist), and java.nio.file.Files, a utility class of static methods that perform actual operations — checking existence, reading, writing, copying, moving, deleting, and inspecting metadata — on the file or directory a Path refers to. This pairing, introduced as NIO.2 in Java 7, is generally preferred over the older java.io.File class for new code.

Path: Representing a Location

A Path is an abstract representation of a file-system location — a sequence of directory names optionally followed by a file name. Creating a Path does not touch the file system at all; it is purely a name/location object until you pass it to a Files method.

Path.of(...) is the modern factory (Java 11+); it accepts one or more path segments joined into a single path.

import java.nio.file.Path;

Path p1 = Path.of("data", "report.txt");      // relative path: data/report.txt
Path p2 = Path.of("/var/log/app.log");         // absolute path
Path p3 = Path.of("data").resolve("report.txt"); // same as p1

System.out.println(p1.getFileName()); // report.txt
System.out.println(p1.isAbsolute());  // false
System.out.println(p2.getParent());   // /var/log

Checking File Existence

Files.exists(path) and Files.notExists(path) check for existence, but note they are not perfect complements — both can return false if the file's existence cannot be verified (e.g. a permissions problem), so treat 'existence unknown' as a real third case in careful code.

import java.nio.file.Files;
import java.nio.file.Path;

Path file = Path.of("config.properties");
if (Files.exists(file)) {
    System.out.println("found it");
} else {
    System.out.println("missing");
}
System.out.println(Files.isRegularFile(file));
System.out.println(Files.isDirectory(file));
System.out.println(Files.isReadable(file));
System.out.println(Files.isWritable(file));

Creating Files and Directories

Files.createFile(path) creates a new, empty regular file (and throws FileAlreadyExistsException if one already exists at that path); Files.createDirectory(path) creates a single directory (its parent must already exist); Files.createDirectories(path) creates any missing parent directories along the way as well, and does not fail if the target directory already exists.

Path dir = Path.of("output/reports/2026");
Files.createDirectories(dir);        // creates output/, reports/, and 2026/ as needed

Path newFile = dir.resolve("summary.txt");
if (!Files.exists(newFile)) {
    Files.createFile(newFile);
}

Reading Files

For small-to-moderate text files, Files.readString(path) (Java 11+) reads the entire file into one String, and Files.readAllLines(path) reads it as a List<String>. For large files that should not be fully loaded into memory at once, Files.lines(path) returns a lazily-read Stream<String> that must be closed (ideally with try-with-resources) once you're done.

Files.lines() streams the file lazily and must be closed; readString/readAllLines load the whole file eagerly and need no explicit closing.

Path file = Path.of("data.txt");
String whole = Files.readString(file);
List<String> lines = Files.readAllLines(file);

try (Stream<String> lineStream = Files.lines(file)) {
    long count = lineStream.filter(l -> l.contains("ERROR")).count();
    System.out.println("error lines: " + count);
}

Writing Files

Files.writeString(path, content, options...) (Java 11+) writes a String to a file, and Files.write(path, bytesOrLines, options...) writes raw bytes or a list of lines. By default these create the file if absent and truncate/overwrite existing content; pass StandardOpenOption.APPEND to append instead, and StandardOpenOption.CREATE_NEW to fail if the file already exists.

import java.nio.file.StandardOpenOption;

Path log = Path.of("app.log");
Files.writeString(log, "started\n", StandardOpenOption.CREATE, StandardOpenOption.APPEND);
Files.writeString(log, "processing\n", StandardOpenOption.APPEND);

List<String> lines = List.of("a", "b", "c");
Files.write(Path.of("list.txt"), lines); // overwrites list.txt with these 3 lines

Deleting Files

Files.delete(path) deletes a file or empty directory, throwing NoSuchFileException if it doesn't exist and DirectoryNotEmptyException for a non-empty directory. Files.deleteIfExists(path) does the same but silently returns false instead of throwing when the path doesn't exist — useful for idempotent cleanup code.

Files.deleteIfExists(Path.of("temp.txt"));  // no exception even if it never existed

try {
    Files.delete(Path.of("config.txt"));
} catch (java.nio.file.NoSuchFileException e) {
    System.out.println("already gone");
}

Working with Directories

Files.list(dir) returns a lazily-populated Stream<Path> of the direct (non-recursive) entries of a directory and must be closed; Files.walk(dir) recursively walks an entire directory tree and also returns a Stream<Path> that must be closed. Both throw IOException-derived exceptions if the target isn't a readable directory.

Both Files.list and Files.walk hold an underlying directory-stream resource open until closed — always use try-with-resources.

try (Stream<Path> entries = Files.list(Path.of("."))) {
    entries.filter(Files::isRegularFile).forEach(System.out::println);
}

try (Stream<Path> all = Files.walk(Path.of("src"))) {
    long javaFiles = all.filter(p -> p.toString().endsWith(".java")).count();
    System.out.println(javaFiles);
}

Reading Metadata

Files.size(path) returns file size in bytes; Files.getLastModifiedTime(path) returns a FileTime; Files.readAttributes(path, BasicFileAttributes.class) retrieves a bundle of common metadata (size, timestamps, whether it's a directory/regular file/symbolic link) in a single call, which is more efficient than several separate calls when you need multiple attributes.

import java.nio.file.attribute.BasicFileAttributes;

Path p = Path.of("report.txt");
BasicFileAttributes attrs = Files.readAttributes(p, BasicFileAttributes.class);
System.out.println("size: " + attrs.size());
System.out.println("created: " + attrs.creationTime());
System.out.println("modified: " + attrs.lastModifiedTime());
System.out.println("is directory: " + attrs.isDirectory());

Copying and Moving

Files.copy(source, target, options...) copies a file (or, for a directory, creates only the directory itself, not its contents recursively — recursive copy requires walking the tree yourself). Files.move(source, target, options...) moves/renames a file or directory. Both accept StandardCopyOption.REPLACE_EXISTING to overwrite an existing target, and Files.move accepts StandardCopyOption.ATOMIC_MOVE on file systems that support atomic renames.

import java.nio.file.StandardCopyOption;

Files.copy(Path.of("source.txt"), Path.of("backup.txt"), StandardCopyOption.REPLACE_EXISTING);
Files.move(Path.of("draft.txt"), Path.of("final.txt"), StandardCopyOption.REPLACE_EXISTING);

Exceptions in File Handling

Nearly every Files method declares IOException (a checked exception), reflecting that file-system operations can fail for reasons outside the program's control (missing files, permission errors, disk full, concurrent deletion by another process). Common specific subclasses include NoSuchFileException, FileAlreadyExistsException, AccessDeniedException, and DirectoryNotEmptyException — catching the specific subclass, or at least IOException, lets you respond appropriately rather than letting the program crash on an ordinary, expected file-system condition.

try {
    Files.createFile(Path.of("lock.txt"));
} catch (java.nio.file.FileAlreadyExistsException e) {
    System.out.println("lock already held");
} catch (IOException e) {
    System.out.println("unexpected I/O failure: " + e.getMessage());
}

Practical Usage

A typical workflow: ensure an output directory exists (createDirectories), check whether an expected input file is present before processing (exists), read its content (readString or lines), process it, then write results to a new file (writeString) and optionally clean up temporary files (deleteIfExists) — always wrapping stream-returning calls (lines, list, walk) in try-with-resources.

Key Takeaways

Path represents a file-system location abstractly; creating one performs no I/O and requires no existing file.

Files provides the actual operations — existence checks, create/read/write/delete, metadata, copy/move — as static methods operating on a Path.

Stream-returning methods (Files.lines, Files.list, Files.walk) hold an open resource and must be closed, ideally via try-with-resources.

Files.copy on a directory is shallow (creates only the directory); recursive copying requires walking the tree yourself.

Most Files methods declare checked IOException (or a specific subclass) because file-system failures are common and expected, not exceptional bugs.

Files.deleteIfExists and Files.createDirectories are the idempotent-friendly variants for cleanup/setup code.

Stream<String> lines = Files.lines(path); lines.forEach(System.out::println); // file handle leaked - never closed try (Stream<String> lines = Files.lines(path)) { lines.forEach(System.out::println); } // automatically closed

Files.lines() (like Files.list() and Files.walk()) opens an underlying file-system resource that is only released when the stream is closed; without try-with-resources, the handle leaks.

Files.delete(Path.of("temp.txt")); // throws NoSuchFileException if it doesn't exist, crashing cleanup code

Files.deleteIfExists(Path.of("temp.txt")); // returns false instead of throwing when absent

For idempotent cleanup where the file may or may not exist, deleteIfExists avoids an unnecessary exception-handling branch for the common 'already gone' case.

Files.copy(sourceDir, targetDir); // expecting a full recursive directory copy try (Stream<Path> walk = Files.walk(sourceDir)) { walk.forEach(src -> { try { Path dst = targetDir.resolve(sourceDir.relativize(src)); if (Files.isDirectory(src)) Files.createDirectories(dst); else Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { throw new UncheckedIOException(e); } }); }

Files.copy on a directory only creates that one directory (not recursively) — a full recursive tree copy must be implemented by walking the source tree yourself.

Does creating a Path object touch the file system?

No. A Path is a purely abstract representation of a location; it performs no I/O and does not require the file/directory to actually exist until it is passed to a Files method that performs a real operation.

What is the difference between Files.createDirectory and Files.createDirectories?

createDirectory creates a single directory and requires its parent to already exist (throwing an exception otherwise). createDirectories creates any missing parent directories along the way as well, and does not fail if the target directory already exists.

Why must Files.lines(), Files.list(), and Files.walk() be used with try-with-resources?

They return a lazily-populated Stream backed by an open file-system resource (a directory stream or file handle) that is only released when the stream is closed; failing to close it leaks that resource.

What is a key limitation of Files.copy() when the source is a directory?

It only creates the target directory itself (a shallow copy) — it does not recursively copy the directory's contents. A full recursive copy requires walking the source tree (e.g. with Files.walk) and copying each entry individually.

Why do most Files methods declare a checked IOException rather than an unchecked exception?

File-system operations can fail for many expected, external reasons outside program control — missing files, permission issues, disk space, concurrent modification by another process — and Java's checked-exception design forces calling code to explicitly handle or declare these expected failure modes.

What is the practical difference between Files.delete() and Files.deleteIfExists()?

Files.delete() throws NoSuchFileException if the target doesn't exist. Files.deleteIfExists() instead returns false in that case without throwing, which is more convenient for idempotent cleanup code where the file's prior existence is uncertain or unimportant.

Why might Files.exists() and Files.notExists() both return false for the same path?

Both methods can return false when the file's existence cannot be determined (e.g. due to a permissions/access problem) rather than being strict logical opposites — 'existence unknown' is effectively a third possible outcome that careful code should account for.

Write a program that ensures a 'logs' directory exists, then appends a timestamped line to logs/app.log.

import java.nio.file.*; import java.time.LocalDateTime; public class LogAppender { public static void main(String[] args) throws Exception { Path logDir = Path.of("logs"); Files.createDirectories(logDir); Path logFile = logDir.resolve("app.log"); String line = LocalDateTime.now() + " - application started\n"; Files.writeString(logFile, line, StandardOpenOption.CREATE, StandardOpenOption.APPEND); } } createDirectories() ensures logs/ exists (creating it and any missing parents, doing nothing if it already exists); writeString with CREATE and APPEND creates app.log if absent and appends to it otherwise, rather than overwriting existing log content.

What does this program print, assuming notes.txt does not already exist?

It prints 'hello world'. The file is created empty, then "hello" is appended, then " world" is appended, so readString() returns the concatenation of both appends: 'hello world'.

Write a program that counts how many .txt files exist directly inside a given directory (non-recursive).

import java.nio.file.*; import java.util.stream.Stream; public class TxtCounter { public static void main(String[] args) throws Exception { Path dir = Path.of("."); long count; try (Stream<Path> entries = Files.list(dir)) { count = entries .filter(Files::isRegularFile) .filter(p -> p.toString().endsWith(".txt")) .count(); } System.out.println("txt files: " + count); } }

Files.list(dir) yields only the direct entries of the directory (no recursion into subdirectories), filtered to regular files whose name ends in '.txt'; the try-with-resources block ensures the underlying directory stream is closed once counting finishes.

Path p = Path.of("notes.txt");
if (Files.notExists(p)) {
    Files.createFile(p);
}
Files.writeString(p, "hello", StandardOpenOption.APPEND);
Files.writeString(p, " world", StandardOpenOption.APPEND);
System.out.println(Files.readString(p));