Java · lesson 63 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11Java NIO.2: Paths, Files, and WatchService
Learn NIO.2 by running it: Path operations, reading and writing with Files, streaming lines, copy and move, walking trees, attributes and WatchService.
By SkillByExample · Updated
NIO.2 (Java 7+) models a file location as an immutable Path and puts every operation in the static
Files class. Every example is a single file run with the JDK 21 source launcher
(java File.java) from an empty directory, using relative paths so the output is the same on every
machine.
Building and manipulating paths
A Path is just a value — creating one does not touch the disk.
import java.nio.file.Path;
public class Paths101 {
public static void main(String[] args) {
Path base = Path.of("projects", "app");
Path file = base.resolve("src/Main.java");
System.out.println(file);
System.out.println("name: " + file.getFileName());
System.out.println("parent: " + file.getParent());
System.out.println("parts: " + file.getNameCount());
Path messy = Path.of("projects/app/src/../lib/./util.jar");
System.out.println("normal: " + messy.normalize());
System.out.println("rel: " + file.relativize(Path.of("projects/app/lib/util.jar")));
System.out.println("exists? " + java.nio.file.Files.exists(file));
}
}
$ java Paths101.java
projects/app/src/Main.java
name: Main.java
parent: projects/app/src
parts: 4
normal: projects/app/lib/util.jar
rel: ../../lib/util.jar
exists? false
resolve appends, normalize removes . and .. segments, and relativize computes the route
from one path to another. Only the last line asks the file system anything — and the path does not
exist.
Writing and reading whole files
Files.writeString, readString, write and readAllLines cover small files in one call each.
import java.nio.file.*;
import java.util.List;
public class ReadWrite {
public static void main(String[] args) throws Exception {
Path notes = Path.of("notes.txt");
Files.writeString(notes, "first line\n");
Files.writeString(notes, "second line\n", StandardOpenOption.APPEND);
Files.write(notes, List.of("third", "fourth"), StandardOpenOption.APPEND);
System.out.print(Files.readString(notes));
List<String> lines = Files.readAllLines(notes);
System.out.println(lines.size() + " lines, last = " + lines.get(lines.size() - 1));
System.out.println(Files.size(notes) + " bytes");
}
}
$ java ReadWrite.java
first line
second line
third
fourth
4 lines, last = fourth
36 bytes
Without APPEND, writeString creates or truncates the file. Both methods use UTF-8 unless you
pass a Charset, unlike the old FileReader, which used the platform default before Java 18.
Streaming large files with Files.lines
Files.lines reads lazily, one line at a time. It holds the file open, so close it.
import java.nio.file.*;
import java.util.stream.*;
public class LogScan {
public static void main(String[] args) throws Exception {
Path log = Path.of("app.log");
Files.write(log, IntStream.rangeClosed(1, 1000)
.mapToObj(i -> (i % 250 == 0 ? "ERROR " : "INFO ") + "request " + i)
.toList());
try (Stream<String> lines = Files.lines(log)) {
lines.filter(l -> l.startsWith("ERROR"))
.forEach(System.out::println);
}
try (Stream<String> lines = Files.lines(log)) {
System.out.println("total lines: " + lines.count());
}
}
}
$ java LogScan.java
ERROR request 250
ERROR request 500
ERROR request 750
ERROR request 1000
total lines: 1000
Only one line needs to be in memory at a time, so the same code works on a file far larger than the heap. A stream can be consumed once, which is why counting opens the file again.
Copying, moving and deleting
Files operations fail loudly with a specific exception instead of returning false.
import java.nio.file.*;
public class CopyMove {
public static void main(String[] args) throws Exception {
Path src = Files.writeString(Path.of("report.txt"), "Q3 numbers");
Path backup = Path.of("backup.txt");
Files.copy(src, backup);
try {
Files.copy(src, backup);
} catch (FileAlreadyExistsException e) {
System.out.println("already exists: " + e.getFile());
}
Files.copy(src, backup, StandardCopyOption.REPLACE_EXISTING);
Files.createDirectories(Path.of("archive"));
Path moved = Files.move(backup, Path.of("archive", "backup.txt"));
System.out.println("moved to " + moved);
Files.delete(src);
System.out.println("deleteIfExists again? " + Files.deleteIfExists(src));
try {
Files.delete(src);
} catch (NoSuchFileException e) {
System.out.println("no such file: " + e.getFile());
}
}
}
$ java CopyMove.java
already exists: backup.txt
moved to archive/backup.txt
deleteIfExists again? false
no such file: report.txt
REPLACE_EXISTING opts in to overwriting. Files.move within one file system is a rename; add
ATOMIC_MOVE when readers must never see a half-moved file.
Walking a directory tree
Files.walk streams every path under a directory, depth first. Sort it for stable output.
import java.nio.file.*;
import java.util.Comparator;
import java.util.stream.Stream;
public class Walk {
public static void main(String[] args) throws Exception {
Path root = Path.of("site");
Files.createDirectories(root.resolve("css"));
Files.createDirectories(root.resolve("js/lib"));
for (String f : new String[] {"index.html", "css/main.css", "js/app.js", "js/lib/util.js"}) {
Files.writeString(root.resolve(f), "x");
}
try (Stream<Path> all = Files.walk(root)) {
all.sorted().forEach(p -> System.out.println((Files.isDirectory(p) ? "d " : "f ") + p));
}
try (Stream<Path> js = Files.find(root, 10, (p, attrs) -> p.toString().endsWith(".js"))) {
System.out.println("js files: " + js.sorted().toList());
}
try (Stream<Path> all = Files.walk(root)) {
all.sorted(Comparator.reverseOrder()).forEach(p -> p.toFile().delete());
}
System.out.println("site exists? " + Files.exists(root));
}
}
$ java Walk.java
d site
d site/css
f site/css/main.css
f site/index.html
d site/js
f site/js/app.js
d site/js/lib
f site/js/lib/util.js
js files: [site/js/app.js, site/js/lib/util.js]
site exists? false
Files.find filters with access to each file’s attributes, so it can select by size or date without
extra calls. Deleting in reverse order removes children before parents — the standard recursive
delete.
File attributes
Files reads and writes metadata: size, timestamps and, on POSIX systems, permissions.
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.time.Instant;
public class Attrs {
public static void main(String[] args) throws Exception {
Path p = Files.writeString(Path.of("data.csv"), "id,name\n1,ada\n");
Files.setLastModifiedTime(p, FileTime.from(Instant.parse("2026-01-15T10:00:00Z")));
Files.setPosixFilePermissions(p, PosixFilePermissions.fromString("rw-r-----"));
BasicFileAttributes a = Files.readAttributes(p, BasicFileAttributes.class);
System.out.println("size: " + a.size());
System.out.println("regular: " + a.isRegularFile());
System.out.println("modified: " + a.lastModifiedTime());
System.out.println("perms: " + PosixFilePermissions.toString(Files.getPosixFilePermissions(p)));
System.out.println("readable: " + Files.isReadable(p));
System.out.println("type: " + Files.probeContentType(p));
}
}
$ java Attrs.java
size: 14
regular: true
modified: 2026-01-15T10:00:00Z
perms: rw-r-----
readable: true
type: text/csv
readAttributes fetches all basic attributes in one system call, cheaper than separate calls when
you need several. POSIX permissions throw UnsupportedOperationException on Windows, where
AclFileAttributeView is the equivalent.
Watching a directory with WatchService
A WatchService delivers create, modify and delete events for a registered directory.
import java.nio.file.*;
import java.util.concurrent.TimeUnit;
public class Watch {
public static void main(String[] args) throws Exception {
Path inbox = Files.createDirectories(Path.of("inbox"));
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
inbox.register(watcher, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE);
Files.createFile(inbox.resolve("order-1.json"));
Files.delete(inbox.resolve("order-1.json"));
int seen = 0;
while (seen < 2) {
WatchKey key = watcher.poll(5, TimeUnit.SECONDS);
if (key == null) break;
for (WatchEvent<?> e : key.pollEvents()) {
System.out.println(e.kind().name() + " " + e.context());
seen++;
}
key.reset(); // re-arm the key for more events
}
}
}
}
$ java Watch.java
ENTRY_CREATE order-1.json
ENTRY_DELETE order-1.json
Events report the file name relative to the watched directory. Forgetting key.reset() is the most
common bug: the key stops queuing events and the watcher goes silent.
Practice
- Write
numbers.txtcontaining the integers 1 to 10, one per line, then useFiles.linesto print the sum of the even ones.
Solution
import java.nio.file.*;
import java.util.stream.*;
public class EvenSum {
public static void main(String[] args) throws Exception {
Path p = Path.of("numbers.txt");
Files.write(p, IntStream.rangeClosed(1, 10).mapToObj(String::valueOf).toList());
try (Stream<String> lines = Files.lines(p)) {
int sum = lines.mapToInt(Integer::parseInt).filter(n -> n % 2 == 0).sum();
System.out.println("even sum: " + sum);
}
}
}
$ java EvenSum.java
even sum: 30
mapToInt gives an IntStream, whose sum() avoids boxing every number.
- Create
docs/a.md,docs/b.txtanddocs/sub/c.md, then print the total size in bytes of every.mdfile underdocs.
Solution
import java.nio.file.*;
import java.util.stream.Stream;
public class MdSize {
public static void main(String[] args) throws Exception {
Files.createDirectories(Path.of("docs/sub"));
Files.writeString(Path.of("docs/a.md"), "# A\n");
Files.writeString(Path.of("docs/b.txt"), "ignored");
Files.writeString(Path.of("docs/sub/c.md"), "# C section\n");
try (Stream<Path> md = Files.find(Path.of("docs"), 5,
(p, attrs) -> attrs.isRegularFile() && p.toString().endsWith(".md"))) {
System.out.println("md bytes: " + md.mapToLong(p -> p.toFile().length()).sum());
}
}
}
$ java MdSize.java
md bytes: 16
The attribute check in Files.find excludes directories without a second system call.
- Try
Files.deleteon a non-empty directory, catch the exception, then delete it properly.
Solution
import java.nio.file.*;
import java.util.Comparator;
import java.util.stream.Stream;
public class DeleteTree {
public static void main(String[] args) throws Exception {
Path dir = Files.createDirectories(Path.of("tmpdir/inner"));
Files.writeString(dir.resolve("f.txt"), "x");
try {
Files.delete(Path.of("tmpdir"));
} catch (DirectoryNotEmptyException e) {
System.out.println("not empty: " + e.getFile());
}
try (Stream<Path> s = Files.walk(Path.of("tmpdir"))) {
for (Path p : s.sorted(Comparator.reverseOrder()).toList()) Files.delete(p);
}
System.out.println("exists? " + Files.exists(Path.of("tmpdir")));
}
}
$ java DeleteTree.java
not empty: tmpdir
exists? false
Collecting to a list first lets Files.delete throw its checked IOException from a normal loop
instead of inside a lambda.
- Convert a
java.io.Fileto aPathand back, and show thatPath.of("a").equals(new File("a").toPath()).
Solution
import java.io.File;
import java.nio.file.Path;
public class Convert {
public static void main(String[] args) {
File legacy = new File("config/app.properties");
Path modern = legacy.toPath();
System.out.println(modern.getFileName() + " in " + modern.getParent());
System.out.println(modern.toFile().getPath());
System.out.println(Path.of("a").equals(new File("a").toPath()));
}
}
$ java Convert.java
app.properties in config
config/app.properties
true
toPath() and toFile() make it easy to migrate code one method at a time.