Incluir el contexto de la ejecución:
Los beneficios de las excepciones checked en Java son mínimos:
(Hay quien recomienda usar solamente excepciones unchecked)
Ejemplo: se necesita procesar un archivo CSV con datos de empleados. El código está estructurado en capas:
EmployeeCSVProcessor (mi aplicación) — Necesita lanzar IOException si hay erroresCSVReader (una librería de terceros) — Itera sobre las líneas del archivoEmployeeRowHandler (mi implementación de callback) — Procesa cada fila// Librería de terceros - NO sabe ni debe saber sobre IOException
public class CSVReader {
public void processRows(RowHandler handler) {
List<String> lines = readFile(filePath);
for (String line : lines) {
handler.handle(line); // Llama al handler
}
}
}
// Contrato que proporciona CSVReader
public interface RowHandler {
void handle(String row); // NO puede lanzar excepciones checked
}
// Mi implementación - ¡PROBLEMA!
public class EmployeeRowHandler implements RowHandler {
@Override
public void handle(String row) throws IOException { //
INCOMPATIBLE
if (!isValid(row)) {
throw new IOException("Invalid employee data");
}
}
}
El dilema:
EmployeeCSVProcessor (quiere IOException) CSVReader (no declara IOException) EmployeeRowHandler (necesita lanzarla)
Contrato violado: ¡no compila!
Cómo afectan al diseño las excepciones checked
catch está tres niveles por encima, hay que declarar la excepción en la signatura de todos los métodos que van entre medias.Muchas APIs de Java lanzan excepciones checked cuando deberían ser unchecked
Ejemplo: Al ejecutar una consulta mediante executeQuery en el API de JDBC se lanza una excepción java.sql.SQLException (de tipo checked) si la SQL es errónea.
Solución: ¿transformación en unchecked?
Transformar las excepciones checked en unchecked:
try {
// Codigo que genera la excepcion checked
} catch (Exception ex) {
throw new RuntimeException("Unchecked exception", ex)
}
La solución es un code smell:
public class EmployeeRowHandler implements RowHandler {
@Override
public void handle(String row) {
if (!isValid(row)) {
throw new RuntimeException("Invalid employee data: " + row);
}
}
}
public class EmployeeCSVProcessor {
public void process(String filePath) throws IOException {
try {
reader.processRows(new EmployeeRowHandler());
} catch (RuntimeException e) {
// ¿Era una IOException o un error real?
if (e.getMessage().contains("Invalid")) {
throw new IOException(e); // Reconvertir
}
throw e; // Relanzar si era otra cosa
}
}
}
Precio a pagar:
RuntimeException es realmente la excepción esperadaTransformación a UncheckedIOException
Java 8 introdujo UncheckedIOException, para transformar una IOException en unchecked sin perder la información de la causa original
import java.io.UncheckedIOException;
public class EmployeeRowHandler implements RowHandler {
@Override
public void handle(String row) {
try {
if (!isValid(row)) {
throw new IOException("Invalid employee data");
}
// Lógica principal
} catch (IOException e) {
// "Envolvemos" el error checked en un wrapper unchecked oficial de Java
throw new UncheckedIOException(e);
}
}
}
Criticar la siguiente implementación:
ACMEPort port = new ACMEPort(12);
try {
port.open();
} catch (DeviceResponseException e) {
reportPortError(e);
logger.log("Device response exception", e);
} catch (ATM1212UnlockedException e) {
reportPortError(e);
logger.log("Unlock exception", e);
} catch (GMXError e) {
reportPortError(e);
logger.log("Device response exception");
} finally {
...
}
Código duplicado: llamada a reportPortError() se repite mucho. ¿Cómo evitarlo?
Solución: Excepción encapsulada
public class LocalPort {
private ACMEPort innerPort;
public LocalPort(int portNumber) {
innerPort = new ACMEPort(portNumber);
}
public void open() throws PortDeviceFailure {
try {
innerPort.open();
} catch (DeviceResponseException e) {
throw new PortDeviceFailure(e);
} catch (ATM1212UnlockedException e) {
throw new PortDeviceFailure(e);
} catch (GMXError e) {
throw new PortDeviceFailure(e);
}
}
...
}
Sustituir ahora por...
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportPortError(e);
logger.log(e.getMessage(), e);
} finally {
...
}
Ejemplo: excepciones en el tratamiento de ficheros: ¿Usar excepciones cuando se intenta abrir un fichero para leer y el fichero no existe? Depende de si el fichero debe estar ahí
public void open_passwd() throws FileNotFoundException {
// This may throw FileNotFoundException...
ipstream = new FileInputStream("/etc/passwd");
// ...
}
public boolean open_user_file(String name)
throws FileNotFoundException {
File f = new File(name);
if (!f.exists())
return false;
ipstream = new FileInputStream(f);
return true;
}
Obtener un null cuando no se espera puede ser un quebradero de cabeza para el tratamiento de errores.
Principio general: no devolver null
Este código puede parecer inofensivo, pero es maligno: ¿Qué pasa si persistentStore es null?
public void registerItem(Item item) {
if (item != null) {
ItemRegistry registry = persistentStore.getItemRegistry();
if (registry != null) {
Item existing = registry.getItem(item.getID());
if (existing.getBillingPeriod().hasRetailOwner()) {
existing.register(item);
}
}
}
}
NullPointerExceptionif null?Evitar esto:
List<Employee> employees =
getEmployees();
if (employees != null) {
for(Employee e : employees) {
totalPay += e.getPay();
}
}
Mejor así:
List<Employee> employees =
getEmployees();
for (Employee e: employees) {
totalPay += e.getPay();
}
public List<Employee> getEmployees() {
if( /* there are no employees */ )
return Collections.emptyList();
}
public class MetricsCalculator
{
public double xProjection(Point p1, Point p2) {
return (p2.x - p1.x) * 1.5;
}
}
¿Qué sucede si llamamos a xProjection() así...?
calculator.xProjection(null, new Point(12, 13))
Devolver null es malo, pero ¡pasar un valor null es peor!
¿Es mejor así...?
public class MetricsCalculator
{
public double xProjection(Point p1, Point p2) {
if (p1 == null || p2 == null)
throw InvalidArgumentException(
"Invalid argument for MetricsCalculator.xProjection");
return (p2.x - p1.x) * 1.5;
}
}
¿Qué acción realizar ante un InvalidArgumentException? ¿Hay alguna buena?
public class MetricsCalculator
{
public double xProjection(Point p1, Point p2) {
assert p1 != null : "p1 should not be null";
assert p2 != null : "p2 should not be null";
return (p2.x - p1.x) * 1.5;
}
}
El uso de assert es una buena forma de
Pueden usarse aserciones o contratos para resolver esto.
Optionjava.util.Optionalstd::optionalundefined (algo que no se ha inicializado) en lugar de null (algo que no está disponible)OptionEn Scala, Option[T] es un contenedor de un valor opcional de tipo T.
Option[T] es una intancia de Some[T] que contiene el valor presente de tipo T.Option[T] es el objeto None.object Demo {
def main(args: Array[String]) {
val a: Option[Int] = Some(5)
val b: Option[Int] = None
println("a.isEmpty: " + a.isEmpty ) //false
println("b.isEmpty: " + b.isEmpty ) //true
}
}
Diferencias entre Null, null, Nil, Nothing, None y Unit en Scala:
null es como el de JavaNull es un trait, subconjunto de todos los tipos-referencia, cuya única instancia es nullNothing es un trait sin instancias; sirve para especificar el tipo de retorno en métodos que siempre elevan una excepciónUnit es análogo a void en JavaNil es una lista con cero elementos (su tipo es List[Nothing])None es uno de los hijos de Optionobject Demo {
def main(args: Array[String]) {
val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo")
println("show(capitals.get( \"Japan\")) : " +
show(capitals.get( "Japan")) )
println("show(capitals.get( \"India\")) : " +
show(capitals.get( "India")) )
}
def show(x: Option[String]) = x match {
case Some(s) => s
case None => "?"
}
}
Optionalprivate static Optional<Double> getDurationOfAlbumWithName(String name) {
Album album;
Optional<Album> albumOptional = getAlbum(name);
if (albumOptional.isPresent()) { // albumOptional == null
album = albumOptional.get();
Optional<List<Track>> tracksOptional = getAlbumTracks(album.getName());
double duration = 0;
if (tracksOptional.isPresent()) { // tracksOptional == null
List<Track> tracks = tracksOptional.get();
for (Track track : tracks) {
duration += track.getDuration();
}
return Optional.of(duration);
} else {
return Optional.empty();
}
} else {
return Optional.empty();
}
}
Al ejecutar varias operaciones seguidas que pueden devolver null, el nivel de anidamiento del código aumenta y queda menos claro (se mezcla código funcional con código de gestión de errores). Solución...
Optional<Double> getDurationOfAlbumWithName(String name) {
Optional<Double> duration = getAlbum(name)
.flatMap((album) -> getAlbumTracks(album.getName()))
.map((tracks) -> getTracksDuration(tracks));
return duration;
}
La función map comprueba si el Optional que recibe está vacío. Si lo está devuelve un Optional vacío y, si no, aplica la función anónima que le hemos pasado por parámetro, pasándole el valor del Optional (es decir, si el Optional está vacío, el método map no hace nada).
Esto sirve para concatenar operaciones sin necesidad de comprobar en cada momento si el Optional está vacío.
Cuando queremos encadenar distintas operaciones que devuelvan Optional, es necesario usar flatMap, ya que si no acabaríamos teniendo un Optional<Optional<Double>>.
Pero... getDurationOfAlbumWithName() devuelve un Optional<Double>. ¿No debería mejor devolver un double?
private static double getDurationOfAlbumWithName(String name) {
return getAlbum(name)
.flatMap((album) -> getAlbumTracks(album.getName()))
.map((tracks) -> getTracksDuration(tracks))
.orElse(0.0);
}
Podríamos seguir devolviendo Optional por toda la aplicación, pero en algún momento tenemos que decidir qué hacer en caso de que el valor que queremos no estuviera presente.
Para ello se usa orElse() para proporcionar un valor alternativo en caso de que el valor no estuviera presente.
record en vez de clasesEl compilador genera automáticamente constructor, accessores (sin get), equals, hashCode y toString:
record ScreenResolution(int width, int height) {}
record DisplayFeatures(String size, ScreenResolution resolution) {}
record Mobile(long id, String brand, String name,
DisplayFeatures displayFeatures) {}
Con var se infiere el tipo de cada variable local:
public class MobileTester {
public static void main(String[] args) {
var resolution1 = new ScreenResolution(750, 1334);
var dfeatures1 = new DisplayFeatures("4.7", resolution1);
var mobile1 = new Mobile(2015001, "Apple", "iPhone 6s", dfeatures1);
var mService = new MobileService();
int mobileWidth = mService.getMobileScreenWidth(mobile1);
System.out.println("Apple iPhone 6s Screen Width = " + mobileWidth);
var resolution2 = new ScreenResolution(0, 0);
var dfeatures2 = new DisplayFeatures("0", resolution2);
var mobile2 = new Mobile(2015001, "Apple", "iPhone 6s", dfeatures2);
int mobileWidth2 = mService.getMobileScreenWidth(mobile2);
System.out.println("Apple iPhone 16s Screen Width = " + mobileWidth2);
}
}
Reducir boilerplate de if (x!_null): expresión switch con record patterns y unnamed patterns _:
public class MobileService {
public int getMobileScreenWidth(Mobile mobile) {
return switch (mobile) {
case null -> 0;
case Mobile(_, _, _, null) -> 0;
case Mobile(_, _, _, DisplayFeatures(_, null)) -> 0;
case Mobile(_, _, _, DisplayFeatures(_, ScreenResolution(int w, _))) -> w;
};
}
}
| Característica | JDK | Qué aporta |
|---|---|---|
record |
16 | Clase inmutable en una línea; accessors sin prefijo get |
Pattern matching en switch** |
21 | Elimina if (x != null) anidados |
| Record patterns | 21 | Deconstrucción en los case |
Unnamed patterns _ |
22 | Ignora campos del record irrelevantes |
var |
10 | Inferencia de tipo local; reduce repetición de tipos |
OptionalsCon var se infiere el tipo de cada variable local:
public class MobileTesterWithOptional {
public static void main(String[] args) {
var resolution = new ScreenResolution(750, 1334);
var dfeatures = new DisplayFeatures("4.7", Optional.of(resolution));
var mobile = new Mobile(2015001, "Apple", "iPhone 13", Optional.of(dfeatures));
var mService = new MobileService();
int width = mService.getMobileScreenWidth(Optional.of(mobile));
System.out.println("Apple iPhone 13 Screen Width = " + width);
var mobile2 = new Mobile(2015001, "Apple", "iPhone 13", Optional.empty());
int width2 = mService.getMobileScreenWidth(Optional.of(mobile2));
System.out.println("Apple iPhone 13 Screen Width = " + width2);
}
}
record ScreenResolution(int width, int height) {}
record DisplayFeatures(String size, Optional<ScreenResolution> resolution) {}
record Mobile(long id, String brand, String name,
Optional<DisplayFeatures> displayFeatures) {}
Menos boilerplate - Sintaxis fluent con flatMap - Elimina el problema de los nulos:
public class MobileService {
public int getMobileScreenWidth(Optional<Mobile> mobile) {
return mobile.flatMap(Mobile::displayFeatures)
.flatMap(DisplayFeatures::resolution)
.map(ScreenResolution::width)
.orElse(0);
}
}
Optional no ofrecen la posibilidad de que decir qué es lo que ha salido mal (en caso de que no haya valor a devolver).Either y Validation.Lecturas para ampliación: clase Validation en Scala
object EitherLeftRightExample extends App {
def divideXByY(x: Int, y: Int): Either[String, Int] = {
if (y == 0) Left("Can't divide by 0")
else Right(x / y)
}
println(divideXByY(1, 0))
println(divideXByY(1, 1))
divideXByY(1, 0) match {
case Left(s) => println("Answer: " + s)
case Right(i) => println("Answer: " + i)
}
}
Nota: No hay implementaciones de Either en el JDK, pero sí en extensiones funcionales a Java (v.g. functionaljλvλ)
Esta solución, además de reducir el boilerplate, elimina el problema de manejar valores nulos