Java NIO.2 Path and Files API: Build, Traverse, Read and Write (1Z0-831)
The java.nio.file API (NIO.2) is Java's modern file-system API: a Path is an immutable description of a location, and the static methods of the Files class create, read, write, copy, move, delete, traverse and inspect the files those Paths name. Oracle 1Z0-831 objective 9.3 asks you to construct, traverse, create, read and write Path objects and their properties, and to predict exactly what a program using them prints or throws. This lesson separates the purely syntactic Path operations (name elements, resolve, relativize, normalize) from the Files operations that touch the disk, and covers each method's exceptions, open and copy options, directory traversal and file attributes.
On this page10 sections
- What a Path is and how to build one
- Reading a Path's properties: root, file name, parent and name elements
- Combining paths: resolve, resolveSibling and relativize
- Cleaning and anchoring paths: normalize, toAbsolutePath and toRealPath
- Comparing paths and Path immutability
- Creating files and directories with Files
- Copying, moving and deleting
- Reading and writing file content through Files
- Traversing directories: list, walk, find and walkFileTree
- Reading file properties and attributes
- Construct Path objects with Path.of, Paths.get and FileSystem.getPath, including varargs joining and empty elements
- Read a Path's root, file name, parent, name count, getName and subpath results, including the null and IllegalArgumentException cases
- Combine and compare paths with resolve, resolveSibling, relativize, normalize, startsWith and endsWith, and explain why Paths are immutable
- Distinguish operations that never touch the disk (normalize, toAbsolutePath) from those that do (toRealPath and every Files method)
- Create, copy, move and delete files and directories, predicting FileAlreadyExistsException, NoSuchFileException and DirectoryNotEmptyException
- Read and write file content with Files, choosing between eager and lazy methods and applying StandardOpenOption rules
- Traverse directory trees with Files.list, walk, find and walkFileTree, and read file attributes including symbolic-link behaviour
What a Path is and how to build one
A java.nio.file.Path is an immutable object that holds a sequence of name elements, with an optional root, describing a location in a file system; creating one never touches the disk, so the file it names does not have to exist. You build one with Path.of(...) (Java 11+), the older Paths.get(...), or FileSystems.getDefault().getPath(...) — all three return an equal Path for the same arguments on the default file system.
- Varargs are joined with the separator:
Path.of("music", "jazz", "live.flac")ismusic/jazz/live.flacon a Unix-style system. The result is absolute only if the joined string starts with a root, so this one is relative andisAbsolute()isfalse. - Empty strings are skipped and redundant or trailing separators collapse:
Path.of("/etc", "", "nginx/", "sites")is/etc/nginx/sites, with three name elements. - The empty path — built from an empty string — is a special case: it has one (empty) name element rather than none, so its name count is not 0. It stands for the current directory when used to access a file.
- From a URI:
Path.of(URI)needs an absolute, hierarchical URI such asfile:///opt/app; a URI without a scheme throwsIllegalArgumentException("Missing scheme"). - Interop with the legacy API:
file.toPath()andpath.toFile()convert betweenjava.io.FileandPath.
toString() uses the platform separator (/ on Unix-style systems, \ on Windows), which is why output-based reasoning always assumes a particular style of file system. Every Path belongs to one FileSystem; mixing Paths from different providers (say a ZIP file system and the default one) in resolve, relativize or Files.copy throws ProviderMismatchException — a provider problem, not a path-shape problem.
Reading a Path's properties: root, file name, parent and name elements
Every Path is a root (or none) followed by zero-based name elements, and the root is not a name element. For /opt/shop/static/img/logo.png the root is /, the names are opt, shop, static, img, logo.png, and getNameCount() is 5.
Path p = Path.of("/opt/shop/static/img/logo.png");
p.getRoot(); // /
p.getFileName(); // logo.png (last element, relative)
p.getParent(); // /opt/shop/static/img
p.getNameCount(); // 5
p.getName(0); // opt (index 0 is the first name, not the root)
p.subpath(2, 4); // static/img (relative, end-exclusive)| Method | Returns | When the component is missing or the index is bad |
|---|---|---|
getRoot() | The root, e.g. / | null for a relative path |
getFileName() | The last name element as a relative Path | null for a path made of only a root (it has no names) |
getParent() | Everything except the last element (root included) | null when there is no parent — Path.of("readme.md").getParent() is null, not . |
getName(i) | The element at zero-based index i, never the root | IllegalArgumentException if i is negative or ≥ getNameCount() — p.getName(5) above throws |
subpath(b, e) | Elements b to e - 1, always relative | IllegalArgumentException unless 0 ≤ b < e ≤ getNameCount() — an empty range such as p.subpath(2, 2) throws |
Missing components give null rather than an exception, so string concatenation of them prints null; calling a method on that null is what throws NullPointerException. Because the root is not a name, the root path on its own has a name count of 0. A Path is also Iterable<Path>: for (Path n : Path.of("/x/y")) visits x then y — the root is skipped there too. isAbsolute() reports whether the path has a root (on Unix).
Combining paths: resolve, resolveSibling and relativize
base.resolve(other) appends a relative other to base, returns other itself when it is absolute, and returns base when other is empty. It is purely syntactic: it never checks the disk and never normalizes, so .. and . stay in the result.
Path base = Path.of("/home/kim");
base.resolve("notes/todo.md"); // /home/kim/notes/todo.md
base.resolve("/etc/motd"); // /etc/motd (absolute wins)
base.resolve("../shared"); // /home/kim/../shared (as is)
Path dump = Path.of("/backup/db.dump");
dump.resolveSibling("db.dump.old"); // /backup/db.dump.old
dump.resolve("db.dump.old"); // /backup/db.dump/db.dump.oldA trailing separator in the base, as in Path.of("/web/"), is dropped when the Path is built, so resolving css/site.css against it gives /web/css/site.css with a single separator. resolveSibling(x) resolves x against the parent — it is shorthand for getParent().resolve(x), the idiom for "same directory, different file name" — and, like resolve, returns x unchanged when it is absolute. Plain resolve on a path that names a file simply nests the argument beneath it, because a Path does not know whether it names a file or a directory.
relativize: the path between two paths
p.relativize(q) returns the relative path that, resolved against p, leads to q: it climbs from p up to the common ancestor with one .. per element, then descends to q. Both paths must be of the same type — both absolute or both relative.
Path from = Path.of("/home/kim/projects/web");
from.relativize(Path.of("/home/lee/music"));
// ../../../lee/music
Path.of("x/y").relativize(Path.of("x/y/z")); // z
Path.of("x/y/z").relativize(Path.of("x")); // ../..
Path.of("x/y").relativize(Path.of("x/y")); // empty path
Path.of("reports").relativize(Path.of("/reports/q3"));
// IllegalArgumentException: 'other' is different type of PathTo relate an absolute and a relative path, convert the relative one first with toAbsolutePath(). Count the climbs carefully: from /home/kim/projects/web three steps (web, projects, kim) reach the common ancestor /home. The documented identity runs this way: for a normalized p and a relative q with no root, p.relativize(p.resolve(q)) equals q. The reverse trip does not give back an equal Path: from.resolve(from.relativize(target)) here is /home/kim/projects/web/../../../lee/music, which only reads as /home/lee/music after normalize() — and with symbolic links on the way even that is implementation-dependent. Like resolve, relativize works on the names alone and needs no file to exist.
Cleaning and anchoring paths: normalize, toAbsolutePath and toRealPath
The three methods differ in one crucial way — only toRealPath looks at the file system. normalize() removes . elements and cancels each name/.. pair as text; toAbsolutePath() prefixes the current working directory; toRealPath() resolves the path against the actual disk.
| Method | Touches the disk? | Throws | What it does |
|---|---|---|---|
normalize() | No | Nothing | Syntactic: drops ., cancels name/.. |
toAbsolutePath() | No | Nothing checked (file may be missing) | Resolves a relative path against the working directory; an absolute path is returned as is |
toRealPath(LinkOption...) | Yes | IOException (e.g. NoSuchFileException) — a checked exception | Absolute, normalized, symbolic links followed unless NOFOLLOW_LINKS |
Path.of("/var/www/./cache/../html/index.php").normalize();
// /var/www/html/index.php
Path.of("a/b/../../../shared/x.txt").normalize();
// ../shared/x.txt (the third .. has nothing to cancel)
Path.of("/../../tmp/x").normalize();
// /tmp/x (nothing exists above the root)Note the edge cases: in a relative path a leading .. that has nothing to cancel survives normalization, but directly after the root of an absolute path it is removed, because you cannot go above the root. And because toRealPath declares IOException, code that calls it must catch or declare it; for a missing file the exception arrives at the toRealPath call itself, so whatever the program printed before that statement — including anything computed with normalize or toAbsolutePath, which cannot fail — still appears first.
Comparing paths and Path immutability
Path.equals, startsWith and endsWith compare roots and whole name elements, not characters, and none of them normalizes or checks the disk. The String overloads convert the argument to a Path first.
Path h = Path.of("/home/kim/music");
h.startsWith("/home/ki"); // false (ki is not an element)
h.endsWith("kim/music"); // true (last two elements)
h.endsWith("/music"); // false (/music is absolute)
Path.of("x/y").equals(Path.of("x/./y")); // falseThe rules behind those results: an absolute argument to startsWith must match the root as well as the leading names, so a relative argument never matches an absolute path (and vice versa); an absolute argument to endsWith only matches the whole path, while a relative argument matches trailing whole elements. For a character prefix or suffix use p.toString().startsWith(...). To ask whether two different spellings name the same file, use Files.isSameFile (below) or compare normalized absolute paths.
Paths are immutable. resolve, resolveSibling, relativize, normalize, toAbsolutePath and subpath return a Path — sometimes the same object, for example toAbsolutePath on a path that is already absolute — and never modify the receiver. Calling one as a statement and discarding the result, as in q.toAbsolutePath();, leaves q exactly as it was. Assign the result: q = q.toAbsolutePath();.
Creating files and directories with Files
The java.nio.file.Files class holds static methods that act on the file a Path names; the creation methods differ in whether they need the parent to exist and whether an existing target is an error.
| Method | Parents must exist? | Target already exists |
|---|---|---|
createFile(p) | Yes — otherwise NoSuchFileException | FileAlreadyExistsException |
createDirectory(p) | Yes — otherwise NoSuchFileException | FileAlreadyExistsException (even if it is a directory) |
createDirectories(p) | No — creates every missing parent | Fine if it is a directory; FileAlreadyExistsException if it is a regular file |
createTempFile(prefix, suffix) / createTempDirectory(prefix) | Uses the default temporary directory (overloads accept a directory) | A unique name is generated |
All of them return the Path they created and throw checked IOException subtypes. createDirectories is idempotent for directories, so calling it twice on the same path succeeds. The legacy File.mkdir() signals failure by returning false rather than throwing, and mkdir (unlike mkdirs) does not create parents. The exceptions that matter here all live in java.nio.file and extend FileSystemException (an IOException); a similarly named class that concerns whole FileSystem instances, created with FileSystems.newFileSystem, plays no part in creating files.
Copying, moving and deleting
Files.copy(source, target) and Files.move(source, target) treat target as the full path of the result — never as a directory to drop the file into — and both fail with FileAlreadyExistsException if the target exists, unless you pass StandardCopyOption.REPLACE_EXISTING.
Path photo = dir.resolve("img.png");
Path album = dir.resolve("album"); // an existing directory
Files.copy(photo, album); // FileAlreadyExistsException
Files.copy(photo, album.resolve(photo.getFileName())); // works
Files.move(photo, dir.resolve("cover.png")); // a rename- Copying a directory is not recursive:
Files.copy(srcDir, dst)creates an empty directorydst; the entries are not copied. Copy a tree by walking it and copying each entry. REPLACE_EXISTINGreplaces a regular file or an empty directory; replacing a non-empty directory throwsDirectoryNotEmptyException.ATOMIC_MOVEasks for the move to happen as one file-system operation (or fail withAtomicMoveNotSupportedException);COPY_ATTRIBUTESandNOFOLLOW_LINKSare the other copy options.- Stream overloads:
Files.copy(InputStream, Path)andFiles.copy(Path, OutputStream)bridge to the classic I/O streams covered in the console and file streams lesson.
| Delete call | Missing file | Non-empty directory |
|---|---|---|
Files.delete(p) | NoSuchFileException | DirectoryNotEmptyException |
Files.deleteIfExists(p) | Returns false | DirectoryNotEmptyException |
Neither delete is recursive: empty a directory before deleting it (typically walking the tree and deleting in postVisitDirectory, or deleting a Files.walk stream in reverse order).
Reading and writing file content through Files
Files offers one-call methods for small files and streaming methods for large ones, The character-based methods use UTF-8 unless you pass a Charset; the byte-based ones (readAllBytes, write(p, bytes)) involve no charset at all.
| Method | Reads the whole file into memory? | Must be closed? |
|---|---|---|
readString(p) / writeString(p, text) | Yes | No — opens and closes internally |
readAllLines(p) → List<String> | Yes | No |
readAllBytes(p) / write(p, bytes) (raw bytes) / write(p, lines) | Yes | No |
lines(p) → Stream<String> | No — lazy, line by line | Yes — it holds an open file handle |
newBufferedReader(p) / newBufferedWriter(p) | No | Yes |
readAllLines splits on line terminators, counts a final line even when it has no terminator, and does not add an empty element for a trailing one: "north\nsouth" and "north\nsouth\n" both give a two-element list. For a file too large to hold in memory, readAllLines and readString are ruled out, and a Files.lines stream used without try-with-resources leaves its file handle open — no terminal operation closes the stream. The correct shape is a lazy stream inside try-with-resources:
try (Stream<String> s = Files.lines(log)) {
return s.anyMatch(line -> line.contains("timeout"));
}Open options. With no options, the write methods use CREATE, TRUNCATE_EXISTING and WRITE. Once you pass any StandardOpenOption, CREATE and TRUNCATE_EXISTING are no longer implied — only those you list apply — but WRITE still is, because write and writeString always open the file for writing. Without truncation, writing starts at position 0 and overwrites only as many bytes as it writes:
Files.writeString(f, "12345"); // 12345 (defaults)
Files.writeString(f, "67", APPEND); // 1234567
Files.writeString(f, "xy", WRITE); // xy34567 — overwrites
// the first two bytes
Files.writeString(f, "z", CREATE, TRUNCATE_EXISTING); // zOther rules worth knowing: APPEND alone on a missing file throws NoSuchFileException (no CREATE); CREATE_NEW throws FileAlreadyExistsException if the file exists. The write methods return the Path, so Path f = Files.writeString(dir.resolve("a.txt"), "x"); is common. The reader and writer classes themselves (BufferedReader, PrintWriter, byte streams, Console) belong to the console and file streams topic.
Traversing directories: list, walk, find and walkFileTree
Files.list returns one level of entries without the start directory; Files.walk returns every level, depth-first, starting with the start path itself; Files.find walks like walk but filters with a BiPredicate<Path, BasicFileAttributes>. All three are lazy streams holding open directory handles, so use them in try-with-resources, and their iteration order is not guaranteed — sort before relying on it.
| Call | Includes start? | Depth | Start is a regular file |
|---|---|---|---|
Files.list(dir) | No | 1 level | NotDirectoryException |
Files.walk(start) | Yes | Unlimited | Stream of just that file |
Files.walk(start, maxDepth) | Yes | maxDepth levels below the start (0 = start only) | Stream of just that file |
Files.find(start, maxDepth, matcher) | Yes, if it matches | Required maxDepth | Tested like any entry |
For a tree holding root/notes.md, root/img/a.png, root/img/b.png and root/img/raw/c.dng, list(root) yields 2 entries (notes.md, img), walk(root) yields 7 (the root, notes.md, img, a.png, b.png, raw, c.dng) and walk(root, 1) yields 3 — the root plus the same entries list returns. A file two levels down is only reached when maxDepth is at least 2, and c.dng above needs 3.
Files.walkFileTree(start, visitor) drives a FileVisitor — usually a SimpleFileVisitor subclass overriding only what you need — calling preVisitDirectory before a directory's entries, visitFile for each non-directory, visitFileFailed when an entry cannot be read, and postVisitDirectory after the entries. Each callback returns a FileVisitResult:
| Result | Effect |
|---|---|
CONTINUE | Keep walking |
SKIP_SUBTREE | From preVisitDirectory: do not enter this directory (its postVisitDirectory is not called) |
SKIP_SIBLINGS | Skip the remaining siblings of this file or directory (the other entries of the same directory, which still gets its postVisitDirectory); returned from preVisitDirectory, it also skips that directory's own entries and its postVisitDirectory |
TERMINATE | Stop the whole walk |
The overload walkFileTree(start, Set<FileVisitOption>, maxDepth, visitor) limits depth, and a directory found exactly at maxDepth is reported through visitFile — not entered. In the tree above, walkFileTree(root, Set.of(), 1, visitor) passes both notes.md and the directory img to visitFile (its attributes report isDirectory() true), never calls preVisitDirectory for img, and never visits a.png, b.png or anything below. Symbolic links are not followed by walk, find or walkFileTree unless you pass FileVisitOption.FOLLOW_LINKS.
Reading file properties and attributes
File properties come from Files, not from Path: exists, notExists, isDirectory, isRegularFile, isSymbolicLink, isReadable/isWritable/isExecutable, isHidden, size, getLastModifiedTime, and readAttributes(p, BasicFileAttributes.class) to read several at once in a single operation.
Path f = Files.writeString(dir.resolve("n.txt"), "naïve");
BasicFileAttributes a =
Files.readAttributes(f, BasicFileAttributes.class);
a.size(); // 6 — bytes, and ï is 2 bytes in UTF-8
a.isRegularFile(); // true
a.lastModifiedTime(); // a FileTime- Size is in bytes, so a non-ASCII character written as UTF-8 makes it larger than the character count.
existsandnotExistsare not always opposites: if existence cannot be determined (for example, no permission), both returnfalse. For a missing file,notExistsreturnstrue.- Which methods throw for a missing file:
exists,notExists,isDirectory,isRegularFile,isSymbolicLink,isReadable,isWritableandisExecutablenever throw — they answerfalse(ortruefornotExists).isHiddenis different: it declaresIOException, and on Windows, where it reads the DOS hidden attribute, it throws for a missing file.size,getLastModifiedTimeandreadAttributesthrowNoSuchFileException. Files.isSameFile(a, b)returnstrueimmediately when the two Paths areequal, without checking the disk — so it is true even for a file that does not exist. For different spellings, such asPath.of("ghost.cfg")andPath.of("conf/../ghost.cfg"), it must look both files up, and it throwsNoSuchFileExceptionwhen they do not exist.- Symbolic links are followed by default. Passing
LinkOption.NOFOLLOW_LINKSasks about the link entry itself. For a linkshortcutthat points at a regular file,isRegularFile(shortcut)istruebutisRegularFile(shortcut, NOFOLLOW_LINKS)isfalse, whileexists(shortcut, NOFOLLOW_LINKS)istrue; a link to a directory behaves the same way withisDirectory.isSymbolicLinknever follows links, so it istruefor the link andfalsefor the file it points at.Files.createSymbolicLink(link, target)creates one.
View-specific attributes (PosixFileAttributes, DosFileAttributes) and Files.setLastModifiedTime/setAttribute extend the same model, but BasicFileAttributes is the portable set every file system supports.
Tip. Objective 9.3 is examined through short code: a Path expression or a complete program that builds a small directory tree in a temporary directory, followed by "What is the result?" Path items assume a Unix-style file system and hinge on exact output — name counts with and without a root, subpath bounds, null components, resolve with an absolute argument, relativize climbs, normalize edge cases with leading .., and element-wise startsWith/endsWith. Files items ask whether a sequence of createFile, createDirectory, copy, move or delete calls succeeds or which exception it throws, what a file contains after writes with different StandardOpenOption combinations, and how many entries list, walk, find or walkFileTree report for a given tree and depth. Multiple-answer items list near-identical expressions and ask which return an equal Path, which throw, or which evaluate to true, including attribute checks with NOFOLLOW_LINKS and isSameFile on missing files. Scenario items ask which approach reads a huge file lazily while releasing the handle.
- Creating or manipulating a Path never touches the disk; only Files methods and toRealPath do.
- The root is not a name element: getNameCount, getName and subpath count only the names after it, and subpath is zero-based, end-exclusive and relative.
- getParent, getRoot and getFileName return null when the component is missing; out-of-range indexes or an empty range for getName or subpath throw IllegalArgumentException.
- resolve appends a relative argument, returns an absolute argument as is, and never normalizes; resolveSibling resolves against the parent.
- relativize climbs to the common ancestor with .. and throws IllegalArgumentException when one path is absolute and the other relative.
- Paths are immutable: discard the result of normalize or resolve and the original is unchanged.
- startsWith, endsWith and equals compare whole name elements and roots, not characters.
- createFile and createDirectory need the parent and refuse existing targets; createDirectories builds parents and is idempotent for directories.
- copy and move take the full target path, need REPLACE_EXISTING to overwrite, and never act recursively; delete fails on a non-empty directory, deleteIfExists returns false for a missing one.
- Passing any open option stops CREATE and TRUNCATE_EXISTING being implied (WRITE always is); Files.lines, list, walk and find are lazy and must be closed.
Frequently asked questions
What is the difference between Path.of and Paths.get in Java?
There is no behavioural difference. Paths.get(...) delegates to Path.of(...) (added in Java 11), and both are equivalent to FileSystems.getDefault().getPath(...). They return equal Path objects for the same arguments; Path.of is the recommended spelling in new code.
Does creating a Path check whether the file exists?
No. A Path is only a description of a location, so Path.of, resolve, relativize, normalize and toAbsolutePath work for files that do not exist. Only methods that access the file system — the methods of the Files class and Path.toRealPath — check existence and can throw IOException subtypes such as NoSuchFileException.
Why does Path.relativize throw IllegalArgumentException?
Path.relativize throws IllegalArgumentException when one path is absolute and the other is relative, because no relative path can lead from one kind to the other. Convert the relative path with toAbsolutePath() first, or make both relative.
What is the difference between normalize and toRealPath?
normalize() is a text operation that removes . elements and cancels name/.. pairs without looking at the disk, and it never throws. toRealPath() resolves the path against the real file system: it returns an absolute path with symbolic links followed and throws NoSuchFileException (an IOException) if the file does not exist.
Why does Files.move throw FileAlreadyExistsException when the target is a directory?
Files.move and Files.copy treat the target as the full path of the result, not as a folder to place the file in. If that path already exists — including as a directory — they throw FileAlreadyExistsException unless REPLACE_EXISTING is given. To move a file into a directory, use dir.resolve(source.getFileName()) as the target.
Does Files.copy copy a directory's contents?
No. Copying a directory with Files.copy creates an empty directory at the target; the entries inside are not copied. To copy a whole tree, traverse it with Files.walk or Files.walkFileTree and copy each file and directory individually.
What is the difference between Files.list and Files.walk?
Files.list returns the entries directly inside a directory, without the directory itself, and throws NotDirectoryException for a regular file. Files.walk descends recursively, depth-first, and its stream starts with the start path itself; walk(start, maxDepth) limits the levels, with 0 meaning only the start. Both are lazy streams that hold open handles and should be used in try-with-resources.
Why must Files.lines be closed?
Files.lines reads a file lazily, one line at a time, so the returned Stream holds an open file handle until the stream is closed. Terminal operations such as count do not close it, so the stream should be created in a try-with-resources statement. Files.readAllLines and Files.readString, by contrast, close the file themselves but load the whole content into memory.
Source
This lesson covers the "Performing Input and Output Operations Using the Java I/O API" domain of the official 1Z0-831 exam guide. Vendors revise their guides — check the source for the current version.
- Java SE 25 Developer Professional (1Z0-831) exam page and topics — Oracle University
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.