CodeGym /Cursos /JAVA 25 SELF /Operaciones masivas con archivos

Operaciones masivas con archivos

JAVA 25 SELF
Nivel 40 , Lección 2
Disponible

1. Recorrer archivos en un directorio

Obtener la lista de archivos: Files.list y Files.walk

En Java hay dos métodos muy útiles para trabajar con el contenido de una carpeta:

  • Files.list(Path) — devuelve un flujo (Stream<Path>) de todos los archivos y carpetas del nivel superior en el directorio indicado.
  • Files.walk(Path) — devuelve un flujo de todos los archivos y carpetas en el directorio y en todas sus subcarpetas (de forma recursiva).

Ejemplo: mostrar todos los archivos y carpetas en el directorio data

import java.nio.file.*;
import java.io.IOException;

public class ListFilesExample {
    public static void main(String[] args) throws IOException {
        Path dir = Paths.get("data");

        // Obtenemos un flujo de todos los archivos y carpetas del nivel superior
        try (var stream = Files.list(dir)) {
            stream.forEach(System.out::println);
        }
    }
}

Si quieres recorrer todas las carpetas anidadas (recursivamente), usa Files.walk:

try (var stream = Files.walk(dir)) {
    stream.forEach(System.out::println);
}

Atención:
Files.walk puede devolver muchísimos archivos si la estructura es grande — no lo ejecutes en la raíz del disco si no quieres ver «la lista del universo entero»!

Visualización: diferencia entre list y walk

Método Qué devuelve
Files.list
Solo el contenido de la carpeta actual
Files.walk
Todos los archivos y carpetas, incluidas las anidadas

2. Filtrado de archivos

Una de las principales ventajas de trabajar con streams (Stream<Path>) es la posibilidad de filtrar archivos por cualquier criterio: extensión, nombre, tamaño, fecha de creación, etc.

Filtrado por extensión

Supongamos que solo necesitamos archivos .txt. Podemos filtrarlos así:

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .forEach(System.out::println);
}

Filtrado por nombre

Queremos todos los archivos cuyo nombre empiece por "report":

stream
    .filter(path -> path.getFileName().toString().startsWith("report"))
    .forEach(System.out::println);

Filtrado por tamaño

Obtendremos solo los archivos «pesados» (más de 1 MB):

stream
    .filter(path -> {
        try {
            return Files.size(path) > 1_000_000;
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

Filtrado por fecha

Por ejemplo, archivos modificados en los últimos 7 días:

import java.time.*;
import java.nio.file.attribute.*;

stream
    .filter(path -> {
        try {
            FileTime lastModified = Files.getLastModifiedTime(path);
            return lastModified.toInstant().isAfter(Instant.now().minus(Duration.ofDays(7)));
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

Todo junto: filtrado combinado

Puedes combinar filtros «como en un restaurante»: por ejemplo, todos los archivos .log de hace más de un mes y de más de 10 KB:

stream
    .filter(path -> path.toString().endsWith(".log"))
    .filter(path -> {
        try {
            return Files.size(path) > 10_000;
        } catch (IOException e) {
            return false;
        }
    })
    .filter(path -> {
        try {
            FileTime lastModified = Files.getLastModifiedTime(path);
            return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(30)));
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

3. Copia y eliminación masivas de archivos

Las operaciones masivas son sencillas: obtienes un flujo de archivos y aplicas la función necesaria a cada elemento. Lo principal — no olvidar los posibles errores (por ejemplo, archivo en uso o ya existente).

Copiar todos los archivos de una carpeta a otra

import java.nio.file.StandardCopyOption;

Path sourceDir = Paths.get("data");
Path targetDir = Paths.get("backup");

try (var stream = Files.list(sourceDir)) {
    stream.forEach(path -> {
        Path targetPath = targetDir.resolve(path.getFileName());
        try {
            Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("Copiado: " + path.getFileName());
        } catch (IOException e) {
            System.out.println("Error al copiar " + path.getFileName() + ": " + e.getMessage());
        }
    });
}

Eliminar todos los archivos temporales (*.tmp)

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".tmp"))
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("Eliminado: " + path.getFileName());
            } catch (IOException e) {
                System.out.println("Error al eliminar " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

Uso de Stream API para procesar colecciones de rutas

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .sorted((p1, p2) -> {
            try {
                return Long.compare(Files.size(p2), Files.size(p1)); // por tamaño, descendente
            } catch (IOException e) {
                return 0;
            }
        })
        .limit(5) // solo los 5 más grandes
        .forEach(System.out::println);
}

4. Tareas prácticas

Buscar y eliminar archivos anteriores a una fecha determinada

Eliminemos todos los archivos que no se han modificado en más de un año:

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> {
            try {
                FileTime lastModified = Files.getLastModifiedTime(path);
                return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(365)));
            } catch (IOException e) {
                return false;
            }
        })
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("Eliminado: " + path.getFileName());
            } catch (IOException e) {
                System.out.println("Error al eliminar " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

Renombrado masivo de archivos según un patrón

Supongamos que hay que añadir el prefijo "old_" a cada archivo .txt:

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .forEach(path -> {
            Path newPath = path.resolveSibling("old_" + path.getFileName());
            try {
                Files.move(path, newPath);
                System.out.println("Renombrado: " + path.getFileName() + " -> " + newPath.getFileName());
            } catch (IOException e) {
                System.out.println("Error al renombrar " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

Copiar todos los archivos de todas las subcarpetas (recursivo)

Usamos Files.walk para recorrer todos los archivos en todas las subcarpetas:

try (var stream = Files.walk(sourceDir)) {
    stream
        .filter(Files::isRegularFile)
        .forEach(path -> {
            Path relative = sourceDir.relativize(path);
            Path targetPath = targetDir.resolve(relative);
            try {
                Files.createDirectories(targetPath.getParent());
                Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
                System.out.println("Copiado: " + path + " -> " + targetPath);
            } catch (IOException e) {
                System.out.println("Error al copiar " + path + ": " + e.getMessage());
            }
        });
}

5. Aspectos importantes y particularidades

Gestión de errores en operaciones masivas

En las operaciones masivas casi siempre hay archivos «problemáticos»: sin acceso, archivo en uso, ruta demasiado larga, archivo ya existente, etc. Si no gestionas las excepciones dentro del bucle, el programa puede fallar en el primer error. Por eso, usa siempre try-catch dentro de forEach para «tragar» el error de un archivo y poder procesar el resto.

Impacto de la recursión en el rendimiento y la pila de llamadas

Al usar Files.walk para directorios grandes, se puede obtener un recorrido muy largo si haces recursión manualmente. Por suerte, el propio método está implementado de forma eficiente y no provoca desbordamiento de pila. Pero si escribes tu propia función recursiva para eliminar/copiar directorios, sé prudente: para estructuras con enormes niveles de anidamiento la pila puede «reventar».

Trabajo con carpetas grandes

  • Usa Files.walk con precaución: si la carpeta contiene miles de archivos, puede llevar tiempo y consumir mucha memoria.
  • Si solo necesitas procesar el nivel superior, usa Files.list.
  • Para buscar archivos por máscara, utiliza el filtrado por nombre (endsWith, matches, etc.).

Ejemplo: «Limpiar una carpeta de archivos temporales»

Path tempDir = Paths.get("data/tmp");

try (var stream = Files.walk(tempDir)) {
    stream
        .filter(Files::isRegularFile)
        .filter(path -> path.toString().endsWith(".tmp"))
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("Eliminado: " + path);
            } catch (IOException e) {
                System.out.println("Error al eliminar " + path + ": " + e.getMessage());
            }
        });
}

Métodos principales para operaciones masivas

Operación Método/enfoque Particularidades
Obtener todos los archivos
Files.list, Files.walk
walk — recursivo, list — solo el nivel superior
Filtrado por extensión
filter(path -> ...)
Se puede combinar con otros filtros
Copiar un grupo de archivos
forEach + Files.copy
Comprueba la existencia de la carpeta de destino
Eliminar un grupo de archivos
forEach + Files.delete
Mejor usar try-catch para cada operación
Renombrado masivo
forEach + Files.move
Renombrar es un move con un nombre nuevo

6. Errores típicos en operaciones masivas

Error n.º 1: Excepciones no gestionadas dentro de forEach.
Un problema muy común: si no envuelves cada operación en try-catch, el programa fallará en el primer archivo problemático y los demás no se procesarán.

Error n.º 2: Intentar eliminar/copiar una carpeta como si fuera un archivo (o al revés).
Los métodos Files.delete y Files.copy tratan archivos y carpetas de forma distinta. ¡No los confundas! Por ejemplo, intentar eliminar una carpeta no vacía con el método estándar provocará un error.

Error n.º 3: Formación incorrecta de la ruta de destino al copiar.
Si creas la ruta de destino sin tener en cuenta la estructura (por ejemplo, sin usar relativize), puedes sobrescribir archivos o acabar con una estructura incorrecta en el destino.

Error n.º 4: Abrir demasiados archivos a la vez.
Si abres streams para lectura/escritura dentro del bucle, ¡no olvides cerrarlos! Es mejor usar try-with-resources.

Error n.º 5: Recorrido recursivo sin limitar la profundidad.
Al usar Files.walk en carpetas muy grandes y profundas, puedes tener problemas de rendimiento y memoria. Si necesitas limitar la profundidad, usa la sobrecarga con parámetro de profundidad: Files.walk(dir, depth).

Error n.º 6: Ignorar de forma implícita archivos ocultos/sistema.
Si tu aplicación trabaja con archivos de usuario, podrías tocar accidentalmente archivos ocultos o del sistema. Para filtrarlos utiliza Files.isHidden(path).

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION