SaveMyCert
Log in
5 of 5 free questions left today·for 30 a day
Performing Input and Output Operations Using the Java I/O API

Java NIO.2 Path and Files API: Build, Traverse, Read and Write (1Z0-831)

16 min read1Z0-831 · Performing Input and Output Operations Using the Java I/O APIUpdated

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.

What you’ll learn
  • 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") is music/jazz/live.flac on a Unix-style system. The result is absolute only if the joined string starts with a root, so this one is relative and isAbsolute() is false.
  • 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 as file:///opt/app; a URI without a scheme throws IllegalArgumentException ("Missing scheme").
  • Interop with the legacy API: file.toPath() and path.toFile() convert between java.io.File and Path.

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)
MethodReturnsWhen 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 Pathnull 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 rootIllegalArgumentException if i is negative or ≥ getNameCount() — p.getName(5) above throws
subpath(b, e)Elements b to e - 1, always relativeIllegalArgumentException 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.old

A 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 Path

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

MethodTouches the disk?ThrowsWhat it does
normalize()NoNothingSyntactic: drops ., cancels name/..
toAbsolutePath()NoNothing checked (file may be missing)Resolves a relative path against the working directory; an absolute path is returned as is
toRealPath(LinkOption...)YesIOException (e.g. NoSuchFileException) — a checked exceptionAbsolute, 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"));  // false

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

MethodParents must exist?Target already exists
createFile(p)Yes — otherwise NoSuchFileExceptionFileAlreadyExistsException
createDirectory(p)Yes — otherwise NoSuchFileExceptionFileAlreadyExistsException (even if it is a directory)
createDirectories(p)No — creates every missing parentFine 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 directory dst; the entries are not copied. Copy a tree by walking it and copying each entry.
  • REPLACE_EXISTING replaces a regular file or an empty directory; replacing a non-empty directory throws DirectoryNotEmptyException.
  • ATOMIC_MOVE asks for the move to happen as one file-system operation (or fail with AtomicMoveNotSupportedException); COPY_ATTRIBUTES and NOFOLLOW_LINKS are the other copy options.
  • Stream overloads: Files.copy(InputStream, Path) and Files.copy(Path, OutputStream) bridge to the classic I/O streams covered in the console and file streams lesson.
Delete callMissing fileNon-empty directory
Files.delete(p)NoSuchFileExceptionDirectoryNotEmptyException
Files.deleteIfExists(p)Returns falseDirectoryNotEmptyException

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.

MethodReads the whole file into memory?Must be closed?
readString(p) / writeString(p, text)YesNo — opens and closes internally
readAllLines(p) → List<String>YesNo
readAllBytes(p) / write(p, bytes) (raw bytes) / write(p, lines)YesNo
lines(p) → Stream<String>No — lazy, line by lineYes — it holds an open file handle
newBufferedReader(p) / newBufferedWriter(p)NoYes

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); // z

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

CallIncludes start?DepthStart is a regular file
Files.list(dir)No1 levelNotDirectoryException
Files.walk(start)YesUnlimitedStream of just that file
Files.walk(start, maxDepth)YesmaxDepth levels below the start (0 = start only)Stream of just that file
Files.find(start, maxDepth, matcher)Yes, if it matchesRequired maxDepthTested 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:

ResultEffect
CONTINUEKeep walking
SKIP_SUBTREEFrom preVisitDirectory: do not enter this directory (its postVisitDirectory is not called)
SKIP_SIBLINGSSkip 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
TERMINATEStop 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.
  • exists and notExists are not always opposites: if existence cannot be determined (for example, no permission), both return false. For a missing file, notExists returns true.
  • Which methods throw for a missing file: exists, notExists, isDirectory, isRegularFile, isSymbolicLink, isReadable, isWritable and isExecutable never throw — they answer false (or true for notExists). isHidden is different: it declares IOException, and on Windows, where it reads the DOS hidden attribute, it throws for a missing file. size, getLastModifiedTime and readAttributes throw NoSuchFileException.
  • Files.isSameFile(a, b) returns true immediately when the two Paths are equal, without checking the disk — so it is true even for a file that does not exist. For different spellings, such as Path.of("ghost.cfg") and Path.of("conf/../ghost.cfg"), it must look both files up, and it throws NoSuchFileException when they do not exist.
  • Symbolic links are followed by default. Passing LinkOption.NOFOLLOW_LINKS asks about the link entry itself. For a link shortcut that points at a regular file, isRegularFile(shortcut) is true but isRegularFile(shortcut, NOFOLLOW_LINKS) is false, while exists(shortcut, NOFOLLOW_LINKS) is true; a link to a directory behaves the same way with isDirectory. isSymbolicLink never follows links, so it is true for the link and false for 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.

Key takeaways
  • 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.

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.

Spotted a mistake in this lesson?