RizTech Academy logo
RizTech Academy
Files, Dates and DataLesson 1 of 430 min

Files, Paths and NIO

Every program eventually needs to read something from disk or write something to it. Java has two APIs for this: the old java.io.File from 1996, and java.nio.file from Java 7.

Use java.nio.file. The old one returns false instead of telling you why something failed, has no useful exception messages, and cannot express symbolic links or file attributes. It survives because a great deal of code was written against it, so you will read it — but do not write it.

Path

A Path is a location. It does not have to exist.

Path file = Path.of("/var/data/riztech", "deliveries.csv");
  path      : /var/data/riztech/deliveries.csv
  fileName  : deliveries.csv
  parent    : /var/data/riztech
  absolute  : true
  normalize : a/c/d          (from "a/b/../c/./d")
  relativize: deliveries.csv
  resolve up: archive.csv
Method Gives
Path.of(a, b, c) Joins with the platform separator
getFileName() The last element
getParent() Everything before it
resolve(other) This path plus another — the usual way to build one
resolveSibling(other) Replace the last element
relativize(other) The route from this path to that one
normalize() Removes . and ..
toAbsolutePath() Against the working directory
startsWith(other) Path-element prefix, not text prefix

Never build a path by concatenating strings with / or \. Path.of uses the right separator for the platform, so the same code works on a developer's Windows laptop and a Linux server.

  File.separator is [/]
  Path.of handles it: data/2026/sep.csv

One security note worth having early: normalize() and startsWith are how you stop a path traversal attack. If a user supplies a filename and you do uploadDir.resolve(userInput), they can send ../../etc/passwd. Normalise the result and check it still starts with your directory.

Files

Everything you do to a file is a static method on Files.

Call Does
Files.exists(p) / notExists(p) Presence
Files.size(p) Bytes
Files.createDirectories(p) Makes the whole tree; no error if it exists
Files.createFile(p) Fails if it already exists
Files.readString(p) The whole file as a String
Files.readAllLines(p) The whole file as a List<String>
Files.lines(p) A lazy Stream<String> — must be closed
Files.writeString(p, s) Writes, replacing
Files.write(p, bytes) Raw bytes
Files.newBufferedReader(p) / newBufferedWriter(p) For large or incremental work
Files.copy(a, b) / move(a, b) With StandardCopyOption flags
Files.delete(p) Throws if missing
Files.deleteIfExists(p) Does not
Files.list(dir) One level, as a Stream<Path> — must be closed
Files.walk(dir) Recursive, as a Stream<Path> — must be closed
Files.readAttributes(p, BasicFileAttributes.class) Size, times, type
Files.createDirectories(base);
Files.writeString(file, content, StandardCharsets.UTF_8);
  exists    : true
  size      : 81 bytes
  readString: 3 lines
  readAllLines first: date,customer,area,tiffins

Appending needs an explicit option, because the default replaces:

Files.writeString(file, "2026-09-02,Kavita,Wagholi,3\n", StandardOpenOption.APPEND);

Always specify the charset

Files.writeString(file, content, StandardCharsets.UTF_8);
Files.readString(file, StandardCharsets.UTF_8);

The methods have overloads that use a default. Since Java 18 that default is UTF-8 everywhere, which fixed a long-standing source of pain — before that it was the platform's encoding, so a file written on a Windows machine in India and read on a Linux server came back with the wrong characters.

Say UTF-8 explicitly anyway. It costs a few characters and it makes the intent obvious to a reader who does not know which Java version this was written for.

The two streams that must be closed

Files.lines and Files.list are lazy and hold an open file handle:

try (Stream<String> lines = Files.lines(file)) {
    long rows = lines.skip(1).filter(l -> !l.isBlank()).count();
}

Without the try-with-resources the handle stays open until garbage collection, and a loop over thousands of files hits the operating system limit — the leak from the last module, in the place you are most likely to cause it.

Use Files.readAllLines when the file fits in memory and you avoid the question entirely. Use Files.lines when it does not. "Fits in memory" for a server with a few hundred megabytes of heap means up to tens of megabytes of text; a gigabyte log file does not.

Same rule for Files.list and Files.walk:

try (Stream<Path> entries = Files.list(base)) {
    entries.map(Path::getFileName).sorted().forEach(System.out::println);
}

Writing safely: the atomic move

