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ícateConceptos 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:
- Cerrar los recursos en el orden inverso al que fueron declarados
- Suprimir excepciones secundarias que puedan ocurrir durante el cierre
- 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
conthrows 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
- Utiliza try-with-resources para garantizar el cierre de recursos
- Captura excepciones específicas antes que las generales
- Proporciona información útil en los mensajes de error
- Registra excepciones con un sistema de logging adecuado
- No ignores las excepciones sin una buena razón
- Considera la recuperación cuando sea posible
- Encapsula excepciones de bajo nivel en excepciones de dominio cuando sea apropiado
- Documenta las excepciones que pueden lanzar tus métodos
- Prueba los caminos de error en tu código, no solo el flujo normal
- 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.
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
Gestión de errores y excepciones
CRUD en Java de modelo Customer sobre un ArrayList
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
API java.nio 2
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
CRUD en Java de modelo Customer sobre un HashMap
Interfaces
Enumeraciones Enums
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
CRUD de productos en Java
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Herencia
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Mapas
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
Todas las lecciones de Java
Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De Java
Introducción Y Entorno
Configuración De Entorno Java
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Recursión
Sintaxis
Arrays Y Matrices
Sintaxis
Excepciones
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Sobrecarga De Métodos
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
La Clase Scanner
Programación Orientada A Objetos
Métodos De La Clase String
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Records
Programación Orientada A Objetos
Pattern Matching
Programación Orientada A Objetos
Inferencia De Tipos Con Var
Programación Orientada A Objetos
Enumeraciones Enums
Programación Orientada A Objetos
Generics
Programación Orientada A Objetos
Clases Sealed
Programación Orientada A Objetos
Listas
Framework Collections
Conjuntos
Framework Collections
Mapas
Framework Collections
Funciones Lambda
Programación Funcional
Interfaz Funcional Consumer
Programación Funcional
Interfaz Funcional Predicate
Programación Funcional
Interfaz Funcional Supplier
Programación Funcional
Interfaz Funcional Function
Programación Funcional
Métodos Referenciados
Programación Funcional
Creación De Streams
Programación Funcional
Operaciones Intermedias Con Streams: Map()
Programación Funcional
Operaciones Intermedias Con Streams: Filter()
Programación Funcional
Operaciones Intermedias Con Streams: Distinct()
Programación Funcional
Operaciones Finales Con Streams: Collect()
Programación Funcional
Operaciones Finales Con Streams: Min Max
Programación Funcional
Operaciones Intermedias Con Streams: Flatmap()
Programación Funcional
Operaciones Intermedias Con Streams: Sorted()
Programación Funcional
Operaciones Finales Con Streams: Reduce()
Programación Funcional
Operaciones Finales Con Streams: Foreach()
Programación Funcional
Operaciones Finales Con Streams: Count()
Programación Funcional
Operaciones Finales Con Streams: Match
Programación Funcional
Api Optional
Programación Funcional
Transformación
Programación Funcional
Reducción Y Acumulación
Programación Funcional
Mapeo
Programación Funcional
Streams Paralelos
Programación Funcional
Agrupación Y Partición
Programación Funcional
Filtrado Y Búsqueda
Programación Funcional
Api Java.nio 2
Entrada Y Salida Io
Fundamentos De Io
Entrada Y Salida Io
Leer Y Escribir Archivos
Entrada Y Salida Io
Httpclient Moderno
Entrada Y Salida Io
Clases De Nio2
Entrada Y Salida Io
Api Java.time
Api Java.time
Localtime
Api Java.time
Localdatetime
Api Java.time
Localdate
Api Java.time
Executorservice
Concurrencia
Virtual Threads (Project Loom)
Concurrencia
Future Y Completablefuture
Concurrencia
Spring Framework
Frameworks Para Java
Micronaut
Frameworks Para Java
Maven
Frameworks Para Java
Gradle
Frameworks Para Java
Lombok Para Java
Frameworks Para Java
Quarkus
Frameworks Para Java
Ecosistema Jakarta Ee De Java
Frameworks Para Java
Introducción A Junit 5
Testing
Certificados de superación de Java
Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender 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.