java-io-nio-files-path-lectura-escritura

Files in Java with Path and Files (NIO.2)

  • 4 min

The java.nio.file API provides modern types and operations for working with paths and file systems.

For many years, reading a file in Java involved combining several classes, such as new BufferedReader(new FileReader(new File(...))).

Since Java 7 we have NIO.2, whose path API lives in java.nio.file. The usual Files methods are blocking; asynchronous operations are provided through specific types like AsynchronousFileChannel.

Let’s learn how to handle the file system with Path and Files.

The foundations: Path and Files

In the modern API, there are two absolute protagonists:

The Path interface

It is the modern alternative to java.io.File for representing paths, although both APIs still exist and can be converted between each other. A Path is not the file itself, it is its address on the hard drive. It is a path, which may or may not exist.

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

// Classic way (Java 7+)
Path ruta = Paths.get("datos", "usuarios.txt"); // datos/usuarios.txt

// Modern way (Java 11+)
Path rutaModerna = Path.of("C:", "Proyectos", "notas.txt");

// Path utilities
System.out.println(rutaModerna.getFileName()); // notas.txt
System.out.println(rutaModerna.getParent());   // C:\Proyectos
System.out.println(rutaModerna.toAbsolutePath()); // Resolves the full path
Copied!

Path.of() composes a path with the system separator. Even so, an absolute Windows path does not automatically become a valid Linux or macOS path.

The Files class

It is a static utility class (like Collections or Math) that has methods to do everything you need with a Path.

import java.nio.file.Files;

Path p = Path.of("documento.txt");

// Basic checks
boolean existe = Files.exists(p);
boolean esDirectorio = Files.isDirectory(p);

// Management operations (Require try-catch for IOException)
try {
    if (!existe) {
        Files.createFile(p); // Create empty
    }
    Files.copy(p, Path.of("copia.txt")); // Copy
    Files.move(p, Path.of("movido.txt")); // Move/Rename
    Files.delete(p); // Delete
} catch (IOException e) {
    e.printStackTrace();
}
Copied!

Reading files

This is where the modern API shines. Depending on the file size, we use one strategy or another.

Case a: Small files (all in memory)

If the file is a few KB or MB, we can read it entirely at once.

Path ruta = Path.of("config.json");

try {
    // Read all content as a single String (Java 11+)
    String contenido = Files.readString(ruta);

    // Read all lines as a List of Strings
    List<String> lineas = Files.readAllLines(ruta);

} catch (IOException e) { ... }
Copied!

Beware of RAM! Do not use readAllLines with a file that might exceed available memory. The method loads all lines and could cause an OutOfMemoryError.

Case b: Large files (streams and buffers)

For large files, we must read line by line (stream) without loading everything. Here we combine NIO with the Streams we learned in the previous block.

Path rutaLog = Path.of("server.log"); // Imagine it weighs 10GB

// Use try-with-resources to ensure the file is closed
try (Stream<String> lineas = Files.lines(rutaLog)) {

    lineas.filter(l -> l.contains("ERROR")) // Process on the fly
          .forEach(System.out::println);

} catch (IOException e) { ... }
Copied!

Notice how powerful this is! We are processing a giant log line by line with minimal memory consumption.

Writing files

Writing is equally simple with the Files class.

Path destino = Path.of("salida.txt");
String texto = "Hello world from Java";

try {
    // 1. Simple write (Overwrites if exists)
    Files.writeString(destino, texto);

    // 2. Write a list of lines
    List<String> lista = List.of("Line 1", "Line 2");
    Files.write(destino, lista);

    // 3. Append to the end instead of overwriting
    Files.writeString(destino, "\nNew line", StandardOpenOption.APPEND);

} catch (IOException e) { ... }
Copied!

Serialization: The past vs the present

In old Java tutorials you will see a lot about the Serializable interface.

Java Native Serialization consists of converting an object (instance in memory) into a byte sequence (.ser) to save it to disk and retrieve it later.

// CLASSIC (Discouraged nowadays)
// The class must implement Serializable
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data.ser"))) {
    out.writeObject(myObject);
}
Copied!

Why is it no longer widely used?

  1. It is Java-dependent: A .ser file can only be understood by Java. You cannot read it with Python or JavaScript.
  2. It is fragile: If you change the class (add an attribute), deserialization will fail unless you manage versions (serialVersionUID).
  3. Security: It has been the source of many critical vulnerabilities in Java.

The modern alternative: JSON

Today, to save objects or send them over a network, we use JSON. It is plain text, human-readable, and compatible with any language.

In Java, we use standard external libraries like Jackson or Gson.

// Conceptual example with Jackson
ObjectMapper mapper = new ObjectMapper();

// Save object as JSON to a file
mapper.writeValue(new File("product.json"), myProduct);

// Read JSON to object
Product p = mapper.readValue(new File("product.json"), Product.class);
Copied!

Although JSON does not come natively in the JDK (you have to add the library), it is the de facto standard in the industry.