Java
Tutorial Java: Clases de NIO2
Aprende a usar las clases Path y Files de Java NIO2 para gestionar rutas, archivos y directorios de forma eficiente y moderna.
Aprende Java y certifícatePath y Paths para representación de rutas
La gestión de archivos y directorios es una tarea fundamental en cualquier aplicación. En Java, la API NIO2 (New Input/Output 2) introducida en Java 7 proporciona clases modernas para trabajar con el sistema de archivos de manera más eficiente y con mayor funcionalidad que las clases tradicionales de Java IO.
La interfaz Path
y la clase utilitaria Paths
son los componentes fundamentales de NIO2 para representar y manipular rutas de archivos y directorios en el sistema de archivos.
La interfaz Path
La interfaz Path
representa una ruta en el sistema de archivos. A diferencia de la clase File
del paquete java.io
, Path
es una interfaz que ofrece métodos más potentes y flexibles para trabajar con rutas.
Un objeto Path
representa una secuencia de nombres de directorios que culminan en un nombre de archivo o directorio. Es importante entender que un objeto Path
no representa el archivo en sí, sino solo su ubicación en el sistema de archivos.
Para utilizar Path
, necesitamos importar el paquete correspondiente:
import java.nio.file.Path;
La clase Paths
La clase Paths
es una clase utilitaria que proporciona métodos estáticos para obtener instancias de Path
. El método más común es get()
, que convierte una cadena de texto o URI en un objeto Path
.
import java.nio.file.Paths;
Creación de objetos Path
Existen varias formas de crear un objeto Path
:
// Usando Paths.get() con una cadena
Path ruta1 = Paths.get("archivo.txt");
// Usando Paths.get() con múltiples cadenas (elementos de la ruta)
Path ruta2 = Paths.get("/home", "usuario", "documentos", "archivo.txt");
// Usando Paths.get() con una ruta completa
Path ruta3 = Paths.get("/home/usuario/documentos/archivo.txt");
// Usando URI
Path ruta4 = Paths.get(URI.create("file:///home/usuario/documentos/archivo.txt"));
A partir de Java 11, también podemos usar el método of()
de la interfaz Path
:
// Usando Path.of() (Java 11+)
Path ruta5 = Path.of("archivo.txt");
Path ruta6 = Path.of("/home", "usuario", "documentos", "archivo.txt");
Rutas absolutas y relativas
Una ruta absoluta contiene toda la información necesaria para localizar un archivo, incluyendo la raíz del sistema de archivos. Una ruta relativa necesita combinarse con otra ruta para identificar un archivo.
// Ruta absoluta
Path rutaAbsoluta = Paths.get("/home/usuario/documentos/archivo.txt");
// Ruta relativa (relativa al directorio de trabajo actual)
Path rutaRelativa = Paths.get("documentos/archivo.txt");
// Comprobar si una ruta es absoluta
boolean esAbsoluta = rutaAbsoluta.isAbsolute(); // true
boolean esRelativa = rutaRelativa.isAbsolute(); // false
// Convertir una ruta relativa a absoluta
Path rutaRelativaAAbsoluta = rutaRelativa.toAbsolutePath();
Operaciones básicas con Path
La interfaz Path
proporciona numerosos métodos para manipular y obtener información sobre rutas:
Path ruta = Paths.get("/home/usuario/documentos/archivo.txt");
// Obtener el nombre del archivo
Path nombreArchivo = ruta.getFileName(); // archivo.txt
// Obtener el directorio padre
Path directorioPadre = ruta.getParent(); // /home/usuario/documentos
// Obtener el número de elementos en la ruta
int elementos = ruta.getNameCount(); // 4
// Obtener un elemento específico de la ruta (índice base 0)
Path elemento = ruta.getName(2); // documentos
// Obtener una subruta
Path subruta = ruta.subpath(1, 3); // usuario/documentos
Normalización y resolución de rutas
NIO2 proporciona métodos para normalizar rutas (eliminar redundancias) y resolver rutas relativas:
// Normalizar una ruta (eliminar elementos como "." y "..")
Path rutaRedundante = Paths.get("/home/./usuario/../usuario/documentos/archivo.txt");
Path rutaNormalizada = rutaRedundante.normalize(); // /home/usuario/documentos/archivo.txt
// Resolver una ruta relativa contra una ruta base
Path base = Paths.get("/home/usuario");
Path relativa = Paths.get("documentos/archivo.txt");
Path resuelta = base.resolve(relativa); // /home/usuario/documentos/archivo.txt
// Calcular la ruta relativa entre dos rutas
Path ruta1 = Paths.get("/home/usuario/documentos");
Path ruta2 = Paths.get("/home/usuario/fotos");
Path rutaRelativaEntre = ruta1.relativize(ruta2); // ../fotos
Comparación de rutas
Podemos comparar rutas de varias maneras:
Path ruta1 = Paths.get("/home/usuario/archivo.txt");
Path ruta2 = Paths.get("/home/usuario/archivo.txt");
Path ruta3 = Paths.get("/home/usuario/ARCHIVO.txt");
// Comparación de igualdad
boolean sonIguales = ruta1.equals(ruta2); // true
// Comparación de contenido de archivos (en sistemas de archivos que distinguen mayúsculas/minúsculas)
boolean mismoArchivo = ruta1.equals(ruta3); // false en sistemas que distinguen mayúsculas/minúsculas
// Comparación de rutas normalizadas
Path rutaA = Paths.get("/home/usuario/../usuario/archivo.txt");
Path rutaB = Paths.get("/home/usuario/archivo.txt");
boolean sonEquivalentes = rutaA.normalize().equals(rutaB); // true
Conversión entre Path y File
Si necesitamos interoperar con código que utiliza la antigua clase File
, podemos convertir fácilmente entre Path
y File
:
// Convertir de File a Path
File archivo = new File("/home/usuario/archivo.txt");
Path ruta = archivo.toPath();
// Convertir de Path a File
Path ruta2 = Paths.get("/home/usuario/documento.txt");
File archivo2 = ruta2.toFile();
Manejo de rutas en diferentes sistemas operativos
Una ventaja importante de Path
es que maneja correctamente las diferencias entre sistemas operativos:
// En Windows, esto se interpretará correctamente con barras invertidas
Path rutaWindows = Paths.get("C:\\Users\\usuario\\documentos\\archivo.txt");
// También podemos usar barras normales en Windows y Java las convertirá
Path rutaWindowsAlternativa = Paths.get("C:/Users/usuario/documentos/archivo.txt");
// En sistemas Unix/Linux
Path rutaUnix = Paths.get("/home/usuario/documentos/archivo.txt");
Obtención de información del sistema de archivos
Podemos obtener información sobre el sistema de archivos utilizando la clase FileSystem
:
// Obtener el separador de rutas del sistema
String separador = FileSystems.getDefault().getSeparator(); // "/" en Unix, "\" en Windows
// Obtener las raíces del sistema de archivos
Iterable<Path> raices = FileSystems.getDefault().getRootDirectories();
for (Path raiz : raices) {
System.out.println(raiz);
}
La interfaz Path
y la clase Paths
son fundamentales para trabajar con archivos y directorios en Java moderno, proporcionando una base sólida para las operaciones de entrada/salida que veremos en las siguientes secciones con la clase Files
.
Clase Files para operaciones comunes
La clase Files
es uno de los componentes más importantes de la API NIO2, proporcionando métodos estáticos para realizar operaciones comunes sobre archivos y directorios. Esta clase trabaja en conjunto con la interfaz Path
que vimos anteriormente para ofrecer una forma moderna y eficiente de manipular el sistema de archivos.
Para utilizar la clase Files
, necesitamos importarla:
import java.nio.file.Files;
Verificación de existencia y tipo
La clase Files
proporciona métodos para verificar si una ruta existe y determinar su tipo:
Path ruta = Paths.get("/home/usuario/documento.txt");
// Verificar si existe
boolean existe = Files.exists(ruta);
// Verificar si es un archivo regular
boolean esArchivo = Files.isRegularFile(ruta);
// Verificar si es un directorio
boolean esDirectorio = Files.isDirectory(ruta);
// Verificar si es un enlace simbólico
boolean esEnlace = Files.isSymbolicLink(ruta);
Estos métodos aceptan opcionalmente opciones de enlace que determinan cómo se manejan los enlaces simbólicos:
// No seguir enlaces simbólicos (verificar el enlace en sí)
boolean existeEnlace = Files.exists(ruta, LinkOption.NOFOLLOW_LINKS);
Acceso a atributos de archivos
La clase Files
permite obtener y modificar atributos de archivos y directorios:
// Verificar permisos
boolean esLegible = Files.isReadable(ruta);
boolean esEscribible = Files.isWritable(ruta);
boolean esEjecutable = Files.isExecutable(ruta);
// Obtener tamaño del archivo en bytes
long tamaño = Files.size(ruta);
// Obtener última fecha de modificación
FileTime ultimaModificacion = Files.getLastModifiedTime(ruta);
// Modificar última fecha de modificación
Files.setLastModifiedTime(ruta, FileTime.fromMillis(System.currentTimeMillis()));
// Obtener propietario
UserPrincipal propietario = Files.getOwner(ruta);
Para obtener múltiples atributos de manera eficiente, podemos usar readAttributes
:
// Obtener atributos básicos
BasicFileAttributes atributos = Files.readAttributes(ruta, BasicFileAttributes.class);
System.out.println("Fecha de creación: " + atributos.creationTime());
System.out.println("Última modificación: " + atributos.lastModifiedTime());
System.out.println("Tamaño: " + atributos.size() + " bytes");
System.out.println("Es directorio: " + atributos.isDirectory());
System.out.println("Es archivo regular: " + atributos.isRegularFile());
Gestión de permisos de archivos
Podemos modificar los permisos de archivos utilizando la clase PosixFilePermission
en sistemas compatibles con POSIX (Linux, macOS):
// Solo en sistemas compatibles con POSIX
if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
Path archivo = Paths.get("/home/usuario/documento.txt");
// Obtener permisos actuales
Set<PosixFilePermission> permisos = Files.getPosixFilePermissions(archivo);
// Añadir permiso de escritura para el grupo
permisos.add(PosixFilePermission.GROUP_WRITE);
// Aplicar los nuevos permisos
Files.setPosixFilePermissions(archivo, permisos);
}
Copia y movimiento de archivos
La clase Files
proporciona métodos para copiar y mover archivos:
Path origen = Paths.get("archivo_original.txt");
Path destino = Paths.get("archivo_copia.txt");
// Copiar un archivo
Files.copy(origen, destino);
// Copiar reemplazando si el destino ya existe
Files.copy(origen, destino, StandardCopyOption.REPLACE_EXISTING);
// Mover un archivo (renombrar)
Files.move(origen, destino);
// Mover reemplazando si el destino ya existe
Files.move(origen, destino, StandardCopyOption.REPLACE_EXISTING);
Estos métodos aceptan varias opciones para personalizar el comportamiento:
// Copiar manteniendo atributos
Files.copy(origen, destino,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
// Mover de manera atómica cuando sea posible
Files.move(origen, destino,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
Creación de archivos y directorios
La clase Files
ofrece métodos para crear archivos y directorios:
// Crear un archivo vacío
Path nuevoArchivo = Paths.get("nuevo_archivo.txt");
Files.createFile(nuevoArchivo);
// Crear un directorio
Path nuevoDirectorio = Paths.get("nuevo_directorio");
Files.createDirectory(nuevoDirectorio);
// Crear directorios incluyendo padres (similar a mkdir -p)
Path rutaCompleta = Paths.get("directorio/subdirectorio/otro");
Files.createDirectories(rutaCompleta);
// Crear un archivo temporal
Path temporal = Files.createTempFile("prefijo_", "_sufijo");
// Crear un directorio temporal
Path dirTemporal = Files.createTempDirectory("prefijo_");
Eliminación de archivos y directorios
Para eliminar archivos y directorios:
// Eliminar un archivo o directorio vacío
Files.delete(ruta);
// Eliminar si existe (no lanza excepción si no existe)
boolean eliminado = Files.deleteIfExists(ruta);
Manejo de enlaces simbólicos
La clase Files
permite crear y trabajar con enlaces simbólicos:
Path objetivo = Paths.get("archivo_original.txt");
Path enlace = Paths.get("enlace_simbolico.txt");
// Crear un enlace simbólico
Files.createSymbolicLink(enlace, objetivo);
// Leer el destino de un enlace simbólico
Path destinoEnlace = Files.readSymbolicLink(enlace);
Obtención de tipo MIME
Podemos determinar el tipo MIME de un archivo:
Path archivo = Paths.get("documento.pdf");
String tipoMime = Files.probeContentType(archivo);
System.out.println("Tipo MIME: " + tipoMime); // application/pdf
Manejo de árboles de directorios
La clase Files
proporciona métodos para recorrer árboles de directorios:
Path directorio = Paths.get("/home/usuario/documentos");
// Listar el contenido de un directorio (solo un nivel)
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directorio)) {
for (Path entrada : stream) {
System.out.println(entrada.getFileName());
}
}
// Listar solo archivos que coincidan con un patrón
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directorio, "*.{txt,pdf}")) {
for (Path entrada : stream) {
System.out.println(entrada.getFileName());
}
}
Para recorridos más complejos, podemos usar walkFileTree
:
Path inicio = Paths.get("/home/usuario/documentos");
Files.walkFileTree(inicio, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("Archivo: " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("Entrando al directorio: " + dir);
return FileVisitResult.CONTINUE;
}
});
Observación de cambios en el sistema de archivos
La API NIO2 permite monitorear cambios en el sistema de archivos:
Path directorio = Paths.get("/home/usuario/documentos");
WatchService watchService = FileSystems.getDefault().newWatchService();
// Registrar eventos que queremos monitorear
directorio.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
// Bucle para procesar eventos
while (true) {
WatchKey key = watchService.take(); // Bloquea hasta que ocurra un evento
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
Path nombreArchivo = (Path) event.context();
System.out.println(kind.name() + ": " + nombreArchivo);
}
// Restablecer la clave para recibir más eventos
boolean valid = key.reset();
if (!valid) {
break; // El directorio ya no es accesible
}
}
La clase Files
proporciona una API completa y moderna para trabajar con archivos y directorios en Java. Combinada con la interfaz Path
, ofrece una solución robusta para todas las operaciones comunes del sistema de archivos, con un diseño más limpio y funcional que las antiguas clases de Java IO.
Creación, lectura, escritura y eliminación de archivos
La API NIO2 de Java proporciona métodos eficientes para las operaciones fundamentales con archivos: crear, leer, escribir y eliminar. Estas operaciones se realizan principalmente a través de la clase Files
en combinación con objetos Path
que ya hemos estudiado.
Creación de archivos
Existen varias formas de crear archivos utilizando la API NIO2:
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
public class CreacionArchivos {
public static void main(String[] args) {
try {
// Método 1: Crear un archivo vacío
Path archivo1 = Paths.get("datos/archivo1.txt");
Files.createFile(archivo1);
// Método 2: Crear un archivo con contenido inicial
Path archivo2 = Paths.get("datos/archivo2.txt");
Files.writeString(archivo2, "Contenido inicial del archivo");
// Método 3: Crear un archivo temporal
Path archivoTemp = Files.createTempFile("prefijo_", "_sufijo");
System.out.println("Archivo temporal creado en: " + archivoTemp);
} catch (IOException e) {
System.err.println("Error al crear archivos: " + e.getMessage());
}
}
}
Al crear archivos, es importante tener en cuenta:
- Si intentamos crear un archivo que ya existe, se lanzará una excepción
FileAlreadyExistsException
. - Si el directorio padre no existe, se lanzará una excepción
NoSuchFileException
. - Podemos asegurarnos de que el directorio padre exista antes de crear el archivo:
Path ruta = Paths.get("datos/subdirectorio/archivo.txt");
Files.createDirectories(ruta.getParent());
Files.createFile(ruta);
Lectura de archivos
NIO2 ofrece múltiples métodos para leer el contenido de archivos, desde los más simples hasta los más avanzados:
Lectura completa de archivos pequeños
Para archivos de tamaño moderado, podemos leer todo el contenido de una vez:
// Leer todo el contenido como una cadena de texto
Path archivo = Paths.get("datos/ejemplo.txt");
String contenido = Files.readString(archivo);
System.out.println(contenido);
// Leer todo el contenido como una lista de líneas
List<String> lineas = Files.readAllLines(archivo);
for (String linea : lineas) {
System.out.println(linea);
}
// Leer todo el contenido como un array de bytes
byte[] datos = Files.readAllBytes(archivo);
Estos métodos son convenientes pero no recomendados para archivos grandes ya que cargan todo el contenido en memoria.
Lectura eficiente de archivos grandes
Para archivos de mayor tamaño, es preferible utilizar streams o buffers:
// Usando BufferedReader para leer línea por línea
try (BufferedReader reader = Files.newBufferedReader(archivo)) {
String linea;
while ((linea = reader.readLine()) != null) {
System.out.println(linea);
}
}
// Usando Stream de líneas (Java 8+)
try (Stream<String> stream = Files.lines(archivo)) {
stream.forEach(System.out::println);
}
Lectura de archivos binarios
Para archivos binarios, podemos usar canales o streams de bytes:
// Usando InputStream
try (InputStream in = Files.newInputStream(archivo)) {
byte[] buffer = new byte[1024];
int bytesLeidos;
while ((bytesLeidos = in.read(buffer)) != -1) {
// Procesar los bytes leídos
System.out.println("Leídos " + bytesLeidos + " bytes");
}
}
// Usando ByteChannel
try (SeekableByteChannel channel = Files.newByteChannel(archivo)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (channel.read(buffer) > 0) {
buffer.flip();
// Procesar los datos en el buffer
buffer.clear();
}
}
Escritura de archivos
Al igual que con la lectura, NIO2 proporciona varios métodos para escribir en archivos:
Escritura simple
Para casos sencillos, podemos escribir todo el contenido de una vez:
// Escribir una cadena de texto
Path archivo = Paths.get("datos/salida.txt");
Files.writeString(archivo, "Contenido del archivo");
// Escribir una lista de líneas
List<String> lineas = Arrays.asList("Línea 1", "Línea 2", "Línea 3");
Files.write(archivo, lineas);
// Escribir bytes
byte[] datos = "Datos binarios".getBytes();
Files.write(archivo, datos);
Por defecto, estos métodos sobrescriben el archivo si ya existe. Para añadir contenido en lugar de sobrescribir, podemos usar opciones adicionales:
// Añadir contenido al final del archivo
Files.writeString(archivo, "\nNueva línea al final", StandardOpenOption.APPEND);
// Añadir una lista de líneas al final
Files.write(archivo, lineas, StandardOpenOption.APPEND);
Escritura eficiente para archivos grandes
Para archivos más grandes o escritura más controlada:
// Usando BufferedWriter
try (BufferedWriter writer = Files.newBufferedWriter(archivo)) {
writer.write("Primera línea");
writer.newLine();
writer.write("Segunda línea");
}
// Usando OutputStream
try (OutputStream out = Files.newOutputStream(archivo)) {
out.write("Datos de ejemplo".getBytes());
}
Opciones de escritura avanzadas
La clase StandardOpenOption
proporciona varias opciones para personalizar el comportamiento de escritura:
// Crear un archivo nuevo, lanzando excepción si ya existe
Files.writeString(archivo, "Contenido", StandardOpenOption.CREATE_NEW);
// Crear si no existe, añadir si existe
Files.writeString(archivo, "Contenido",
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
// Escritura atómica (todo o nada)
Files.writeString(archivo, "Contenido",
StandardOpenOption.CREATE,
StandardOpenOption.DSYNC);
Eliminación de archivos
La eliminación de archivos con NIO2 es directa:
// Eliminar un archivo (lanza excepción si no existe)
Files.delete(archivo);
// Eliminar si existe (no lanza excepción si no existe)
boolean eliminado = Files.deleteIfExists(archivo);
Al eliminar archivos, debemos tener en cuenta:
- No se pueden eliminar directorios que no estén vacíos con estos métodos.
- Si intentamos eliminar un archivo que está en uso, podemos recibir una excepción.
Manejo de excepciones
Las operaciones de archivo pueden fallar por diversas razones. Es importante manejar adecuadamente las excepciones:
Path archivo = Paths.get("datos/importante.txt");
try {
String contenido = Files.readString(archivo);
// Procesar contenido
} catch (NoSuchFileException e) {
System.err.println("El archivo no existe: " + e.getFile());
} catch (AccessDeniedException e) {
System.err.println("Acceso denegado al archivo: " + e.getFile());
} catch (IOException e) {
System.err.println("Error de E/S: " + e.getMessage());
}
Operaciones atómicas
NIO2 permite realizar algunas operaciones de manera atómica, lo que es útil en entornos concurrentes:
// Actualización atómica de un archivo
Path archivo = Paths.get("datos/contador.txt");
try {
// Leer valor actual
String valorActual = Files.exists(archivo) ?
Files.readString(archivo).trim() : "0";
int contador = Integer.parseInt(valorActual);
// Incrementar y escribir de vuelta
contador++;
Files.writeString(archivo, String.valueOf(contador),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.SYNC);
} catch (IOException | NumberFormatException e) {
System.err.println("Error al actualizar contador: " + e.getMessage());
}
Ejemplo práctico: Registro de eventos
Veamos un ejemplo práctico que combina varias operaciones:
public class RegistroEventos {
private static final Path ARCHIVO_LOG = Paths.get("datos/eventos.log");
public static void registrarEvento(String evento) throws IOException {
// Crear directorio si no existe
Files.createDirectories(ARCHIVO_LOG.getParent());
// Formatear evento con fecha y hora
LocalDateTime ahora = LocalDateTime.now();
String entradaLog = String.format("[%s] %s%n",
ahora.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
evento);
// Añadir al archivo de log
Files.writeString(ARCHIVO_LOG, entradaLog,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
}
public static List<String> obtenerUltimosEventos(int cantidad) throws IOException {
if (!Files.exists(ARCHIVO_LOG)) {
return Collections.emptyList();
}
try (Stream<String> lineas = Files.lines(ARCHIVO_LOG)) {
return lineas.skip(Math.max(0, Files.lines(ARCHIVO_LOG).count() - cantidad))
.collect(Collectors.toList());
}
}
}
Este ejemplo muestra cómo implementar un sistema simple de registro de eventos que:
- Crea automáticamente el directorio necesario
- Añade entradas de registro con marca de tiempo
- Proporciona una función para recuperar los eventos más recientes
La API NIO2 de Java proporciona herramientas potentes y flexibles para trabajar con archivos. Al dominar estas operaciones básicas de creación, lectura, escritura y eliminación, tendremos una base sólida para implementar cualquier funcionalidad relacionada con archivos en nuestras aplicaciones.
Listado y navegación de directorios
La capacidad de listar y navegar por directorios es esencial para muchas aplicaciones que necesitan trabajar con el sistema de archivos. La API NIO2 de Java proporciona varias formas de explorar la estructura de directorios, desde métodos simples para listar el contenido de un directorio hasta técnicas avanzadas para recorrer árboles de directorios completos.
Listado básico de directorios
El método más sencillo para listar el contenido de un directorio es utilizando DirectoryStream
, que proporciona una forma eficiente de iterar sobre las entradas de un directorio:
import java.nio.file.*;
import java.io.IOException;
public class ListadoDirectorio {
public static void main(String[] args) {
Path directorio = Paths.get("src");
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directorio)) {
for (Path entrada : stream) {
System.out.println(entrada.getFileName());
}
} catch (IOException e) {
System.err.println("Error al listar directorio: " + e.getMessage());
}
}
}
El uso de try-with-resources
garantiza que los recursos del sistema se liberan correctamente después de su uso, lo cual es una buena práctica al trabajar con recursos del sistema de archivos.
Filtrado de entradas de directorio
DirectoryStream
permite filtrar las entradas utilizando un patrón glob o un filtro personalizado:
// Listar solo archivos Java usando patrón glob
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directorio, "*.java")) {
for (Path archivo : stream) {
System.out.println("Archivo Java: " + archivo.getFileName());
}
}
// Usar un filtro personalizado para mostrar solo directorios
DirectoryStream.Filter<Path> soloDirectorios = path -> Files.isDirectory(path);
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directorio, soloDirectorios)) {
for (Path subdir : stream) {
System.out.println("Subdirectorio: " + subdir.getFileName());
}
}
Los patrones glob son una forma concisa de especificar patrones de nombres de archivo, similar a los comodines en la línea de comandos:
*
- coincide con cualquier número de caracteres?
- coincide con un solo carácter{}
- permite especificar alternativas separadas por comas
// Listar archivos con extensiones específicas
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directorio, "*.{java,class,txt}")) {
for (Path archivo : stream) {
System.out.println("Archivo encontrado: " + archivo.getFileName());
}
}
Recorrido de árboles de directorios
Para explorar directorios de forma recursiva, NIO2 ofrece dos enfoques principales:
1. Usando walkFileTree
El método walkFileTree
permite recorrer un árbol de directorios completo con un control detallado sobre el proceso:
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;
public class RecorridoArbol {
public static void main(String[] args) throws IOException {
Path inicio = Paths.get("proyecto");
Files.walkFileTree(inicio, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("Archivo: " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("Entrando al directorio: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
System.out.println("Saliendo del directorio: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Error al acceder a: " + file);
return FileVisitResult.CONTINUE;
}
});
}
}
La clase SimpleFileVisitor
implementa la interfaz FileVisitor
con métodos predeterminados que podemos sobrescribir según nuestras necesidades. Los métodos clave son:
preVisitDirectory
- llamado antes de visitar las entradas de un directoriopostVisitDirectory
- llamado después de visitar todas las entradas de un directoriovisitFile
- llamado para cada archivo regularvisitFileFailed
- llamado cuando falla el acceso a un archivo
Cada método devuelve un FileVisitResult
que determina cómo continuar el recorrido:
CONTINUE
- continúa normalmenteTERMINATE
- termina inmediatamente el recorridoSKIP_SUBTREE
- continúa sin visitar las entradas del directorio actualSKIP_SIBLINGS
- continúa sin visitar los hermanos restantes del archivo o directorio actual
2. Usando walk (Java 8+)
Para casos más simples, el método walk
proporciona una interfaz de Stream más concisa:
try (Stream<Path> paths = Files.walk(Paths.get("proyecto"))) {
paths.forEach(path -> System.out.println(path));
}
// Limitar la profundidad de búsqueda
try (Stream<Path> paths = Files.walk(Paths.get("proyecto"), 2)) {
paths.forEach(path -> System.out.println(path));
}
El método walk
es ideal cuando necesitamos aplicar operaciones de Stream a los resultados:
// Encontrar todos los archivos Java en el árbol de directorios
try (Stream<Path> paths = Files.walk(Paths.get("src"))) {
List<Path> archivosJava = paths
.filter(path -> path.toString().endsWith(".java"))
.collect(Collectors.toList());
System.out.println("Archivos Java encontrados: " + archivosJava.size());
archivosJava.forEach(System.out::println);
}
Búsqueda de archivos
NIO2 también proporciona métodos específicos para buscar archivos que cumplan ciertos criterios:
// Buscar archivos que coincidan con un patrón
try (Stream<Path> paths = Files.find(
Paths.get("src"),
Integer.MAX_VALUE,
(path, attrs) -> path.toString().endsWith(".java") && attrs.isRegularFile())) {
paths.forEach(System.out::println);
}
// Buscar archivos modificados en las últimas 24 horas
FileTime ayer = FileTime.from(Instant.now().minus(1, ChronoUnit.DAYS));
try (Stream<Path> paths = Files.find(
Paths.get("documentos"),
Integer.MAX_VALUE,
(path, attrs) -> attrs.isRegularFile() &&
attrs.lastModifiedTime().compareTo(ayer) > 0)) {
paths.forEach(path -> System.out.println("Archivo reciente: " + path));
}
Ejemplo práctico: Análisis de estructura de proyecto
Veamos un ejemplo más completo que analiza la estructura de un proyecto de software:
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;
import java.util.*;
public class AnalizadorProyecto {
private static Map<String, Integer> contadorExtensiones = new HashMap<>();
private static int totalArchivos = 0;
private static int totalDirectorios = 0;
private static long tamañoTotal = 0;
public static void main(String[] args) throws IOException {
Path raizProyecto = Paths.get("mi_proyecto");
if (!Files.exists(raizProyecto)) {
System.err.println("El directorio del proyecto no existe");
return;
}
System.out.println("Analizando proyecto en: " + raizProyecto.toAbsolutePath());
Files.walkFileTree(raizProyecto, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
totalArchivos++;
tamañoTotal += attrs.size();
String nombre = file.getFileName().toString();
int punto = nombre.lastIndexOf('.');
if (punto > 0) {
String extension = nombre.substring(punto).toLowerCase();
contadorExtensiones.put(extension,
contadorExtensiones.getOrDefault(extension, 0) + 1);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (!dir.equals(raizProyecto)) {
totalDirectorios++;
}
return FileVisitResult.CONTINUE;
}
});
// Mostrar resultados
System.out.println("\nResumen del proyecto:");
System.out.println("Total de archivos: " + totalArchivos);
System.out.println("Total de directorios: " + totalDirectorios);
System.out.println("Tamaño total: " + (tamañoTotal / 1024) + " KB");
System.out.println("\nDistribución por tipo de archivo:");
contadorExtensiones.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.forEach(e -> System.out.printf("%s: %d archivos%n", e.getKey(), e.getValue()));
}
}
Este ejemplo muestra cómo podemos utilizar walkFileTree
para recopilar estadísticas sobre un proyecto, como el número de archivos por tipo, el tamaño total y la estructura de directorios.
Navegación eficiente en directorios grandes
Cuando trabajamos con directorios que contienen muchos archivos, es importante considerar el rendimiento:
// Para directorios muy grandes, procesar en paralelo puede ser más eficiente
try (Stream<Path> paths = Files.walk(Paths.get("datos_masivos"))) {
paths.parallel()
.filter(Files::isRegularFile)
.forEach(path -> {
// Procesar cada archivo
});
}
Sin embargo, hay que tener cuidado con las operaciones paralelas en el sistema de archivos, ya que pueden causar contención de recursos si no se manejan adecuadamente.
Observación de cambios en directorios
Para aplicaciones que necesitan reaccionar a cambios en el sistema de archivos, NIO2 proporciona el servicio WatchService
:
Path directorio = Paths.get("directorio_vigilado");
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
directorio.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
System.out.println("Vigilando cambios en: " + directorio);
while (true) {
WatchKey key = watchService.take(); // Espera hasta que ocurra un evento
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) {
continue; // Eventos perdidos o desbordados
}
@SuppressWarnings("unchecked")
WatchEvent<Path> pathEvent = (WatchEvent<Path>) event;
Path nombreArchivo = pathEvent.context();
System.out.printf("Evento %s: %s%n", kind.name(), nombreArchivo);
}
boolean valid = key.reset();
if (!valid) {
break; // El directorio ya no es accesible
}
}
} catch (IOException | InterruptedException e) {
System.err.println("Error en la vigilancia: " + e.getMessage());
}
Este mecanismo es útil para implementar funcionalidades como recarga automática de configuración, sincronización de archivos o monitoreo de directorios.
La API NIO2 de Java proporciona herramientas potentes y flexibles para listar y navegar por directorios, desde operaciones simples hasta recorridos complejos de árboles de directorios. Estas capacidades son fundamentales para aplicaciones que necesitan interactuar con el sistema de archivos de manera eficiente y robusta.
Ejercicios de esta lección Clases de NIO2
Evalúa tus conocimientos de esta lección Clases de NIO2 con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Streams: match
Gestión de errores y excepciones
CRUD en Java de modelo Customer sobre un ArrayList
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
API java.nio 2
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
CRUD en Java de modelo Customer sobre un HashMap
Interfaces
Enumeraciones Enums
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
CRUD de productos en Java
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Herencia
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Mapas
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
Todas las lecciones de Java
Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De Java
Introducción Y Entorno
Configuración De Entorno Java
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Recursión
Sintaxis
Arrays Y Matrices
Sintaxis
Excepciones
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Sobrecarga De Métodos
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
La Clase Scanner
Programación Orientada A Objetos
Métodos De La Clase String
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Records
Programación Orientada A Objetos
Pattern Matching
Programación Orientada A Objetos
Inferencia De Tipos Con Var
Programación Orientada A Objetos
Enumeraciones Enums
Programación Orientada A Objetos
Generics
Programación Orientada A Objetos
Clases Sealed
Programación Orientada A Objetos
Listas
Framework Collections
Conjuntos
Framework Collections
Mapas
Framework Collections
Funciones Lambda
Programación Funcional
Interfaz Funcional Consumer
Programación Funcional
Interfaz Funcional Predicate
Programación Funcional
Interfaz Funcional Supplier
Programación Funcional
Interfaz Funcional Function
Programación Funcional
Métodos Referenciados
Programación Funcional
Creación De Streams
Programación Funcional
Operaciones Intermedias Con Streams: Map()
Programación Funcional
Operaciones Intermedias Con Streams: Filter()
Programación Funcional
Operaciones Intermedias Con Streams: Distinct()
Programación Funcional
Operaciones Finales Con Streams: Collect()
Programación Funcional
Operaciones Finales Con Streams: Min Max
Programación Funcional
Operaciones Intermedias Con Streams: Flatmap()
Programación Funcional
Operaciones Intermedias Con Streams: Sorted()
Programación Funcional
Operaciones Finales Con Streams: Reduce()
Programación Funcional
Operaciones Finales Con Streams: Foreach()
Programación Funcional
Operaciones Finales Con Streams: Count()
Programación Funcional
Operaciones Finales Con Streams: Match
Programación Funcional
Api Optional
Programación Funcional
Transformación
Programación Funcional
Reducción Y Acumulación
Programación Funcional
Mapeo
Programación Funcional
Streams Paralelos
Programación Funcional
Agrupación Y Partición
Programación Funcional
Filtrado Y Búsqueda
Programación Funcional
Api Java.nio 2
Entrada Y Salida Io
Fundamentos De Io
Entrada Y Salida Io
Leer Y Escribir Archivos
Entrada Y Salida Io
Httpclient Moderno
Entrada Y Salida Io
Clases De Nio2
Entrada Y Salida Io
Api Java.time
Api Java.time
Localtime
Api Java.time
Localdatetime
Api Java.time
Localdate
Api Java.time
Executorservice
Concurrencia
Virtual Threads (Project Loom)
Concurrencia
Future Y Completablefuture
Concurrencia
Spring Framework
Frameworks Para Java
Micronaut
Frameworks Para Java
Maven
Frameworks Para Java
Gradle
Frameworks Para Java
Lombok Para Java
Frameworks Para Java
Quarkus
Frameworks Para Java
Ecosistema Jakarta Ee De Java
Frameworks Para Java
Introducción A Junit 5
Testing
Certificados de superación de Java
Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender la interfaz Path y la clase Paths para representar y manipular rutas en el sistema de archivos.
- Utilizar la clase Files para realizar operaciones comunes sobre archivos y directorios, como creación, lectura, escritura y eliminación.
- Aprender a listar y navegar directorios, incluyendo recorridos recursivos y filtrado de contenido.
- Manejar atributos, permisos y enlaces simbólicos de archivos con la API NIO2.
- Implementar vigilancia de cambios en el sistema de archivos mediante WatchService.