Skip to content
elephantoo

File I/O with java.nio

Lesson 34 of 43 15 min read

Path and Files: read, write, append, list and walk directories, and stream large files safely.


Programs often need to save data that outlives a single run: settings, reports, CSV exports, logs. Java's modern file API, NIO.2 (java.nio.file, Java 7+), centres on two types: Path (where a file is) and Files (static methods that do things with it). It replaced most uses of the older java.io.File class.

Path: locating files#

Paths.java
import java.nio.file.Path;

public class Paths {
    public static void main(String[] args) {
        Path relative = Path.of("data", "reports", "sales.csv");   // OS-correct separators
        Path absolute = Path.of("/home/asha/notes.txt");

        System.out.println(relative);
        System.out.println(relative.getFileName());
        System.out.println(relative.getParent());
        System.out.println(absolute.isAbsolute() + " " + relative.isAbsolute());
        System.out.println(relative.resolveSibling("summary.csv"));
        System.out.println(Path.of("data").resolve("backup").resolve("old.csv"));
        System.out.println(Path.of("/a/b/../c/./d.txt").normalize());
        System.out.println(Path.of("/home/asha").relativize(Path.of("/home/asha/docs/cv.pdf")));
    }
}
Output
data/reports/sales.csv
sales.csv
data/reports
true false
data/reports/summary.csv
data/backup/old.csv
/a/c/d.txt
docs/cv.pdf

(On Windows the separator prints as \.) Relative paths are resolved against the current working directory, the folder you ran java from. relative.toAbsolutePath() shows the full path. Creating a Path never touches the disk; the file doesn't need to exist.

Reading and writing small files#

For files that comfortably fit in memory (configuration, small reports), Files has one-liners:

SmallFiles.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 SmallFiles {
    public static void main(String[] args) throws IOException {
        Path file = Path.of("todo.txt");

        Files.writeString(file, "Buy milk\n");                        // create or overwrite
        Files.writeString(file, "Learn NIO\n", StandardOpenOption.APPEND);
        Files.write(file, List.of("Call mum", "Practise Java"), StandardOpenOption.APPEND);

        String all = Files.readString(file);                           // whole file as one String
        System.out.print(all);

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

        System.out.println(Files.exists(file) + " " + Files.size(file) + " bytes");
        Files.delete(file);
        System.out.println(Files.exists(file));
    }
}
Output
Buy milk
Learn NIO
Call mum
Practise Java
4 tasks, first: Buy milk
true 42 bytes
false

Notes:

  • These methods use UTF-8 by default, which is what you want. Pass a Charset explicitly if a file uses another encoding.
  • Almost every Files method throws the checked IOException; here main simply declares it. In real code, handle it at a sensible level.
  • Files.write(path, lines) writes each element followed by a line separator.

Streaming large files line by line#

readAllLines loads everything into memory. For big files, read one line at a time:

BigFile.java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;

public class BigFile {
    public static void main(String[] args) throws IOException {
        Path log = Path.of("app.log");

        try (BufferedWriter out = Files.newBufferedWriter(log)) {
            for (int i = 1; i <= 10_000; i++) {
                String level = i % 1000 == 0 ? "ERROR" : (i % 10 == 0 ? "WARN" : "INFO");
                out.write(level + " request " + i);
                out.newLine();
            }
        }

        // Option 1: BufferedReader loop
        int errors = 0;
        try (BufferedReader in = Files.newBufferedReader(log)) {
            String line;
            while ((line = in.readLine()) != null) {
                if (line.startsWith("ERROR")) errors++;
            }
        }
        System.out.println("Errors: " + errors);

        // Option 2: a lazy Stream<String>; must be closed!
        try (Stream<String> lines = Files.lines(log)) {
            long warns = lines.filter(l -> l.startsWith("WARN")).count();
            System.out.println("Warnings: " + warns);
        }

        Files.delete(log);
    }
}
Output
Errors: 10
Warnings: 990

Buffered readers and writers move data in large chunks, which is far faster than byte-by-byte access. Always wrap them in try-with-resources so the file is closed even if an exception occurs.

Working with CSV data#

CsvReport.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 CsvReport {
    record Sale(String city, String product, int qty, double price) {
        static Sale parse(String line) {
            String[] f = line.split(",");
            return new Sale(f[0].strip(), f[1].strip(), Integer.parseInt(f[2].strip()), Double.parseDouble(f[3].strip()));
        }
        double total() { return qty * price; }
    }

    public static void main(String[] args) throws IOException {
        Path csv = Path.of("sales.csv");
        Files.write(csv, List.of(
            "city,product,qty,price",
            "Pune,Pen,10,25.0",
            "Delhi,Notebook,4,60.0",
            "Pune,Notebook,3,60.0",
            "Delhi,Pen,20,25.0"));

        Map<String, Double> revenueByCity;
        try (Stream<String> lines = Files.lines(csv)) {
            revenueByCity = lines.skip(1)                     // skip the header
                    .filter(l -> !l.isBlank())
                    .map(Sale::parse)
                    .collect(Collectors.groupingBy(Sale::city, TreeMap::new, Collectors.summingDouble(Sale::total)));
        }
        System.out.println(revenueByCity);

        Path report = Path.of("report.txt");
        List<String> out = revenueByCity.entrySet().stream()
                .map(e -> String.format("%-6s %8.2f", e.getKey(), e.getValue()))
                .toList();
        Files.write(report, out);
        System.out.print(Files.readString(report));

        Files.delete(csv);
        Files.delete(report);
    }
}
Output
{Delhi=740.0, Pune=430.0}
Delhi    740.00
Pune     430.00