A program that writes a report directly to its final location has a window where the file is half-written. If something reads it then — or if the process is killed — the result is a truncated file that looks valid.

Path target = base.resolve("report.txt");
Path tmp = base.resolve("report.txt.tmp");

Files.writeString(tmp, content);
Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
  target    : final content
  tmp gone  : true

Write to a temporary file in the same directory, then move. A move within one filesystem is atomic: readers see either the old file or the new one, never a partial one. The "same directory" part matters — a move across filesystems is a copy and delete, and is not atomic.

This is the pattern the capstone uses for saving data, and it is what separates a program you would trust with a month of records from one you would not.

Failures

Files.readString(base.resolve("nope.csv"));
NoSuchFileException: /var/folders/.../riztech-java-m8/nope.csv

NoSuchFileException extends IOException and its message is the path, which is exactly what you want. The ones you will meet:

Exception Means
NoSuchFileException The path does not exist
FileAlreadyExistsException createFile on something already there
AccessDeniedException Permissions
DirectoryNotEmptyException delete on a non-empty directory
NotDirectoryException list on a regular file

All checked, all extending IOException. Per module 7: catch them where you can add context — which file, which operation — and wrap.

Do not check exists() and then read. Between the two calls the file can disappear, and you have written a race condition. Just read, and catch NoSuchFileException.

Deleting a tree

There is no one-call recursive delete. The idiom:

try (Stream<Path> all = Files.walk(base)) {
    all.sorted(Comparator.reverseOrder()).forEach(p -> {
        try { Files.delete(p); } catch (IOException ignored) { }
    });
}

reverseOrder() puts children before their parents, because a directory must be empty before it can be deleted. The absence of a one-liner is deliberate — a recursive delete is not something a language should make easy to do by accident.

Check your work

Which API should you use, and why not the other? java.nio.file. java.io.File returns false rather than saying why something failed and has no useful exceptions.

Why Path.of("a", "b") rather than "a/" + "b"? It uses the platform's separator, so the same code works on Windows and Linux.

How do you stop a path traversal attack? normalize() the resolved path and check it still startsWith your base directory.

Which two Files methods must be closed, and why? Files.lines and Files.list (and walk) return lazy streams holding an open file handle.

When should you prefer readAllLines to lines? Whenever the file fits in memory. It closes immediately and removes the question.

How do you write a file without a window where it is half-written? Write to a temporary file in the same directory, then Files.move with ATOMIC_MOVE.

Why not call exists() before reading? The file can vanish between the two calls. Read, and catch NoSuchFileException.

Why does a recursive delete need reverseOrder()? A directory must be empty before it can be deleted, so children must come before their parents.

Practice 3, the atomic write. Writing directly to the target and killing the program halfway leaves a truncated file that still parses as valid CSV — fewer rows, no error. With the temp-then-move version, the target either holds the complete old file or the complete new one. That is the whole argument, and it is worth causing once with Ctrl+C.

Practice 5, the leak. Calling Files.lines without closing it usually appears to work, because garbage collection eventually closes the handle. Run it a few thousand times in a loop and you get Too many open files — pointing at whichever open happened to be unlucky, not at any of the leaks. Lower the limit with ulimit -n 256 to see it quickly.

Practice

  1. Build and inspect paths. Create a path with Path.of, then print getFileName, getParent, normalize on something containing .., and relativize between two paths. Predict each before running.

  2. Write, append and read. Write a three-line CSV, append a fourth line, then read it back with readString, readAllLines and lines. Confirm all three agree on the line count.

  3. Write atomically. Write a report directly to its final path with a Thread.sleep in the middle, and kill the program during the sleep. Look at the file. Then do it again with the temp-then-move pattern.

  4. List and walk. List one directory, then walk a tree, printing relative paths. Use try-with-resources for both.

  5. Leak a handle. Call Files.lines without closing it, in a loop over the same file several thousand times. Lower your shell's limit first with ulimit -n 256. Then fix it.

  6. Harder — a safe upload directory. Write Path safeResolve(Path base, String userSuppliedName) that rejects anything escaping base. Test it with report.csv, ../secret.txt, sub/dir/x.csv, /etc/passwd and ..%2Fsecret.txt. Then write down which of those your first attempt let through — most people's lets one through, and the one it lets through is why the check belongs in a named method rather than inline.

Next: reading and writing real data — CSV, and why you should not write your own parser for long.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship