File I/O with java.nio.file

Java · Lesson 25 of 32 6 min read Updated

Programs forget everything when they stop, unless they save it. Files let your program keep data between runs, read reports or CSV exports from other systems, and write logs and receipts. Java's modern file API lives in the package java.nio.file, built around two classes: Path, which names a file or folder, and Files, which does the work.

In this lesson you will write and read text files, process a CSV file line by line, build paths that work on every operating system, and copy, move, list and delete files. Every file method can fail (the disk is full, the file is missing), so these methods throw the checked IOException you met in Exceptions.

Writing and reading text files

For small files, four methods cover almost everything: Files.writeString and Files.readString handle the whole file as one string, while Files.write and Files.readAllLines work with a list of lines.

Java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;

public class Diary {
    public static void main(String[] args) throws IOException {
        Path file = Path.of("notes.txt");

        Files.writeString(file, "Buy milk\nPay electricity bill\n");
        Files.writeString(file, "Call Meera\n", StandardOpenOption.APPEND);

        String all = Files.readString(file);
        System.out.print(all);

        List<String> lines = Files.readAllLines(file);
        System.out.println("Tasks: " + lines.size() + ", first: " + lines.get(0));

        Files.write(file, List.of("Fresh list", "Only this"));   // overwrites
        System.out.println(Files.readAllLines(file));
        System.out.println("Size in bytes: " + Files.size(file));
        System.out.println("Exists? " + Files.exists(file));
    }
}
Output
Buy milk
Pay electricity bill
Call Meera
Tasks: 3, first: Buy milk
[Fresh list, Only this]
Size in bytes: 23
Exists? true

By default, writing creates the file if needed and replaces its contents. StandardOpenOption.APPEND adds to the end instead. main declares throws IOException to keep the example short; in a real program you would catch the exception and show a helpful message. (The size is 23 bytes on Windows, where Files.write ends lines with \r\n, and 21 on Linux and macOS, which use \n.)

All these methods use UTF-8, so Hindi text and the ₹ sign are stored correctly. Since Java 18, UTF-8 is also the default for the older file classes.

Building paths

A Path is just a name for a location; creating one does not touch the disk. Build paths from parts with Path.of("a", "b") or resolve, and Java uses the right separator for the operating system: \ on Windows and / on Linux and macOS.

Java
import java.nio.file.Path;

public class Paths {
    public static void main(String[] args) {
        Path base = Path.of("shop", "2026", "october");
        Path invoice = base.resolve("invoice-101.txt");

        System.out.println(invoice);
        System.out.println(invoice.getFileName());
        System.out.println(invoice.getParent());
        System.out.println(invoice.getNameCount());
        System.out.println(invoice.isAbsolute());
        System.out.println(Path.of("shop/./2026/../2026/october").normalize());
    }
}

Output on Windows (on Linux and macOS you see / instead of \):

Output
shop\2026\october\invoice-101.txt
invoice-101.txt
shop\2026\october
4
false
shop\2026\october

These are relative paths: they start from the folder where you ran the program (the working directory). toAbsolutePath() shows the full path, and normalize() removes . and .. parts. Never build paths by gluing strings with "\\"; your program would break on Linux servers.

Processing a CSV file line by line

readAllLines loads the whole file into memory. For large files, such as a log with millions of lines, use Files.lines, which returns a stream that reads lazily, one line at a time. A stream that holds an open file must be closed, so put it in try-with-resources.

Suppose data/marks.csv contains:

Text
name,city,maths,science
Ananya,Lucknow,91,88
Farhan,Pune,78,82
Isha,Kochi,95,90
Rohan,Rohtak,58,64

This program reads it, keeps students with an average of 80 or more, and writes them to a new report file:

Java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;

public class Report {
    record Result(String name, String city, int maths, int science) {
        double average() {
            return (maths + science) / 2.0;
        }
    }

    static Result parse(String line) {
        String[] p = line.split(",");
        return new Result(p[0], p[1], Integer.parseInt(p[2]), Integer.parseInt(p[3]));
    }

    public static void main(String[] args) throws IOException {
        Path input = Path.of("data", "marks.csv");
        Path output = Path.of("reports", "toppers.txt");

        List<Result> results;
        try (Stream<String> lines = Files.lines(input)) {
            results = lines.skip(1)                 // skip the header row
                           .map(Report::parse)
                           .toList();
        }

        List<String> report = results.stream()
                .filter(r -> r.average() >= 80)
                .map(r -> "%s (%s): %.1f".formatted(r.name(), r.city(), r.average()))
                .toList();

        Files.createDirectories(output.getParent());
        Files.write(output, report);

        System.out.println("Read " + results.size() + " students, wrote " + report.size() + " toppers");
        Files.readAllLines(output).forEach(System.out::println);
    }
}
Output
Read 4 students, wrote 3 toppers
Ananya (Lucknow): 89.5
Farhan (Pune): 80.0
Isha (Kochi): 92.5