Real CSV can contain quoted fields with commas inside ("Mumbai, MH"). For production data, use a library such as Apache Commons CSV or OpenCSV instead of split(",").

Directories: create, list, walk, copy, move#

Directories.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
import java.util.stream.Stream;

public class Directories {
    public static void main(String[] args) throws IOException {
        Path root = Path.of("demo-dir");
        Files.createDirectories(root.resolve("images/2026"));       // creates parents too
        Files.writeString(root.resolve("readme.md"), "# Demo");
        Files.writeString(root.resolve("images/2026/cat.jpg"), "fake image bytes");
        Files.writeString(root.resolve("images/logo.png"), "png");

        try (Stream<Path> entries = Files.list(root)) {              // direct children only
            entries.map(p -> p.getFileName() + (Files.isDirectory(p) ? "/" : ""))
                   .sorted()
                   .forEach(System.out::println);
        }

        try (Stream<Path> all = Files.walk(root)) {                  // recursive
            all.filter(Files::isRegularFile)
               .map(p -> root.relativize(p).toString())
               .sorted()
               .forEach(p -> System.out.println("  file: " + p));
        }

        Path copy = Files.copy(root.resolve("readme.md"), root.resolve("readme-copy.md"),
                StandardCopyOption.REPLACE_EXISTING);
        Path moved = Files.move(copy, root.resolve("images/readme.md"));
        System.out.println(Files.exists(copy) + " " + Files.exists(moved));

        try (Stream<Path> all = Files.walk(root)) {                  // delete deepest first
            all.sorted(Comparator.reverseOrder()).forEach(p -> {
                try { Files.delete(p); } catch (IOException e) { throw new RuntimeException(e); }
            });
        }
        System.out.println("cleaned up: " + !Files.exists(root));
    }
}
Output
images/
readme.md
  file: images/2026/cat.jpg
  file: images/logo.png
  file: readme.md
false true
cleaned up: true

Useful Files methods:

MethodPurpose
exists, notExists, isDirectory, isRegularFile, isReadablechecks
createFile, createDirectory, createDirectoriescreate
createTempFile, createTempDirectorytemporary files in the OS temp folder
copy, move (with StandardCopyOption)copy/move/rename
delete, deleteIfExistsdelete (a directory must be empty)
size, getLastModifiedTimemetadata
list, walk, finddirectory streams (close them!)

Handling errors properly#

File operations fail for many reasons: missing files, permissions, full disks. NIO throws specific subclasses of IOException:

Errors.java
import java.io.IOException;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;

public class Errors {
    public static void main(String[] args) {
        try {
            Files.readString(Path.of("does-not-exist.txt"));
        } catch (NoSuchFileException e) {
            System.out.println("Missing file: " + e.getFile());
        } catch (AccessDeniedException e) {
            System.out.println("No permission: " + e.getFile());
        } catch (IOException e) {
            System.out.println("Other I/O problem: " + e);
        }

        Path p = Path.of("once.txt");
        try {
            Files.createFile(p);
            Files.createFile(p);
        } catch (FileAlreadyExistsException e) {
            System.out.println("Already exists: " + e.getFile());
        } catch (IOException e) {
            System.out.println(e);
        } finally {
            try { Files.deleteIfExists(p); } catch (IOException ignored) { }
        }
    }
}
Output
Missing file: does-not-exist.txt
Already exists: once.txt

Binary files and the classic java.io streams#

Text is just one kind of data. For images, PDFs or any raw bytes:

Java
byte[] bytes = Files.readAllBytes(Path.of("photo.jpg"));          // small files
Files.write(Path.of("copy.jpg"), bytes);

try (InputStream in = Files.newInputStream(Path.of("big.iso"));
     OutputStream out = Files.newOutputStream(Path.of("big-copy.iso"))) {
    in.transferTo(out);                                             // streams in chunks
}

InputStream/OutputStream (bytes) and Reader/Writer (characters) from java.io are still the foundation for network sockets, HTTP bodies and compression (GZIPOutputStream). Files.newInputStream and friends connect them to NIO paths. If you meet old code using new File("x.txt"), convert with file.toPath().

Reading resources from the classpath#

Files bundled inside your application (templates, default config) live on the classpath, often inside a JAR, so they aren't normal files. Load them with:

Java
try (InputStream in = MyApp.class.getResourceAsStream("/config/defaults.properties")) {
    Properties props = new Properties();
    props.load(in);
}

In a Maven/Gradle project, such files go in src/main/resources.

Common mistakes#

  • Forgetting to close streams from Files.lines, list or walk, which leaks file handles.
  • Reading huge files with readAllLines or readString, causing OutOfMemoryError.
  • Assuming the working directory: relative paths depend on where the program was launched. Print Path.of("").toAbsolutePath() when in doubt.
  • Overwriting files accidentally: writeString truncates by default.
  • Hard-coding / or \. Use Path.of("a", "b") or resolve.
  • Ignoring character encoding when exchanging files with other systems.

What's next#

Our programs have done one thing at a time. Next we unlock the power of modern multi-core CPUs with multithreading.

Check your understanding

Quick quiz

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

  2. 2.What does Files.writeString(path, text) do if the file already exists?

  3. 3.You need to process a 5 GB log file line by line. Which approach is appropriate?

Finished reading?

Mark this lesson complete to track your progress.