Clases de NIO2

Avanzado
Java
Java
Actualizado: 08/05/2025

¡Desbloquea el curso completo!

IA
Ejercicios
Certificado
Entrar

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.

¿Te está gustando esta lección?

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

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.

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.

Aprendizajes 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.

Completa Java y certifícate

Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración