Reading and Writing Files in Java with Path and Files

Introduction Reading a configuration file, writing a report, processing a CSV export - almost every real program works with files. Modern Java does this with two classes from the java.nio.file package: Path, which represents a location in the file system, and Files, which has static methods to read, write, copy, move and inspect files. This tutorial explains reading and writing files in Java with Path and Files. We will start with creating and working with Path objects. We will then read and write whole files in one call, and work with files line by line. Next we will process large files lazily with streams, and append to files. Lastly, we will create directories, copy, move and delete files, and handle the exceptions file operations throw.

(Note - File operations throw the checked IOException, explained in Checked vs Unchecked ExceptionsClick to read tutorial: Checked vs Unchecked Exceptions in Java. The examples declare throws IOException on main() to keep them short.) Path - a location in the file system A Path represents the path to a file or directory - it does not open the file, and the file does not even need to exist. Create one with Path.of() (Java 11+), giving a single string or several parts which are joined with the right separator for the operating system.
Creating and working with Path objects
//PathBasics.java
import java.nio.file.Path;

public class PathBasics {
  public static void main(String[] args) {
    Path report = Path.of("reports", "2026", "october.csv");
    System.out.println("path          : " + report);
    System.out.println("file name     : " + report.getFileName());
    System.out.println("parent        : " + report.getParent());
    System.out.println("name count    : " + report.getNameCount());
    System.out.println("is absolute   : " + report.isAbsolute());

    Path base = Path.of("data");
    Path resolved = base.resolve("customers.txt");           // data/customers.txt
    Path sibling = resolved.resolveSibling("orders.txt");      // data/orders.txt
    System.out.println("resolve       : " + resolved);
    System.out.println("resolveSibling: " + sibling);
    System.out.println("normalize     : " + Path.of("data", "..", "logs", ".", "app.log").normalize());
    System.out.println("relativize    : " + Path.of("data").relativize(Path.of("data", "2026", "jan.txt")));
  }
}
 OUTPUT of the above code (on Windows; macOS and Linux show / instead of \)
path          : reports\2026\october.csv
file name     : october.csv
parent        : reports\2026
name count    : 3
is absolute   : false
resolve       : data\customers.txt
resolveSibling: data\orders.txt
normalize     : logs\app.log
relativize    : 2026\jan.txt
  • resolve() joins paths, and resolveSibling() replaces the last part - safer than building paths by joining strings with "/" or "\\".
  • normalize() removes . and .. parts, and relativize() works out the path from one location to another.
  • A relative path such as data/customers.txt is resolved against the program's working directory; toAbsolutePath() shows the full path.
Reading and writing a whole file For small and medium files, one call does the whole job. All of these methods open the file, do the work and close it again -
  • Files.writeString(path, text) and Files.readString(path) (Java 11+) - the whole file as one String.
  • Files.write(path, lines) and Files.readAllLines(path) - a List<String> of lines.
  • Files.write(path, bytes) and Files.readAllBytes(path) - raw bytes, for images and other binary files.
They use UTF-8 by default. Writing creates the file if it does not exist, and replaces its contents if it does.
Reading and writing whole files
//WholeFile.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class WholeFile {
  public static void main(String[] args) throws IOException {
    Path notes = Path.of("notes.txt");
    Files.writeString(notes, "Java Fundamentals\nFile I/O with NIO\n");
    String text = Files.readString(notes);
    System.out.print("readString:\n" + text);

    Path list = Path.of("team.txt");
    Files.write(list, List.of("Tom", "Harry", "Ethan"));
    List<String> lines = Files.readAllLines(list);
    System.out.println("readAllLines: " + lines + " (" + lines.size() + " lines)");

    byte[] bytes = Files.readAllBytes(list);
    System.out.println("readAllBytes: " + bytes.length + " bytes, file size " + Files.size(list));

    Files.delete(notes);
    Files.delete(list);
  }
}
 OUTPUT of the above code
readString:
Java Fundamentals
File I/O with NIO
readAllLines: [Tom, Harry, Ethan] (3 lines)
readAllBytes: 19 bytes, file size 19
Files.write() ends every line with the operating system's line separator - \r\n on Windows - which is why team.txt has 19 bytes: 13 for the three names, plus three 2-byte separators. On macOS and Linux it would be 16.
Large files - reading lazily with lines() readString() and readAllLines() load the entire file into memory, which is a problem for files of hundreds of megabytes. Files.lines(path) instead returns a Stream<String> which reads the file one line at a time as it is consumed. The stream holds the file open, so use it in try-with-resourcesClick to read tutorial: Java try-with-resources and AutoCloseable Explained.
Processing a file line by line with Files.lines()
//LargeFile.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class LargeFile {
  public static void main(String[] args) throws IOException {
    Path sales = Path.of("sales.csv");
    List<String> rows = new ArrayList<>(List.of("region,amount"));
    for (int i = 1; i <= 100_000; i++) {
      rows.add((i % 3 == 0 ? "north" : i % 3 == 1 ? "south" : "west") + "," + (i % 50));
    }
    Files.write(sales, rows);
    System.out.println("sales.csv: " + Files.size(sales) + " bytes");

    try (Stream<String> lines = Files.lines(sales)) {
      long northTotal = lines.skip(1)                                  // skip the header
          .filter(line -> line.startsWith("north,"))
          .mapToLong(line -> Long.parseLong(line.substring(line.indexOf(',') + 1)))
          .sum();
      System.out.println("north total: " + northTotal);
    }
    Files.delete(sales);
  }
}
 OUTPUT of the above code
sales.csv: 946682 bytes
north total: 816683
The file was never held in memory all at once - each line was read, used and discarded. The Streams API - see Java 8 Streams API TutorialClick to read tutorial on the Streams API - makes filtering and summing such data short. For full control over line-by-line reading and writing, Files.newBufferedReader() and newBufferedWriter() are covered in Byte Streams vs Character StreamsClick to read tutorial: Java Byte Streams vs Character Streams and Buffered I/O. Appending and open options Writing methods accept optional StandardOpenOption values which change how the file is opened -
  • APPEND - add to the end of the file instead of replacing it.
  • CREATE - create the file if needed; CREATE_NEW - create it, but fail if it already exists.
  • TRUNCATE_EXISTING and WRITE - the defaults when no option is given (together with CREATE).
Appending to a file
//AppendLog.java
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class AppendLog {
  static void log(Path file, String message) throws IOException {
    Files.writeString(file, message + System.lineSeparator(),
        StandardOpenOption.CREATE, StandardOpenOption.APPEND);
  }

  public static void main(String[] args) throws IOException {
    Path logFile = Path.of("app.log");
    log(logFile, "started");
    log(logFile, "user Tom logged in");
    log(logFile, "stopped");
    System.out.print(Files.readString(logFile));

    try {
      Files.writeString(logFile, "x", StandardOpenOption.CREATE_NEW);
    } catch (FileAlreadyExistsException e) {
      System.out.println("CREATE_NEW refused: " + e.getMessage() + " already exists");
    }
    Files.delete(logFile);
  }
}
 OUTPUT of the above code
started
user Tom logged in
stopped
CREATE_NEW refused: app.log already exists
Directories, copying, moving and deleting Files also manages files and directories -
  • exists(), isDirectory(), isRegularFile(), size(), getLastModifiedTime() - inspect a path.
  • createDirectory() and createDirectories() - the second also creates any missing parent directories.
  • copy() and move() - with StandardCopyOption.REPLACE_EXISTING to overwrite the target.
  • delete() - fails if the file is missing; deleteIfExists() does not. A directory must be empty before it can be deleted.
  • list() and walk() - the entries of a directory, or of a whole directory tree.
Working with directories and files
//ManageFiles.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 ManageFiles {
  public static void main(String[] args) throws IOException {
    Path project = Path.of("demo-project");
    Path docs = Files.createDirectories(project.resolve("docs").resolve("2026"));
    Path readme = Files.writeString(project.resolve("README.txt"), "Demo project");
    Files.copy(readme, docs.resolve("README-copy.txt"), StandardCopyOption.REPLACE_EXISTING);
    Files.move(readme, project.resolve("ABOUT.txt"), StandardCopyOption.REPLACE_EXISTING);

    System.out.println("README.txt exists: " + Files.exists(readme));
    try (Stream<Path> all = Files.walk(project)) {
      all.filter(p -> !p.equals(project)).forEach(p -> System.out.println((Files.isDirectory(p) ? "[dir]  " : "[file] ") + project.relativize(p)));
    }

    try (Stream<Path> all = Files.walk(project)) {                 // delete the tree, deepest first
      all.sorted(Comparator.reverseOrder()).forEach(p -> {
        try { Files.delete(p); } catch (IOException e) { throw new RuntimeException(e); }
      });
    }
    System.out.println("demo-project exists: " + Files.exists(project));
  }
}
 OUTPUT of the above code (on Windows)
README.txt exists: false
[file] ABOUT.txt
[dir]  docs
[dir]  docs\2026
[file] docs\2026\README-copy.txt
demo-project exists: false
Deleting a directory tree means deleting its contents first, so the paths from walk() are sorted in reverse - the deepest entries come first. More task-focused examples are in How to Copy and Move Files in JavaClick to read tutorial on copying and moving files and How to Delete a File or Directory in JavaClick to read tutorial on deleting files and directories. Handling file exceptions File operations throw IOException or one of its more specific subclasses, so you can react to each problem -
  • NoSuchFileException - the file or directory does not exist (see resolving NoSuchFileExceptionClick to read how to resolve NoSuchFileException).
  • FileAlreadyExistsException - the target already exists.
  • DirectoryNotEmptyException - deleting a directory which still has entries.
  • AccessDeniedException - no permission to read or write.
  • MalformedInputException - the file is not valid in the character set used, such as non-UTF-8 text read with readString().
Catching specific file exceptions
//FileErrors.java
import java.io.IOException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;

public class FileErrors {
  public static void main(String[] args) throws IOException {
    try {
      Files.readString(Path.of("missing.txt"));
    } catch (NoSuchFileException e) {
      System.out.println("No such file: " + e.getFile());
    }

    Path dir = Files.createDirectories(Path.of("full-dir"));
    Files.writeString(dir.resolve("a.txt"), "a");
    try {
      Files.delete(dir);
    } catch (DirectoryNotEmptyException e) {
      System.out.println("Directory not empty: " + e.getFile());
    }
    Files.delete(dir.resolve("a.txt"));
    Files.delete(dir);
    System.out.println("Cleaned up: " + !Files.exists(dir));
  }
}
 OUTPUT of the above code
No such file: missing.txt
Directory not empty: full-dir
Cleaned up: true
Summary In this tutorial we first created and combined Path objects with Path.of(), resolve(), normalize() and relativize(). We then read and wrote whole files with readString(), writeString(), readAllLines() and write(). Next we processed a 100,000-line file lazily with Files.lines(), and appended to a log with StandardOpenOption. We then created directories, copied, moved, listed and deleted files, and lastly caught the specific exceptions file operations throw. In the next tutorial, we will look underneath these methods at byte streams, character streams and buffered I/OClick to read tutorial: Java Byte Streams vs Character Streams and Buffered I/O.