W3docs

Clase Files de Java NIO

Operaciones de sistema de archivos en Java con java.nio.file.Files: leer, escribir, copiar, mover y recorrer directorios.

Path (el capítulo anterior) era el sustantivo. Files es el verbo — una clase de utilidades estática cuyo cada método recibe un Path y hace algo con el archivo en esa ruta. Es el hogar de los one-liners que han ido acortando el resto de esta sección: Files.readString, Files.newBufferedReader, Files.createTempFile, Files.size. Este capítulo recorre el catálogo completo.

Files es grande — unos 80 métodos — y está agrupado por propósito: leer, escribir, crear, inspeccionar, modificar, recorrer. No es necesario memorizarlo; lo importante es saber que es el primer lugar donde buscar cuando se quiere hacer cualquier cosa con un archivo.

Leer

Los lectores de archivo completo son de una sola línea cada uno:

String   text  = Files.readString(path);                           // UTF-8 by default (Java 11+)
String   utf16 = Files.readString(path, StandardCharsets.UTF_16);
byte[]   bytes = Files.readAllBytes(path);
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);

Para archivos suficientemente pequeños para caber en memoria, readString y readAllBytes son las herramientas adecuadas. Abren el archivo, lo leen por completo, lo cierran y te entregan el contenido. Sin streams, sin buffers, sin lógica de cierre.

Para archivos demasiado grandes para cargar por completo, usa las formas de streaming:

try (BufferedReader r = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
  String line;
  while ((line = r.readLine()) != null) process(line);
}

try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
  lines.filter(...).forEach(...);                                  // closes the file when the stream closes
}

try (InputStream in = Files.newInputStream(path)) {
  // raw bytes for binary formats
}

Files.lines es BufferedReader.lines con la fontanería de apertura y cierre envuelta dentro. El try-with-resources alrededor del Stream realiza el cierre — sin él, el file handle se filtra.

Escribir

Misma forma en el lado de escritura:

Files.writeString(path, "hello\n", StandardCharsets.UTF_8);
Files.write(path, bytes);                                          // byte[]
Files.write(path, lines, StandardCharsets.UTF_8);                  // Iterable<? extends CharSequence>

Los tres son atómicos de una sola llamada: abrir, escribir, cerrar. Por defecto crean o truncan — si el archivo existía, su contenido anterior desaparece. Para añadir al final:

Files.writeString(path, "more\n", StandardCharsets.UTF_8, StandardOpenOption.APPEND);

Para la forma de streaming (escritura incremental):

try (BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
  for (String line : lines) w.write(line);
}

Opciones de apertura

Cada método de lectura/escritura que abre un archivo acepta un varargs opcional de StandardOpenOption:

OpciónSignificado
READAbrir para lectura
WRITEAbrir para escritura
CREATECrear si no existe; no hacer nada si existe
CREATE_NEWCrear si no existe; fallar si existe
APPENDLas escrituras van al final del archivo
TRUNCATE_EXISTINGLimpiar el contenido al abrir
DELETE_ON_CLOSEEliminar cuando el canal se cierra (archivos temporales)
SYNC / DSYNCBloquear las escrituras hasta que el SO confirme que los datos están en disco

El modo de apertura predeterminado para newBufferedWriter y writeString es CREATE, TRUNCATE_EXISTING, WRITE. El predeterminado para newBufferedReader y readString es READ. Las opciones explícitas anulan los valores predeterminados — pasar cualquier opción desactiva el conjunto implícito, por lo que normalmente hay que repetir los implícitos al personalizar:

Files.newBufferedWriter(path, StandardCharsets.UTF_8,
    StandardOpenOption.CREATE,
    StandardOpenOption.APPEND);                                    // appends, creates if absent

Crear

Files.createFile(path);                                            // empty file; fails if it exists
Files.createDirectory(path);                                       // single dir; fails if parent absent
Files.createDirectories(path);                                     // recursive: like `mkdir -p`
Files.createSymbolicLink(link, target);
Files.createLink(link, target);                                    // hard link

Path tmpFile = Files.createTempFile("prefix-", ".txt");            // in the default temp dir
Path tmpDir  = Files.createTempDirectory("prefix-");

createDirectories es la herramienta adecuada para "quiero que este directorio exista." Es idempotente: si el directorio ya existe, retorna sin error; si falta algún ancestro, crea toda la cadena. createDirectory (sin -ies) solo crea un nivel y falla si el padre no existe — casi siempre es incorrecto a menos que específicamente necesites esa comprobación.

Para archivos temporales, las sobrecargas de createTempFile y createTempDirectory eligen automáticamente el directorio temporal del sistema y devuelven el Path que crearon. Combínalos con .toFile().deleteOnExit() para la limpieza, o haz un Files.delete explícito en un finally.

Inspeccionar

Los predicados y accesores:

boolean ok    = Files.exists(path);
boolean nope  = Files.notExists(path);                             // NOT the negation of exists
boolean file  = Files.isRegularFile(path);
boolean dir   = Files.isDirectory(path);
boolean link  = Files.isSymbolicLink(path);
boolean read  = Files.isReadable(path);
boolean write = Files.isWritable(path);
boolean exec  = Files.isExecutable(path);

long size                  = Files.size(path);                     // throws IOException
FileTime mtime             = Files.getLastModifiedTime(path);
String mimeType            = Files.probeContentType(path);         // best-effort, can return null
UserPrincipal owner        = Files.getOwner(path);

exists y notExists no son negaciones: ambos pueden devolver false cuando no se puede determinar el acceso al archivo (permiso denegado, enlace simbólico colgante). Usa el correcto según lo que necesites — !exists(p) y notExists(p) difieren en casos límite.

Copiar, mover, eliminar

Files.copy(source, target);                                        // fails if target exists
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.copy(source, target,
    StandardCopyOption.REPLACE_EXISTING,
    StandardCopyOption.COPY_ATTRIBUTES);                            // copy mtime/owner too

Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);        // rename within a filesystem; rename-or-fail

Files.delete(path);                                                // throws if absent
boolean deleted = Files.deleteIfExists(path);                       // idempotent

Files.move con ATOMIC_MOVE es la herramienta adecuada para "escribir en un archivo temporal y luego reemplazar atómicamente el archivo en producción." En el mismo sistema de archivos se mapea a rename(2); el archivo en producción cambia de antiguo a nuevo en un instante, sin estado intermedio. Así es como se construyen escrituras seguras ante fallos:

Path tmp = path.resolveSibling(path.getFileName() + ".tmp");
Files.writeString(tmp, content, StandardCharsets.UTF_8);
Files.move(tmp, path, StandardCopyOption.ATOMIC_MOVE,
    StandardCopyOption.REPLACE_EXISTING);

Si la JVM muere después de writeString pero antes de move, el archivo en producción queda intacto.

Listar y recorrer

try (Stream<Path> entries = Files.list(directory)) {
  entries.forEach(System.out::println);                            // direct children only
}

try (Stream<Path> tree = Files.walk(directory)) {
  tree.filter(Files::isRegularFile).forEach(...);                  // recursive
}

try (Stream<Path> tree = Files.walk(directory, 2)) {               // depth-limited
  ...
}

try (Stream<Path> found = Files.find(directory, Integer.MAX_VALUE,
    (p, attrs) -> attrs.isRegularFile() && p.toString().endsWith(".log"))) {
  ...
}

Usa siempre try-with-resources alrededor de estos — el DirectoryStream subyacente está abierto hasta que el Stream se cierre. Si omites el cierre, la JVM mantiene un manejador de directorio hasta que el recolector de basura lo note, lo cual en un proceso de larga ejecución es "nunca." El próximo capítulo, Java Walk File Tree, profundiza en el recorredor.

Por qué este capítulo es breve

