Eliminar archivos en Java
Elimina archivos y directorios en Java con File.delete, Files.delete y Files.deleteIfExists. Guía práctica con ejemplos.
Tres llamadas pequeñas, una gran diferencia. File.delete() devuelve un boolean para todo, desde éxito hasta permiso denegado; Files.delete() lanza excepciones específicas para cada fallo; Files.deleteIfExists() es la opción intermedia: boolean solo para la pregunta "¿eliminé algo?", excepciones para los fallos reales. Elegir la correcta depende principalmente de cuánto te importa por qué falló la eliminación.
También se cubre: eliminar un árbol de directorios no vacío (ninguna de las tres llamadas hace esto por sí sola), la opción de apertura DELETE_ON_CLOSE para archivos temporales, y el patrón seguro "mover y luego eliminar" para reemplazar un archivo de forma atómica.
File.delete() — heredado, devuelve boolean
File f = new File("notes.txt");
boolean ok = f.delete(); // true on success
// false on every failure: missing, locked, perms, non-empty dirLa brecha del legado, con la misma forma que mkdir y renameTo: un único boolean para cada resultado. La llamada devuelve false si el archivo no existía, si carecías de permiso, si la ruta era un directorio no vacío o, en Windows, si otro proceso tenía el archivo abierto. No puedes saber cuál fue la causa a partir del valor de retorno.
File.delete() elimina:
- Un archivo regular.
- Un directorio vacío. Los directorios no vacíos devuelven
false. - El propio enlace simbólico (no el destino).
Si solo necesitas que "el archivo haya desaparecido al final de la llamada", comprueba f.exists() después en lugar de confiar en el valor de retorno:
f.delete();
if (f.exists()) throw new IOException("could not delete " + f);Ese patrón es el que encontrarás reemplazándolo en la mayoría de las bases de código más antiguas.
Files.delete(path) — moderno, lanza excepciones
El equivalente de java.nio.file cambia el boolean por excepciones específicas:
Path p = Path.of("notes.txt");
Files.delete(p);
// throws NoSuchFileException — the file didn't exist
// throws DirectoryNotEmptyException — path was a non-empty directory
// throws AccessDeniedException — permission denied
// throws IOException — anything else (locked, OS error, network FS hiccup)Los tipos de excepción son subclases de IOException, por lo que un catch (IOException e) genérico sigue funcionando. La diferencia es que el manejo real de errores puede ser específico:
try {
Files.delete(path);
} catch (NoSuchFileException e) {
// already gone — not an error in the "delete if there" pattern
} catch (DirectoryNotEmptyException e) {
// need a recursive delete; handled below
}Si "ya no existe" es aceptable, usa Files.deleteIfExists en lugar de capturar NoSuchFileException.
Files.deleteIfExists(path) — la opción intermedia
boolean deleted = Files.deleteIfExists(path);
// returns true — the file existed and was deleted
// returns false — the file did not exist
// throws DirectoryNotEmptyException, AccessDeniedException, etc. for real failuresEl boolean aquí solo distingue "eliminé algo" de "nada que eliminar". Los errores reales siguen lanzando excepciones. Esa es la llamada que quieres para código de setUp / tearDown, limpieza idempotente y patrones del tipo "eliminar este marcador antiguo si está ahí":
Files.deleteIfExists(Path.of("lock")); // safe whether the lock was there or notUsa la tabla:
| Quieres… | Usa |
|---|---|
| Boolean heredado, sin información de error | File.delete() |
| "Elimina o dime por qué falló" | Files.delete(path) |
| "Elimina si existe; silencio si no" | Files.deleteIfExists(path) |
Eliminar un árbol de directorios no vacío
Ninguno de los métodos de eliminación de una sola llamada elimina un directorio no vacío. Hay dos patrones estándar:
Patrón 1: recorrer y eliminar en orden inverso. Files.walk produce un Stream<Path> en orden arbitrario; ordenar en orden inverso coloca las hojas al frente, de modo que cada directorio padre esté vacío cuando lo alcanzas.
try (Stream<Path> walk = Files.walk(root)) {
walk.sorted(Comparator.reverseOrder())
.forEach(p -> {
try { Files.delete(p); } catch (IOException e) { throw new UncheckedIOException(e); }
});
}Conciso y la versión que usa la mayoría del código hoy en día. La desventaja: carga todas las rutas en el orden. Para directorios con millones de entradas, prefiere el patrón 2.
Patrón 2: Files.walkFileTree con un SimpleFileVisitor. El patrón visitante te permite eliminar hojas al visitarlas y padres en postVisitDirectory, sin necesidad de ordenar:
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override public FileVisitResult visitFile(Path file, BasicFileAttributes a) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});Mismo resultado, sin ordenamiento en memoria, más líneas. La API de visitantes se cubre en el capítulo Walk file tree.
No existe ningún rmrf incorporado en el JDK. Ambos patrones anteriores son los sustitutos estándar; muchas bases de código incluyen un pequeño helper Files.deleteRecursively(root) sobre uno de ellos.
DELETE_ON_CLOSE — archivos temporales que se limpian solos
Para "necesito un archivo solo mientras este stream esté abierto", la opción StandardOpenOption.DELETE_ON_CLOSE elimina el archivo cuando el stream se cierra, incluso en la ruta de excepción:
Path scratch = Files.createTempFile("work-", ".tmp");
try (BufferedWriter w = Files.newBufferedWriter(scratch,
StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE)) {
// ... write to and read from scratch ...
} // scratch is gone after this brace, regardless of how we got hereEl archivo se desvincula del directorio inmediatamente en la mayoría de los sistemas Unix (otros procesos ya no pueden verlo por nombre; solo el handle lo mantiene vivo). Ese es el patrón correcto para datos temporales de corta duración: no hay plomería try/finally que recordar.
File.deleteOnExit() es la versión más antigua y débil: encola una eliminación para que se ejecute durante el cierre de la JVM. No se llama con kill -9 ni ante un fallo de la JVM, por lo que produce fugas. Usa DELETE_ON_CLOSE cuando la vida del archivo está vinculada a un stream; usa try/finally (o try-with-resources alrededor de un titular AutoCloseable) cuando no lo está.
Una nota sobre "reemplazo atómico"
Reemplazar un archivo con otro de forma atómica — de modo que un lector nunca vea una versión a medio escribir — no es un patrón de eliminar-y-escribir. El idioma estándar es "escribir en un hermano y luego renombrar atómicamente":
Path target = Path.of("data.json");
Path tmp = target.resolveSibling("data.json.tmp");
Files.writeString(tmp, payload);
Files.move(tmp, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);ATOMIC_MOVE intercambia las dos rutas en un único paso del SO (en sistemas de archivos que lo admiten). El data.json antiguo es reemplazado; no existe ningún momento intermedio en que el archivo esté a medio escribir o ausente.
Un ejemplo completo: cada método de eliminación y un desmontaje recursivo
El programa a continuación construye un pequeño árbol y luego ejercita cada método de eliminación: el boolean heredado, la versión moderna que lanza excepciones, la opción intermedia "si existe" y finalmente el patrón Files.walk + reverseOrder que elimina un árbol completo. Cada paso imprime lo que ocurrió.
Qué extraer de la ejecución:
- Las dos llamadas a
File.delete()sobrea.txtdevolvierontruey luegofalse. El segundofalseparece idéntico a un fallo de permisos — esa es la brecha del legado. Files.delete(sub)lanzóDirectoryNotEmptyExceptionyFiles.delete(missing)lanzóNoSuchFileException. Dos subclases específicas deIOException, dos modos de fallo distintos — exactamente lo que la API boolean no puede decirte.Files.deleteIfExists(b)devolviótruela primera vez yfalsela segunda. Ese segundofalsees solo "no estaba ahí" — un fallo real (permiso denegado, bloqueo) habría lanzado una excepción.- El bloque
Files.walk + reverseOrdereliminó las hojas primero y los padres al final. Cada llamada aFiles.deletea lo largo del camino tuvo éxito porque, cuando el visitante alcanzó un directorio, sus hijos ya habían sido eliminados. - El archivo
DELETE_ON_CLOSEhabía desaparecido en el momento en que el escritor se cerró — garantizado incluso en la ruta de excepción. (En la mayoría de los sistemas Unix se desvincula del directorio inmediatamente al abrirse, por lo queFiles.existspuede reportarfalsedentro deltry; en Windows sobrevive hasta que el handle se cierra. De cualquier forma, no queda nada tras el cierre.) Ese es el patrón de archivo temporal más limpio del JDK: sin shutdown hook, sintry/finallyque recordar.
Qué sigue
Con esto se cierran los capítulos de alto nivel sobre "hacer una sola cosa a un archivo". El próximo capítulo, Byte Streams in Java, desciende un nivel: InputStream y OutputStream, la abstracción orientada a bytes en crudo sobre la que se construye cada archivo, socket y pipe en java.io. Muchos de los helpers que has usado hasta ahora — Files.readString, Files.newBufferedWriter, incluso FileReader — son decoradores sobre esas dos interfaces.