This is a realistic pattern: parse each line into a record, process the records with a stream, and write the result. Files.createDirectories creates the reports folder (and any missing parents) if it does not exist yet, and does nothing if it does. Simple split(",") works for clean data like this; real-world CSV files with quoted commas need a CSV library.

Copying, moving, listing and deleting

Java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.stream.Stream;

public class Manage {
    public static void main(String[] args) throws IOException {
        Path reports = Path.of("reports");
        Path original = reports.resolve("toppers.txt");
        Path backup = reports.resolve("toppers-backup.txt");

        Files.copy(original, backup, StandardCopyOption.REPLACE_EXISTING);
        Files.move(backup, reports.resolve("old-toppers.txt"), StandardCopyOption.REPLACE_EXISTING);

        try (Stream<Path> files = Files.list(reports)) {
            files.map(p -> p.getFileName().toString())
                 .sorted()
                 .forEach(name -> System.out.println("found " + name));
        }

        System.out.println("Deleted? " + Files.deleteIfExists(reports.resolve("old-toppers.txt")));
        System.out.println("Deleted again? " + Files.deleteIfExists(reports.resolve("old-toppers.txt")));

        try {
            Files.readString(Path.of("missing.txt"));
        } catch (NoSuchFileException e) {
            System.out.println("No such file: " + e.getMessage());
        }
    }
}
Output
found old-toppers.txt
found toppers.txt
Deleted? true
Deleted again? false
No such file: missing.txt

Without REPLACE_EXISTING, copying or moving onto an existing file throws FileAlreadyExistsException. Files.list shows one folder's entries; Files.walk goes into sub-folders too. Both return streams that must be closed. The order of Files.list is not guaranteed, which is why the example sorts the names. A missing file throws NoSuchFileException, a subclass of IOException.

Common mistakes

  • Not closing Files.lines, Files.list or Files.walk. Use try-with-resources, or the file stays locked (especially on Windows).
  • Using readAllLines on huge files. It loads everything into memory; stream with Files.lines instead.
  • Hard-coding separators. Build paths with Path.of(...) and resolve, not "folder\\file.txt".
  • Forgetting that writing replaces the file. Use StandardOpenOption.APPEND to add.
  • Confusing the working directory. A relative path starts where the program runs, which in an IDE is usually the project folder.

Key points

  • Path.of(...) and resolve build paths; Files reads, writes and manages files.
  • Files.writeString / readString and Files.write / readAllLines handle small text files in UTF-8.
  • StandardOpenOption.APPEND adds to a file instead of replacing it.
  • Files.lines streams large files line by line; close it with try-with-resources.
  • Files.copy, move, deleteIfExists, createDirectories, list and walk manage files and folders.
  • File operations throw IOException; missing files give NoSuchFileException.

Exercise

Write Expenses.java that creates expenses.csv with a header date,category,amount and five rows of your own (for example groceries, travel and phone bills in rupees). Then read the file back with Files.lines, skip the header, and write a file summary.txt with one line per category showing its total, sorted by category. Print the summary file.

Show solution
Java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Expenses {
    public static void main(String[] args) throws IOException {
        Path csv = Path.of("expenses.csv");
        Files.write(csv, List.of(
            "date,category,amount",
            "2026-10-01,Groceries,1850",
            "2026-10-03,Travel,420",
            "2026-10-05,Phone,299",
            "2026-10-12,Groceries,960",
            "2026-10-20,Travel,1150"));

        Map<String, Integer> totals;
        try (Stream<String> lines = Files.lines(csv)) {
            totals = lines.skip(1)
                    .map(line -> line.split(","))
                    .collect(Collectors.groupingBy(p -> p[1], TreeMap::new,
                             Collectors.summingInt(p -> Integer.parseInt(p[2]))));
        }

        List<String> summary = totals.entrySet().stream()
                .map(e -> e.getKey() + ": Rs " + e.getValue())
                .toList();
        Path out = Path.of("summary.txt");
        Files.write(out, summary);
        Files.readAllLines(out).forEach(System.out::println);
    }
}
Output
Groceries: Rs 2810
Phone: Rs 299
Travel: Rs 1570

groupingBy with a TreeMap adds up each category and keeps them sorted. The stream from Files.lines is closed by try-with-resources before the summary is written.

Quick Quiz

1. Which method reads a whole small text file into one String?

2. How do you add text to the end of an existing file?

3. Why should Files.lines(path) be used inside try-with-resources?

4. Which code builds a path that works on both Windows and Linux?

Learn Java with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call