Files no necesita mucha narrativa. Cada método hace una cosa, los nombres son descriptivos, los parámetros son Path, Charset y Option. La carga cognitiva está en el catálogo — saber qué está disponible — no en el comportamiento de ningún método individual. Revisa el Javadoc de java.nio.file.Files una vez; vuelve cuando necesites un verbo que no recuerdas.

Un ejemplo práctico: el ciclo de vida completo

El programa a continuación crea un directorio temporal, escribe un pequeño archivo de texto con writeString, lo lee de nuevo con readString, añade contenido con la opción de apertura correcta, copia el archivo, lo mueve atómicamente, lista el directorio en cada paso y finalmente lo limpia con deleteIfExists. Es el ciclo de vida cotidiano de archivos en Java comprimido en un único método main.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • Files.writeString(...) abrió el archivo, escribió el contenido y lo cerró — una sola llamada donde java.io hubiera necesitado FileOutputStream + OutputStreamWriter(UTF-8) + BufferedWriter + try-with-resources. El comportamiento predeterminado de truncar al abrir es exactamente lo que "guardar este contenido" necesita. Cuando se necesita conservar el contenido existente, el StandardOpenOption.APPEND explícito (pasado junto con WRITE) es la forma de anularlo.
  • Files.lines(log).filter(...) hizo el mismo trabajo de lectura en streaming que BufferedReader.lines() con la fontanería de apertura y cierre envuelta dentro. El try-with-resources alrededor del Stream es el mecanismo de cierre — omitirlo provoca una fuga del file handle. Cada método de Files que devuelve un Stream es cerrable; trátalo así.
  • El paso de copia usó tanto REPLACE_EXISTING (permitir sobreescritura) como COPY_ATTRIBUTES (conservar mtime/propietario). Sin COPY_ATTRIBUTES la copia de seguridad tendría un mtime nuevo, lo que importa para comprobaciones de "¿sigue siendo actual esta copia de seguridad?". Files.copy usa el comportamiento conservador por defecto; hay que indicar explícitamente cualquier otra opción.
  • El bloque de movimiento atómico es el patrón de escritura segura: escribe el contenido en target.tmp, luego usa ATOMIC_MOVE para colocarlo sobre el nombre en producción. Si la JVM falla a mitad de la escritura, el archivo en producción queda sin cambios; si el renombrado tiene éxito, el archivo en producción cambia en un instante. En el mismo sistema de archivos esto se mapea a rename(2) — no hay paso de copia. Usa esto para cualquier archivo en el que los lectores nunca deban ver un estado escrito a medias (configuración, archivos de guardado, activos generados).
  • Files.walk(dir) produjo un Stream<Path> de cada entrada bajo el directorio en orden de profundidad primero. La limpieza en el paso 10 ordenó en sentido inverso para que los hijos se eliminaran antes que los padres — el mismo truco que se usaría con un borrado recursivo real. (El helper completo de borrado de árbol se encuentra en el próximo capítulo bajo walkFileTree; la forma de streaming aquí es la versión más corta para árboles pequeños.)

Qué viene a continuación

Files cubrió las operaciones que actúan sobre un solo archivo o un solo nivel de directorio. El próximo capítulo, Java Walk File Tree, profundiza en el recorrido de un árbol de directorios completo — Files.walkFileTree, FileVisitor, omisión de subárboles, la API de patrón visitante que maneja los casos que la forma con Stream no puede.

Práctica

Práctica
Quieres sobreescribir el archivo `/var/data/config.json` con un nuevo payload, pero los lectores nunca deben ver un estado escrito a medias si la JVM falla durante la escritura. ¿Qué secuencia de llamadas a `Files` implementa el patrón de escritura segura?
Quieres sobreescribir el archivo `/var/data/config.json` con un nuevo payload, pero los lectores nunca deben ver un estado escrito a medias si la JVM falla durante la escritura. ¿Qué secuencia de llamadas a `Files` implementa el patrón de escritura segura?
Was this page helpful?