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ícate

Path 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 directorio
  • postVisitDirectory - llamado después de visitar todas las entradas de un directorio
  • visitFile - llamado para cada archivo regular
  • visitFileFailed - llamado cuando falla el acceso a un archivo

Cada método devuelve un FileVisitResult que determina cómo continuar el recorrido:

  • CONTINUE - continúa normalmente
  • TERMINATE - termina inmediatamente el recorrido
  • SKIP_SUBTREE - continúa sin visitar las entradas del directorio actual
  • SKIP_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.

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.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende Java online

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

Test

Gestión de errores y excepciones

Código

CRUD en Java de modelo Customer sobre un ArrayList

Proyecto

Clases abstractas

Test

Listas

Código

Métodos de la clase String

Código

Streams: reduce()

Test

API java.nio 2

Puzzle

Polimorfismo

Código

Pattern Matching

Código

Streams: flatMap()

Test

Llamada y sobrecarga de funciones

Puzzle

Métodos referenciados

Test

Métodos de la clase String

Código

Representación de Fecha

Puzzle

Operadores lógicos

Test

Inferencia de tipos con var

Código

Tipos de datos

Código

Estructuras de iteración

Puzzle

Streams: forEach()

Test

Objetos

Puzzle

Funciones lambda

Test

Uso de Scanner

Puzzle

Tipos de variables

Puzzle

Streams: collect()

Puzzle

Operadores aritméticos

Puzzle

Arrays y matrices

Código

Clases y objetos

Código

Interfaz funcional Consumer

Test

CRUD en Java de modelo Customer sobre un HashMap

Proyecto

Interfaces

Código

Enumeraciones Enums

Código

API Optional

Test

Interfaz funcional Function

Test

Encapsulación

Test

Interfaces

Código

Uso de API Optional

Puzzle

Representación de Hora

Test

Herencia básica

Test

Clases y objetos

Código

Interfaz funcional Supplier

Puzzle

HashMap

Puzzle

Sobrecarga de métodos

Test

Polimorfismo de tiempo de ejecución

Puzzle

OOP en Java

Proyecto

Sobrecarga de métodos

Código

CRUD de productos en Java

Proyecto

Clases sealed

Código

Creación de Streams

Test

Records

Código

Encapsulación

Código

Streams: min max

Puzzle

Herencia

Código

Métodos avanzados de la clase String

Puzzle

Funciones

Código

Polimorfismo de tiempo de compilación

Test

Reto sintaxis Java

Proyecto

Conjuntos

Código

Estructuras de control

Código

Recursión

Código

Excepciones

Puzzle

Herencia avanzada

Puzzle

Estructuras de selección

Test

Uso de interfaces

Test

Operadores

Código

Variables

Código

HashSet

Test

Objeto Scanner

Test

Streams: filter()

Puzzle

Operaciones de Streams

Puzzle

Interfaz funcional Predicate

Puzzle

Streams: sorted()

Test

Configuración de entorno

Test

Uso de variables

Test

Clases

Test

Streams: distinct()

Puzzle

Streams: count()

Test

ArrayList

Test

Mapas

Código

Datos de referencia

Test

Interfaces funcionales

Puzzle

Métodos básicos de la clase String

Test

Tipos de datos

Código

Clases abstractas

Código

Instalación

Test

Funciones

Código

Excepciones

Código

Estructuras de control

Código

Herencia de clases

Código

La clase Scanner

Código

Generics

Código

Streams: map()

Puzzle

Funciones y encapsulamiento

Test

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

Accede GRATIS a Java y certifícate

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.