Java

Tutorial Java: Fundamentos de IO

Aprende los conceptos básicos de I/O en Java, manejo seguro de recursos con try-with-resources y estrategias avanzadas para excepciones.

Aprende Java y certifícate

Conceptos básicos: flujos, archivos, directorios

El sistema de entrada y salida (I/O) en Java permite a las aplicaciones interactuar con recursos externos como archivos, conexiones de red y dispositivos. Este sistema se basa en tres conceptos fundamentales: flujos para transferir datos, archivos como unidades de almacenamiento, y directorios como contenedores organizativos.

Flujos (Streams)

Los flujos son secuencias ordenadas de datos que viajan desde un origen hacia un destino. En Java, los flujos se dividen en dos categorías principales:

  • Flujos de bytes: Trabajan con datos binarios (secuencias de bytes)
  • Flujos de caracteres: Especializados en texto (secuencias de caracteres)

Flujos de bytes

Los flujos de bytes son ideales para manejar datos binarios como imágenes, archivos comprimidos o cualquier dato no textual. Las clases principales son:

  • InputStream: Clase abstracta base para leer bytes
  • OutputStream: Clase abstracta base para escribir bytes

Ejemplo básico de lectura de bytes desde un archivo:

try (FileInputStream fis = new FileInputStream("imagen.jpg")) {
    byte[] buffer = new byte[1024];
    int bytesRead;
    
    while ((bytesRead = fis.read(buffer)) != -1) {
        // Procesar los bytes leídos
        System.out.println("Leídos " + bytesRead + " bytes");
    }
} catch (IOException e) {
    e.printStackTrace();
}

Algunas implementaciones comunes de flujos de bytes incluyen:

  • FileInputStream/FileOutputStream: Para leer/escribir archivos
  • BufferedInputStream/BufferedOutputStream: Añaden buffer para mejorar rendimiento
  • DataInputStream/DataOutputStream: Para tipos de datos primitivos
  • ByteArrayInputStream/ByteArrayOutputStream: Para trabajar con arrays de bytes

Flujos de caracteres

Los flujos de caracteres están optimizados para texto y manejan automáticamente la codificación de caracteres. Las clases principales son:

  • Reader: Clase abstracta base para leer caracteres
  • Writer: Clase abstracta base para escribir caracteres

Ejemplo de escritura de texto en un archivo:

try (FileWriter writer = new FileWriter("datos.txt")) {
    writer.write("Esto es una línea de texto.\n");
    writer.write("Esta es otra línea de texto.");
} catch (IOException e) {
    e.printStackTrace();
}

Implementaciones comunes de flujos de caracteres:

  • FileReader/FileWriter: Para leer/escribir archivos de texto
  • BufferedReader/BufferedWriter: Añaden buffer y métodos convenientes como readLine()
  • InputStreamReader/OutputStreamWriter: Puentes entre flujos de bytes y caracteres
  • StringReader/StringWriter: Para trabajar con Strings como origen/destino

Archivos

En Java, la clase File ha sido tradicionalmente la forma de representar archivos y directorios en el sistema de archivos. Desde Java 7, la API NIO.2 introdujo la interfaz Path y la clase Files que ofrecen funcionalidades más avanzadas.

Clase File

La clase File representa una ruta a un archivo o directorio en el sistema de archivos:

// Crear una referencia a un archivo
File archivo = new File("documentos/informe.txt");

// Verificar si existe
boolean existe = archivo.exists();

// Obtener información
long tamaño = archivo.length();
boolean esArchivo = archivo.isFile();
boolean esDirectorio = archivo.isDirectory();
String nombreArchivo = archivo.getName();
String rutaAbsoluta = archivo.getAbsolutePath();

// Crear un nuevo archivo
try {
    boolean creado = archivo.createNewFile();
} catch (IOException e) {
    e.printStackTrace();
}

// Eliminar un archivo
boolean eliminado = archivo.delete();

Path y Files (NIO.2)

La API NIO.2 proporciona la interfaz Path y la clase utilitaria Files que ofrecen operaciones más potentes y flexibles:

// Crear una referencia a un archivo usando Path
Path ruta = Paths.get("documentos", "informe.txt");

// Operaciones básicas usando Files
try {
    // Verificar si existe
    boolean existe = Files.exists(ruta);
    
    // Leer todo el contenido de un archivo de texto
    List<String> lineas = Files.readAllLines(ruta, StandardCharsets.UTF_8);
    
    // Escribir contenido a un archivo
    List<String> contenido = Arrays.asList("Línea 1", "Línea 2", "Línea 3");
    Files.write(ruta, contenido, StandardCharsets.UTF_8);
    
    // Copiar un archivo
    Path destino = Paths.get("documentos/copia_informe.txt");
    Files.copy(ruta, destino, StandardCopyOption.REPLACE_EXISTING);
    
    // Mover/renombrar un archivo
    Path nuevaRuta = Paths.get("documentos/nuevo_nombre.txt");
    Files.move(ruta, nuevaRuta, StandardCopyOption.REPLACE_EXISTING);
    
    // Eliminar un archivo
    Files.delete(ruta);
    
} catch (IOException e) {
    e.printStackTrace();
}

Directorios

Los directorios son contenedores que organizan archivos y otros directorios en una estructura jerárquica. Java proporciona métodos para crear, listar y manipular directorios.

Trabajando con directorios usando File

// Crear una referencia a un directorio
File directorio = new File("documentos/proyectos");

// Crear un directorio
boolean creado = directorio.mkdir();

// Crear directorios incluyendo padres si no existen
boolean creadoConPadres = directorio.mkdirs();

// Listar contenido de un directorio
File[] contenido = directorio.listFiles();
if (contenido != null) {
    for (File item : contenido) {
        if (item.isFile()) {
            System.out.println("Archivo: " + item.getName());
        } else if (item.isDirectory()) {
            System.out.println("Directorio: " + item.getName());
        }
    }
}

// Filtrar archivos por extensión
File[] archivosTxt = directorio.listFiles((dir, name) -> name.endsWith(".txt"));

Trabajando con directorios usando NIO.2

La API NIO.2 ofrece capacidades más avanzadas para trabajar con directorios:

// Crear un directorio
Path directorio = Paths.get("documentos/proyectos");
try {
    Files.createDirectory(directorio);
    
    // Crear directorios incluyendo padres
    Files.createDirectories(directorio);
    
    // Listar contenido de un directorio
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(directorio)) {
        for (Path entrada : stream) {
            if (Files.isDirectory(entrada)) {
                System.out.println("Directorio: " + entrada.getFileName());
            } else {
                System.out.println("Archivo: " + entrada.getFileName());
            }
        }
    }
    
    // Listar con filtro (solo archivos .java)
    try (DirectoryStream<Path> stream = 
            Files.newDirectoryStream(directorio, "*.java")) {
        for (Path entrada : stream) {
            System.out.println("Archivo Java: " + entrada.getFileName());
        }
    }
    
    // Recorrer un árbol de directorios
    Files.walkFileTree(directorio, 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("Directorio: " + dir);
            return FileVisitResult.CONTINUE;
        }
    });
    
} catch (IOException e) {
    e.printStackTrace();
}

Propiedades de archivos y directorios

Tanto con la API tradicional como con NIO.2, podemos obtener y modificar atributos de archivos:

// Con File
File archivo = new File("documento.txt");
boolean puedeEscribir = archivo.canWrite();
boolean puedeEjecutar = archivo.canExecute();
long ultimaModificacion = archivo.lastModified();

// Cambiar permisos
archivo.setReadable(true);
archivo.setWritable(false);
archivo.setExecutable(true);

// Con NIO.2
Path ruta = Paths.get("documento.txt");
try {
    // Obtener atributos básicos
    BasicFileAttributes atributos = 
            Files.readAttributes(ruta, BasicFileAttributes.class);
    
    System.out.println("Tamaño: " + atributos.size());
    System.out.println("Creación: " + atributos.creationTime());
    System.out.println("Último acceso: " + atributos.lastAccessTime());
    System.out.println("Última modificación: " + atributos.lastModifiedTime());
    
    // Obtener permisos (POSIX - sistemas Unix/Linux)
    if (System.getProperty("os.name").toLowerCase().contains("linux") || 
        System.getProperty("os.name").toLowerCase().contains("mac")) {
        
        PosixFileAttributes posixAttr = 
                Files.readAttributes(ruta, PosixFileAttributes.class);
        System.out.println("Permisos: " + posixAttr.permissions());
        
        // Modificar permisos
        Set<PosixFilePermission> permisos = PosixFilePermissions.fromString("rw-r--r--");
        Files.setPosixFilePermissions(ruta, permisos);
    }
    
} catch (IOException e) {
    e.printStackTrace();
}

Los conceptos de flujos, archivos y directorios son la base para cualquier operación de entrada/salida en Java. Dominar estas herramientas te permitirá gestionar eficientemente el almacenamiento y transferencia de datos en tus aplicaciones.

Patrón try-with-resources para manejo seguro de recursos

El manejo adecuado de recursos como archivos, conexiones de red o bases de datos es crucial en el desarrollo de aplicaciones Java. Estos recursos utilizan memoria y conexiones del sistema que deben ser liberadas correctamente cuando ya no se necesitan, incluso si ocurren excepciones durante su uso.

Antes de Java 7, la forma tradicional de gestionar recursos implicaba bloques try-catch-finally anidados que resultaban en código complejo y propenso a errores. El patrón try-with-resources fue introducido para solucionar estos problemas.

Problema del enfoque tradicional

Veamos primero cómo se manejaban los recursos antes de Java 7:

FileInputStream fis = null;
BufferedInputStream bis = null;
try {
    fis = new FileInputStream("datos.txt");
    bis = new BufferedInputStream(fis);
    // Operaciones con los flujos
    int data;
    while ((data = bis.read()) != -1) {
        System.out.print((char) data);
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    // Cierre de recursos en orden inverso
    try {
        if (bis != null) bis.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    try {
        if (fis != null) fis.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Este enfoque presenta varios problemas:

  • Código verboso y difícil de mantener
  • Posibilidad de fugas de recursos si olvidamos cerrar alguno
  • Manejo complejo cuando hay múltiples recursos
  • Si ocurre una excepción en el bloque finally, puede ocultar la excepción original

Solución: try-with-resources

El patrón try-with-resources simplifica drásticamente el manejo de recursos mediante una sintaxis concisa que garantiza el cierre adecuado de los recursos:

try (FileInputStream fis = new FileInputStream("datos.txt");
     BufferedInputStream bis = new BufferedInputStream(fis)) {
    
    // Operaciones con los flujos
    int data;
    while ((data = bis.read()) != -1) {
        System.out.print((char) data);
    }
} catch (IOException e) {
    e.printStackTrace();
}
// Los recursos se cierran automáticamente al finalizar el bloque try

Requisitos para usar try-with-resources

Para que un recurso pueda utilizarse con este patrón, debe implementar la interfaz AutoCloseable o su subinterfaz Closeable:

public interface AutoCloseable {
    void close() throws Exception;
}

public interface Closeable extends AutoCloseable {
    void close() throws IOException;
}

La mayoría de las clases de E/S en Java ya implementan estas interfaces, incluyendo:

  • Flujos de entrada/salida (InputStream, OutputStream, Reader, Writer)
  • Conexiones JDBC (Connection, Statement, ResultSet)
  • Canales NIO (Channel)
  • Scanners y Formatters

Funcionamiento interno

El compilador de Java transforma el bloque try-with-resources en un bloque try-finally tradicional, pero con manejo especial para:

  1. Cerrar los recursos en el orden inverso al que fueron declarados
  2. Suprimir excepciones secundarias que puedan ocurrir durante el cierre
  3. Preservar la excepción original para facilitar la depuración

Ejemplos prácticos

Lectura de un archivo de texto

try (BufferedReader reader = new BufferedReader(new FileReader("config.txt"))) {
    String linea;
    while ((linea = reader.readLine()) != null) {
        System.out.println(linea);
    }
} catch (IOException e) {
    System.err.println("Error al leer el archivo: " + e.getMessage());
}

Escritura en un archivo

try (FileWriter writer = new FileWriter("salida.txt");
     BufferedWriter bufferedWriter = new BufferedWriter(writer)) {
    
    bufferedWriter.write("Primera línea de texto");
    bufferedWriter.newLine();
    bufferedWriter.write("Segunda línea de texto");
    
} catch (IOException e) {
    System.err.println("Error al escribir en el archivo: " + e.getMessage());
}

Copia de archivos con NIO.2

try (InputStream in = Files.newInputStream(Paths.get("origen.dat"));
     OutputStream out = Files.newOutputStream(Paths.get("destino.dat"))) {
    
    byte[] buffer = new byte[8192];
    int bytesRead;
    
    while ((bytesRead = in.read(buffer)) != -1) {
        out.write(buffer, 0, bytesRead);
    }
    
} catch (IOException e) {
    System.err.println("Error durante la copia: " + e.getMessage());
}

Conexión a base de datos

try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
     PreparedStatement stmt = conn.prepareStatement("SELECT * FROM usuarios WHERE id = ?")) {
    
    stmt.setInt(1, 123);
    
    try (ResultSet rs = stmt.executeQuery()) {
        while (rs.next()) {
            System.out.println("Usuario: " + rs.getString("nombre"));
        }
    }
    
} catch (SQLException e) {
    System.err.println("Error de base de datos: " + e.getMessage());
}

Manejo de excepciones suprimidas

Cuando ocurre una excepción en el bloque try y otra durante el cierre de recursos, Java preserva la excepción original y adjunta las demás como excepciones suprimidas:

try (MiRecursoProblematico recurso = new MiRecursoProblematico()) {
    throw new RuntimeException("Excepción en el bloque try");
} catch (Exception e) {
    System.err.println("Excepción principal: " + e.getMessage());
    
    // Acceder a las excepciones suprimidas
    Throwable[] suprimidas = e.getSuppressed();
    for (Throwable suprimida : suprimidas) {
        System.err.println("Excepción suprimida: " + suprimida.getMessage());
    }
}

Creación de recursos personalizados compatibles

Podemos crear nuestras propias clases compatibles con try-with-resources implementando AutoCloseable:

public class ConexionPersonalizada implements AutoCloseable {
    private final String nombre;
    
    public ConexionPersonalizada(String nombre) {
        this.nombre = nombre;
        System.out.println("Abriendo conexión: " + nombre);
    }
    
    public void enviarDatos(String datos) {
        System.out.println("Enviando: " + datos);
    }
    
    @Override
    public void close() throws Exception {
        System.out.println("Cerrando conexión: " + nombre);
        // Lógica de limpieza de recursos
    }
}

Uso de nuestro recurso personalizado:

try (ConexionPersonalizada conexion = new ConexionPersonalizada("Servidor-A")) {
    conexion.enviarDatos("Hola mundo");
    // La conexión se cerrará automáticamente al salir del bloque
}

Declaración de recursos fuera del bloque try

Desde Java 9, es posible utilizar variables finales o efectivamente finales declaradas fuera del bloque try:

// Java 9+
final BufferedReader reader1 = new BufferedReader(new FileReader("archivo1.txt"));
BufferedReader reader2 = new BufferedReader(new FileReader("archivo2.txt")); // Efectivamente final

try (reader1; reader2) {
    // Usar los recursos
    String linea1 = reader1.readLine();
    String linea2 = reader2.readLine();
    System.out.println(linea1 + " - " + linea2);
} catch (IOException e) {
    e.printStackTrace();
}

Mejores prácticas

  • Ordena los recursos de manera que los dependientes se declaren después de sus dependencias
  • Evita operaciones complejas dentro de la declaración de recursos
  • No realices operaciones en los recursos después del bloque try-with-resources
  • Implementa AutoCloseable en tus clases que gestionen recursos externos
  • Documenta el comportamiento del método close() en tus clases personalizadas
  • Captura excepciones específicas en lugar de excepciones genéricas

El patrón try-with-resources es una mejora significativa en la gestión de recursos en Java, que simplifica el código, reduce errores y mejora la robustez de las aplicaciones. Su uso es considerado una buena práctica en cualquier código que trabaje con recursos externos que deban ser liberados.

Manejo de excepciones de I/O básicas a alto nivel

El manejo efectivo de excepciones es una parte fundamental del desarrollo de aplicaciones Java robustas, especialmente cuando se trabaja con operaciones de entrada/salida (I/O). Las operaciones I/O son propensas a fallos debido a factores externos como permisos insuficientes, archivos inexistentes o problemas de hardware.

Java proporciona un sistema jerárquico de excepciones para operaciones I/O que permite manejar errores desde un nivel básico hasta estrategias más sofisticadas. Vamos a explorar este sistema y las mejores prácticas para implementarlo.

Jerarquía de excepciones I/O

La base de todas las excepciones de I/O en Java es la clase IOException, que extiende de Exception. Esta jerarquía incluye:

  • IOException: Excepción base para todos los errores de I/O
  • FileNotFoundException: El archivo especificado no existe
  • EOFException: Se alcanzó el final del archivo inesperadamente
  • MalformedURLException: URL con formato incorrecto
  • SocketException: Error en operaciones de socket
  • ConnectException: Error al conectar a un host remoto
  • UnknownHostException: No se puede resolver el nombre del host

Entender esta jerarquía es esencial para implementar estrategias de manejo de excepciones efectivas.

Manejo básico de excepciones I/O

El enfoque más simple para manejar excepciones I/O es capturarlas y proporcionar un mensaje de error:

public void leerArchivo(String ruta) {
    try {
        FileReader fileReader = new FileReader(ruta);
        BufferedReader bufferedReader = new BufferedReader(fileReader);
        
        String linea;
        while ((linea = bufferedReader.readLine()) != null) {
            System.out.println(linea);
        }
        
        bufferedReader.close();
    } catch (IOException e) {
        System.err.println("Error al leer el archivo: " + e.getMessage());
    }
}

Este enfoque tiene varias limitaciones:

  • No distingue entre diferentes tipos de errores
  • No garantiza el cierre de recursos
  • No permite recuperación específica según el tipo de error

Manejo intermedio: captura específica

Un enfoque más refinado es capturar excepciones específicas para proporcionar respuestas adaptadas a cada tipo de error:

public void leerArchivo(String ruta) {
    BufferedReader bufferedReader = null;
    try {
        FileReader fileReader = new FileReader(ruta);
        bufferedReader = new BufferedReader(fileReader);
        
        String linea;
        while ((linea = bufferedReader.readLine()) != null) {
            System.out.println(linea);
        }
    } catch (FileNotFoundException e) {
        System.err.println("El archivo no existe: " + ruta);
        System.err.println("Detalles: " + e.getMessage());
    } catch (IOException e) {
        System.err.println("Error de lectura en el archivo: " + ruta);
        System.err.println("Detalles: " + e.getMessage());
    } finally {
        if (bufferedReader != null) {
            try {
                bufferedReader.close();
            } catch (IOException e) {
                System.err.println("Error al cerrar el archivo: " + e.getMessage());
            }
        }
    }
}

Este enfoque mejora el manejo de errores al:

  • Distinguir entre archivo no encontrado y otros errores de I/O
  • Garantizar el cierre de recursos mediante el bloque finally
  • Proporcionar mensajes de error más específicos

Manejo avanzado: propagación controlada

En aplicaciones más complejas, a menudo es mejor propagar las excepciones a capas superiores donde se puede tomar una decisión más informada:

public List<String> leerLineasArchivo(String ruta) throws IOException {
    List<String> lineas = new ArrayList<>();
    
    try (BufferedReader reader = new BufferedReader(new FileReader(ruta))) {
        String linea;
        while ((linea = reader.readLine()) != null) {
            lineas.add(linea);
        }
        return lineas;
    }
    // El recurso se cierra automáticamente gracias a try-with-resources
}

Este método:

  • Declara que puede lanzar IOException con throws IOException
  • Utiliza try-with-resources para garantizar el cierre de recursos
  • Permite que el código que lo llama decida cómo manejar los errores

El código que llama a este método puede entonces implementar su propia estrategia:

public void procesarArchivo(String ruta) {
    try {
        List<String> lineas = leerLineasArchivo(ruta);
        for (String linea : lineas) {
            // Procesar cada línea
        }
    } catch (FileNotFoundException e) {
        // Crear el archivo si no existe
        crearArchivoVacio(ruta);
    } catch (IOException e) {
        // Registrar el error y notificar al usuario
        logger.error("Error al procesar el archivo: " + ruta, e);
        mostrarErrorUsuario("No se pudo procesar el archivo. Verifique los permisos.");
    }
}

Estrategias para excepciones específicas

FileNotFoundException

Esta excepción ocurre cuando intentamos abrir un archivo que no existe:

try {
    File archivo = new File("configuracion.properties");
    if (!archivo.exists()) {
        // Crear archivo con configuración predeterminada
        crearConfiguracionPredeterminada(archivo);
    }
    
    FileInputStream fis = new FileInputStream(archivo);
    // Continuar con la lectura
} catch (FileNotFoundException e) {
    // Este bloque se ejecutará solo si el archivo se eliminó
    // entre la verificación de exists() y la creación del FileInputStream
    System.err.println("El archivo fue eliminado inesperadamente");
}

EOFException

Se lanza cuando se alcanza el final de un archivo antes de lo esperado:

try (DataInputStream dis = new DataInputStream(
        new FileInputStream("datos.bin"))) {
    
    while (true) {
        // Leer hasta que se lance EOFException
        int valor = dis.readInt();
        procesarValor(valor);
    }
} catch (EOFException e) {
    // Fin normal del archivo
    System.out.println("Lectura completada");
} catch (IOException e) {
    // Otros errores de I/O
    System.err.println("Error durante la lectura: " + e.getMessage());
}

SocketException y excepciones de red

Para operaciones de red, es importante manejar diferentes tipos de errores:

try {
    URL url = new URL("https://api.ejemplo.com/datos");
    HttpURLConnection conexion = (HttpURLConnection) url.openConnection();
    conexion.setRequestMethod("GET");
    
    try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(conexion.getInputStream()))) {
        
        // Procesar respuesta
        String linea;
        while ((linea = reader.readLine()) != null) {
            System.out.println(linea);
        }
    }
} catch (MalformedURLException e) {
    System.err.println("URL inválida: " + e.getMessage());
} catch (UnknownHostException e) {
    System.err.println("No se puede conectar al servidor. Verifique su conexión a internet.");
} catch (ConnectException e) {
    System.err.println("Servidor no disponible. Intente más tarde.");
} catch (SocketTimeoutException e) {
    System.err.println("La conexión ha excedido el tiempo de espera.");
} catch (IOException e) {
    System.err.println("Error de I/O: " + e.getMessage());
}

Manejo de excepciones con NIO.2

La API NIO.2 introduce nuevas excepciones y patrones para el manejo de errores:

try {
    Path ruta = Paths.get("documentos", "informe.txt");
    List<String> lineas = Files.readAllLines(ruta, StandardCharsets.UTF_8);
    
    // Procesar líneas
    
} catch (NoSuchFileException e) {
    System.err.println("El archivo no existe: " + e.getFile());
    // Crear archivo vacío
    try {
        Files.createFile(e.getFile());
    } catch (IOException ex) {
        System.err.println("No se pudo crear el archivo: " + ex.getMessage());
    }
} catch (AccessDeniedException e) {
    System.err.println("Permiso denegado para acceder a: " + e.getFile());
    // Solicitar elevación de privilegios o cambiar ubicación
} catch (IOException e) {
    System.err.println("Error de I/O: " + e.getMessage());
}

NIO.2 proporciona excepciones más específicas como:

  • NoSuchFileException: El archivo no existe
  • DirectoryNotEmptyException: Intento de eliminar un directorio no vacío
  • AccessDeniedException: Permisos insuficientes
  • FileAlreadyExistsException: El archivo ya existe

Creación de excepciones personalizadas

Para aplicaciones complejas, es útil crear excepciones personalizadas que encapsulen errores específicos de I/O:

public class ConfiguracionException extends IOException {
    public ConfiguracionException(String mensaje) {
        super(mensaje);
    }
    
    public ConfiguracionException(String mensaje, Throwable causa) {
        super(mensaje, causa);
    }
}

// Uso
public void cargarConfiguracion(Path archivo) throws ConfiguracionException {
    try {
        Properties propiedades = new Properties();
        try (InputStream input = Files.newInputStream(archivo)) {
            propiedades.load(input);
        }
        
        // Validar configuración
        if (!propiedades.containsKey("db.url")) {
            throw new ConfiguracionException("Falta la propiedad obligatoria db.url");
        }
        
        // Aplicar configuración
        aplicarConfiguracion(propiedades);
        
    } catch (IOException e) {
        throw new ConfiguracionException("Error al cargar el archivo de configuración", e);
    }
}

Logging en lugar de impresión de errores

En aplicaciones de producción, es mejor utilizar un sistema de logging en lugar de imprimir mensajes de error:

import java.util.logging.Level;
import java.util.logging.Logger;

public class GestorArchivos {
    private static final Logger logger = Logger.getLogger(GestorArchivos.class.getName());
    
    public byte[] leerArchivoBinario(String ruta) {
        try {
            Path path = Paths.get(ruta);
            return Files.readAllBytes(path);
        } catch (NoSuchFileException e) {
            logger.log(Level.WARNING, "Archivo no encontrado: {0}", ruta);
            return new byte[0];
        } catch (IOException e) {
            logger.log(Level.SEVERE, "Error al leer archivo: " + ruta, e);
            throw new RuntimeException("No se pudo leer el archivo", e);
        }
    }
}

Estrategias de recuperación

Un manejo avanzado de excepciones incluye estrategias de recuperación:

public String leerArchivoConReintentos(String ruta, int maxIntentos) {
    int intentos = 0;
    long esperaBase = 1000; // milisegundos
    
    while (intentos < maxIntentos) {
        try {
            return new String(Files.readAllBytes(Paths.get(ruta)), StandardCharsets.UTF_8);
        } catch (IOException e) {
            intentos++;
            if (intentos >= maxIntentos) {
                logger.severe("Error persistente al leer " + ruta + " después de " + 
                             maxIntentos + " intentos: " + e.getMessage());
                throw new RuntimeException("No se pudo leer el archivo después de múltiples intentos", e);
            }
            
            // Espera exponencial entre reintentos
            long tiempoEspera = esperaBase * (long)Math.pow(2, intentos - 1);
            logger.warning("Error al leer " + ruta + ", reintentando en " + 
                          tiempoEspera + "ms. Intento " + intentos + "/" + maxIntentos);
            
            try {
                Thread.sleep(tiempoEspera);
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Operación interrumpida", ie);
            }
        }
    }
    
    // Este punto nunca debería alcanzarse debido al throw en el catch
    return null;
}

Mejores prácticas para el manejo de excepciones I/O

  1. Utiliza try-with-resources para garantizar el cierre de recursos
  2. Captura excepciones específicas antes que las generales
  3. Proporciona información útil en los mensajes de error
  4. Registra excepciones con un sistema de logging adecuado
  5. No ignores las excepciones sin una buena razón
  6. Considera la recuperación cuando sea posible
  7. Encapsula excepciones de bajo nivel en excepciones de dominio cuando sea apropiado
  8. Documenta las excepciones que pueden lanzar tus métodos
  9. Prueba los caminos de error en tu código, no solo el flujo normal
  10. Utiliza aserciones para validar precondiciones y postcondiciones
// Ejemplo que combina varias mejores prácticas
public List<Usuario> cargarUsuarios(Path archivo) throws DatosException {
    Objects.requireNonNull(archivo, "La ruta del archivo no puede ser null");
    
    List<Usuario> usuarios = new ArrayList<>();
    
    try (BufferedReader reader = Files.newBufferedReader(archivo, StandardCharsets.UTF_8)) {
        String linea;
        int numeroLinea = 0;
        
        while ((linea = reader.readLine()) != null) {
            numeroLinea++;
            
            // Omitir líneas vacías y comentarios
            if (linea.trim().isEmpty() || linea.startsWith("#")) {
                continue;
            }
            
            try {
                Usuario usuario = parsearUsuario(linea);
                usuarios.add(usuario);
            } catch (FormatoInvalidoException e) {
                logger.warning("Error en línea " + numeroLinea + ": " + e.getMessage());
                // Continuar con la siguiente línea
            }
        }
        
        return usuarios;
    } catch (NoSuchFileException e) {
        throw new DatosException("El archivo de usuarios no existe: " + archivo, e);
    } catch (IOException e) {
        throw new DatosException("Error al leer el archivo de usuarios", e);
    }
}

El manejo efectivo de excepciones I/O es un equilibrio entre robustez, claridad y capacidad de recuperación. Al implementar estas estrategias, tus aplicaciones podrán manejar graciosamente los inevitables errores que ocurren durante las operaciones de entrada y salida.

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 Fundamentos de IO

Evalúa tus conocimientos de esta lección Fundamentos de IO 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 los conceptos básicos de flujos de bytes y caracteres en Java.
  • Aprender a manipular archivos y directorios usando las APIs tradicionales y NIO.2.
  • Aplicar el patrón try-with-resources para un manejo seguro y eficiente de recursos.
  • Identificar y manejar adecuadamente las excepciones comunes en operaciones de I/O.
  • Implementar buenas prácticas y estrategias avanzadas para el manejo de errores en I/O.