Checked vs Unchecked exceptions strategy
Conceptos Fundamentales
Section titled “Conceptos Fundamentales”Checked Exceptions
Section titled “Checked Exceptions”- Deben ser declaradas o manejadas obligatoriamente
- DEBE declarar la excepción en el método
// Deben ser declaradas o manejadas obligatoriamentepublic class UserService {
// DEBE declarar la excepción en el método public User findUser(String id) throws UserNotFoundException, DataAccessException { if (id == null) { throw new UserNotFoundException("User ID cannot be null"); }
try { return userRepository.findById(id); } catch (SQLException e) { // Conversión de checked a checked throw new DataAccessException("Database error", e); } }}
// Excepciones checked personalizadaspublic class UserNotFoundException extends Exception { public UserNotFoundException(String message) { super(message); }}
public class DataAccessException extends Exception { public DataAccessException(String message, Throwable cause) { super(message, cause); }}
Unchecked Exceptions
Section titled “Unchecked Exceptions”- No requieren declaración obligatoria
- NO necesita declarar RuntimeException
// No requieren declaración obligatoriapublic class UserService {
// NO necesita declarar RuntimeException public User findUser(String id) { if (id == null) { throw new IllegalArgumentException("User ID cannot be null"); }
if (id.trim().isEmpty()) { throw new UserNotFoundException("User not found: " + id); }
return userRepository.findById(id); }}
// Excepciones unchecked personalizadaspublic class UserNotFoundException extends RuntimeException { public UserNotFoundException(String message) { super(message); }}
public class InvalidUserDataException extends RuntimeException { public InvalidUserDataException(String message) { super(message); }}
Comparación Detallada
Section titled “Comparación Detallada”Aspecto | Checked Exceptions | Unchecked Exceptions |
---|---|---|
Herencia | Extienden Exception | Extienden RuntimeException |
Compilación | Error si no se manejan | Compila sin manejo |
Declaración | Obligatoria en método | Opcional |
Manejo | try/catch obligatorio | try/catch opcional |
Performance | Overhead mínimo | Overhead mínimo |
API Design | Hace explícito el contrato | API más limpia |
Estrategias de Implementación
Section titled “Estrategias de Implementación”Estrategia 1: Solo Checked Exceptions
Section titled “Estrategia 1: Solo Checked Exceptions”// Enfoque tradicional - Todo checkedpublic class OrderService {
public Order createOrder(OrderRequest request) throws ValidationException, PaymentException, InventoryException {
validateOrder(request); // throws ValidationException
try { processPayment(request.getPayment()); // throws PaymentException reserveInventory(request.getItems()); // throws InventoryException
return saveOrder(request); } catch (PaymentException | InventoryException e) { // Manejo específico y re-throw logError("Order creation failed", e); throw e; } }
private void validateOrder(OrderRequest request) throws ValidationException { if (request == null) { throw new ValidationException("Order request cannot be null"); } if (request.getItems().isEmpty()) { throw new ValidationException("Order must contain items"); } }}
// Uso - Manejo obligatoriopublic class OrderController {
public ResponseEntity<Order> createOrder(OrderRequest request) { try { Order order = orderService.createOrder(request); return ResponseEntity.ok(order); } catch (ValidationException e) { return ResponseEntity.badRequest().body(null); } catch (PaymentException e) { return ResponseEntity.status(402).body(null); } catch (InventoryException e) { return ResponseEntity.status(409).body(null); } }}
Estrategia 2: Solo Unchecked Exceptions
Section titled “Estrategia 2: Solo Unchecked Exceptions”// Enfoque moderno - Todo uncheckedpublic class OrderService {
// API limpia sin throws public Order createOrder(OrderRequest request) { validateOrder(request);
try { processPayment(request.getPayment()); reserveInventory(request.getItems());
return saveOrder(request); } catch (PaymentException | InventoryException e) { logError("Order creation failed", e); throw e; // Re-throw unchecked } }
private void validateOrder(OrderRequest request) { if (request == null) { throw new ValidationException("Order request cannot be null"); } if (request.getItems().isEmpty()) { throw new ValidationException("Order must contain items"); } }}
// Excepciones uncheckedpublic class ValidationException extends RuntimeException { public ValidationException(String message) { super(message); }}
public class PaymentException extends RuntimeException { public PaymentException(String message, Throwable cause) { super(message, cause); }}
// Uso - Manejo opcional pero recomendadopublic class OrderController {
public ResponseEntity<Order> createOrder(OrderRequest request) { try { Order order = orderService.createOrder(request); return ResponseEntity.ok(order); } catch (ValidationException e) { return ResponseEntity.badRequest().body(null); } catch (PaymentException e) { return ResponseEntity.status(402).body(null); } catch (InventoryException e) { return ResponseEntity.status(409).body(null); } // Manejo opcional - podría omitirse }}
Estrategia 3: Híbrida (Recomendada)
Section titled “Estrategia 3: Híbrida (Recomendada)”- Checked
- Errores recuperables/esperados
- Errores de formato esperado
- Errores de negocio esperados
- Unchecked
- Error de programación
// Combina ambos enfoques según el contextopublic class FileProcessorService {
// Checked para errores recuperables/esperados public ProcessResult processFile(String filePath) throws FileNotFoundException, InvalidFormatException {
validateInput(filePath); // unchecked - error de programación
try { File file = loadFile(filePath); // checked - error esperado return parseFile(file); // checked - error de formato esperado } catch (IOException e) { throw new FileNotFoundException("File not accessible: " + filePath, e); } }
// Unchecked para errores de programación private void validateInput(String filePath) { if (filePath == null || filePath.trim().isEmpty()) { throw new IllegalArgumentException("File path cannot be null or empty"); } }
// Checked para errores de negocio esperados private ProcessResult parseFile(File file) throws InvalidFormatException { try { return parser.parse(file); } catch (ParseException e) { throw new InvalidFormatException("Invalid file format", e); } }}
// Jerarquía de excepciones híbridapublic abstract class BusinessException extends Exception { protected BusinessException(String message) { super(message); } protected BusinessException(String message, Throwable cause) { super(message, cause); }}
public abstract class SystemException extends RuntimeException { protected SystemException(String message) { super(message); } protected SystemException(String message, Throwable cause) { super(message, cause); }}
// Excepciones específicaspublic class InvalidFormatException extends BusinessException { public InvalidFormatException(String message, Throwable cause) { super(message, cause); }}
public class ConfigurationException extends SystemException { public ConfigurationException(String message) { super(message); }}
Criterios de Decisión
Section titled “Criterios de Decisión”Usar Checked Exceptions cuando
Section titled “Usar Checked Exceptions cuando”- Errores recuperables por el caller
- Parte fundamental del contrato de la API
- Errores de recursos externos
// 1. Errores recuperables por el callerpublic class EmailService { public void sendEmail(Email email) throws EmailDeliveryException { // El caller puede reintentar o usar otro proveedor }}
// 2. Parte fundamental del contrato de la APIpublic class PaymentGateway { public PaymentResult processPayment(Payment payment) throws InsufficientFundsException, CardExpiredException { // Errores esperados que el caller debe manejar }}
// 3. Errores de recursos externospublic class DatabaseService { public List<User> getUsers() throws SQLException { // Error de conectividad - caller puede decidir qué hacer }}
Usar Unchecked Exceptions cuando
Section titled “Usar Unchecked Exceptions cuando”- Errores de programación
- Errores que raramente se pueden recuperar
- Errores del sistema
// 1. Errores de programaciónpublic class Calculator { public double divide(double a, double b) { if (b == 0) { throw new IllegalArgumentException("Division by zero"); } return a / b; }}
// 2. Errores que raramente se pueden recuperarpublic class ConfigurationLoader { public Configuration load() { try { return loadFromFile(); } catch (IOException e) { throw new ConfigurationException("Failed to load config", e); } }}
// 3. Errores del sistemapublic class CacheService { public void put(String key, Object value) { if (isMemoryFull()) { throw new OutOfMemoryError("Cache memory exhausted"); } }}
Patrones de Conversión
Section titled “Patrones de Conversión”Wrapping de Checked a Unchecked
Section titled “Wrapping de Checked a Unchecked”public class RepositoryImpl {
// Convierte checked SQLException a unchecked public User findById(String id) { try { return jdbcTemplate.queryForObject(sql, mapper, id); } catch (SQLException e) { throw new DataAccessException("Database error", e); } }}
// Excepción wrapper uncheckedpublic class DataAccessException extends RuntimeException { public DataAccessException(String message, Throwable cause) { super(message, cause); }
// Método helper para obtener la causa original public SQLException getSQLException() { return (SQLException) getCause(); }}
Wrapping de Unchecked a Checked
Section titled “Wrapping de Unchecked a Checked”public class ValidationService {
// Convierte unchecked a checked para hacer explícito el contrato public void validateUser(User user) throws ValidationException { try { validator.validate(user); // Puede lanzar IllegalArgumentException } catch (IllegalArgumentException e) { throw new ValidationException("User validation failed", e); } }}
Testing de Estrategias
Section titled “Testing de Estrategias”Testing Checked Exceptions
Section titled “Testing Checked Exceptions”@Testvoid processFile_WhenFileNotFound_ShouldThrowFileNotFoundException() { String invalidPath = "nonexistent.txt";
assertThatThrownBy(() -> fileProcessor.processFile(invalidPath)) .isInstanceOf(FileNotFoundException.class) .hasMessage("File not accessible: nonexistent.txt") .hasCauseInstanceOf(IOException.class);}
@Testvoid processFile_WithValidFile_ShouldReturnResult() throws Exception { // Test del happy path - manejo obligatorio de excepciones ProcessResult result = fileProcessor.processFile("valid.txt"); assertThat(result).isNotNull();}
Testing Unchecked Exceptions
Section titled “Testing Unchecked Exceptions”@Testvoid divide_ByZero_ShouldThrowIllegalArgumentException() { assertThatThrownBy(() -> calculator.divide(10, 0)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Division by zero");}
@Testvoid divide_WithValidNumbers_ShouldReturnResult() { // No manejo obligatorio de excepciones double result = calculator.divide(10, 2); assertThat(result).isEqualTo(5.0);}
Mejores Prácticas
Section titled “Mejores Prácticas”DO - Buenas Prácticas
Section titled “DO - Buenas Prácticas”// 1. Usar checked para errores recuperablespublic class OrderService { public Order createOrder(OrderData data) throws OrderValidationException { // Caller puede mostrar errores al usuario y pedir corrección }}
// 2. Usar unchecked para errores de programaciónpublic class StringUtils { public static String reverse(String input) { if (input == null) { throw new IllegalArgumentException("Input cannot be null"); } return new StringBuilder(input).reverse().toString(); }}
// 3. Documentar excepciones unchecked importantes/** * Processes user data * @param userData the user data to process * @return processed user * @throws IllegalArgumentException if userData is null or invalid * @throws ConfigurationException if system is not properly configured */public User processUser(UserData userData) { // Implementation}
// 4. Crear jerarquías de excepciones coherentespublic abstract class OrderException extends Exception { protected OrderException(String message) { super(message); }}
public class OrderValidationException extends OrderException { public OrderValidationException(String message) { super(message); }}
public class OrderProcessingException extends OrderException { public OrderProcessingException(String message, Throwable cause) { super(message, cause); }}
DON’T - Malas Prácticas
Section titled “DON’T - Malas Prácticas”// 1. NO usar checked exceptions para errores de programaciónpublic class BadValidator { public void validate(String input) throws ValidationException { if (input == null) { // Error de programación throw new ValidationException("Input is null"); // MAL } }}
// 2. NO ignorar excepciones silenciosamentepublic class BadFileReader { public String readFile(String path) { try { return Files.readString(Paths.get(path)); } catch (IOException e) { return ""; // MAL - pérdida de información } }}
// 3. NO crear demasiadas checked exceptionspublic class OverComplicatedService { public void process() throws ExceptionA, ExceptionB, ExceptionC, ExceptionD, ExceptionE { // MAL - demasiadas }}
// 4. NO mezclar niveles de abstracciónpublic class BadController { public ResponseEntity<String> getData() { try { return ResponseEntity.ok(service.getData()); } catch (SQLException e) { // MAL - detalles de implementación filtrados return ResponseEntity.status(500).build(); } }}
Recomendaciones por Contexto
Section titled “Recomendaciones por Contexto”APIs Públicas
Section titled “APIs Públicas”- Usar checked exceptions para errores que el cliente debe manejar
- Documentar claramente todas las excepciones
- Mantener estabilidad en el contrato de excepciones
Código Interno
Section titled “Código Interno”- Preferir unchecked exceptions para mayor flexibilidad
- Usar checked solo cuando el manejo sea realmente necesario
- Crear jerarquías simples y coherentes
Frameworks/Librerías
Section titled “Frameworks/Librerías”- Seguir las convenciones del ecosistema
- Spring: principalmente unchecked
- Jakarta EE: mix de checked/unchecked
- Crear wrappers apropiados para APIs externas