4 Optional¶
Introducción¶
La clase Optional<T>
es un wrapper alrededor de un valor que puede estar presente o no. El objetivo de esta clase es servir básicamente como tipo de retorno de aquellos métodos que pueden retornar una valor o no.
Por ejemplo, supongamos que queremos realizar un método que retorne la posición en la que se encuentra un determinado elemento de una lista de enteros. La pregunta que inmediatamente nos haríamos como desarrolladores sería, ¿qué debo hacer si el elemento a buscar, que ha sido pasado como argumento del método, no se encuentra en la lista? Debo lanzar una excepción? No puedo retornar false
porque el método retorna un entero. ¿Debo retornar un valor especial como por ejemplo -1
?¿Retorno como valor especial del valor null
?
Tradicionalmente los desarrolladores han resulto esta situación de distintas maneras y todas tienen sus inconvenientes. Por un lado, lanzar una excepción parece excesivo porque no es un error del programa y al cliente del método simplemente hay que informarle de alguna manera de que no se ha encontrado el elemento.
Por otra parte, retornar un valor especial tiene el inconveniente que de que obliga a que el cliente conozca dicho valor especial y además debe acordarse de comprobar que el valor retornada no es el valor especial si quiere usarlo. En cierta manera estamos dándolo al cliente la responsabilidad de la comprobación pero no estamos obligándola a ello. La consecuencia es que si el desarrollador del código cliente olvida realizar la comprobación, estará usando un valor no válido. Este hecho se convierte en más peligroso aún si el valor retornado es null
, porque si olvida realizar la comprobación y más adelante en el código se trata de acceder a una propiedad del objeto retornado por el método, se produciría una excepción NullPointerException
.
Entonces, ¿cuál es la solución? La solución propuesta por Java 8 es que el método no retorne directamente un objeto de clase T
, sino un Optional<T>
. La ventaja de esta solución es que si el código cliente quiere acceder al objeto real debe forzosamente comprobar si el Optional tiene un valor presente o no, de manera que pueda extraerse.
Es muy importante resaltar que no es posible tener un Optional cuyo valor contenido sea null.
Construcción¶
La clase Optional<T>
es una clase inmutable, por lo que no proporciona métodos setter que permitan cambiar el valor que contiene. Además el proceso de construcción de un Optional no se realizar a través de un constructor, sino usando alguno de los métodos estáticos factoría que se indican a continuación.
static <T> Optional<T> empty()
: retorna un Optional que no contiene valor.static <T> Optional<T> of(T value)
: retorna un Optional que contiene un valor pasado como argumento. Si tratamos de pasar el valornull
a dicho método se producirá una excepción.static <T> Optional<T> ofNullable(T value)
: retorna un Optional que contiene el valor pasado como argumento. A diferencia del método anterior, si tratamos de pasar el valornull
, el método retorna un Optional vacío.
import java.util.Optional;
public class Construction{
public void show(){
Optional<Integer> optional1 = Optional.empty(); //(1)!
System.out.println(optional1);
Optional<Integer> optional2 = Optional.of(1000); //(2)!
System.out.println(optional2);
Optional<Integer> optional3 = Optional.ofNullable(methodWhichCanReturnNull(5)); //(3)!
System.out.println(optional3);
Optional<Integer> optional4 = Optional.ofNullable(methodWhichCanReturnNull(2));
System.out.println(optional4);
Optional<Integer> optional5;
try {
optional5 = Optional.of(null);
System.out.println(optional5);
} catch (NullPointerException e) {
System.out.println("Se ha lanzado la excepción NullPointerException");
}
}
public static Integer methodWhichCanReturnNull(int num){
if (num >= 5) {
return num;
} else {
return null;
}
}
public static void main(String[] args) {
new Construction().show();
}
}
- Crea un optional sin valor.
- Crea un optional con valor 1000
- Retorna un Optional vacío si el valor pasado como argumento es null:
Igualdad¶
Podemos comparar los valores de dos Optional con el método boolean equals(Object obj)
: compara los valores de los dos Optional. Dos Optional vacíos de distinto tipo son considerados iguales:
import java.util.Optional;
public class Equality {
public void show() {
Optional<Integer> optional1 = Optional.of(1000); //Crea un optional con valor 1000
Optional<Integer> optional2 = Optional.of(1000);
Optional<Integer> optional3 = Optional.empty();
Optional<String> optional4 = Optional.empty();
System.out.println(optional1.equals(optional2)); //Comprueba si tienen el mismo valor: true
System.out.println(optional1 == optional2); //Comprueba si son el mismo objeto: false
System.out.println(optional3.equals(optional4));//Dos Optional vacíos de distinto tipo son considerados iguales: true
System.out.println(optional3.equals(null));//False
}
public static void main(String[] args) {
new Equality().show();
}
}
Procesamiento¶
Supongamos que queremos usar el método estático Collections.max(collection)
que retorna el valor máximo contenido en una colección:
Pero ¿qué ocurre si la colección está vacía? En este caso el método max()
lanzará la excepción NoSuchElementException
. ¿Cuál es el problema? Que el cliente debe mirar la documentación para enterarse de ello y capturar la excepción o de lo contrario producirá un error en tiempo de ejecución. Está obligando a darse cuenta de que debe hacer:
¿Y si existiera una forma de indicar que el valor de retorno de una función es opcional? Pues bien, Java 8 introdujo para este problema la clase Optional<T>
.
La clase Optional<T>
es una clase parametrizada que representa la abstracción de un valor de retorno opcional. Así, podríamos codificar nuestro método anterior haciendo que retorno un Optional<T>
en vez de lanzar una excepción en el caso de que la colección esté vacía:
public static <T extends Comparable<T>> Optional<T> max(Collection <? extends T> coll){
try{
return Optional.of(Collections.max(coll));
} catch(NoSuchElementException){
return Optional.empty();
}
}
En ese caso, cuando el cliente llame a este método estará obligado a tratar el hecho de que puede que no se retorne un valor. No tiene que mirar la documentación ni recordar hacer ningún tipo de comprobación porque el tipo de terno es Optional<T>
, de manera que si de verdad quiere obtener el valor, va a tener que comprobar si el Optional tiene o no valor.
Veamos más métodos de Optional:
T get()
: si un valor está presente, devuelve el valor, de lo contrario lanza la excepción NoSuchElementException.-
boolean isPresent()
: si un valor está presente, devuelve true, en caso contrario false.import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; public class GetAndIsPresent { public void show() { List<Integer> list1 = List.of(3, 2, 6, 1, 5, 4); List<Integer> list2 = new ArrayList<>(); Optional<Integer> maximumOptional1 = max(list1); Optional<Integer> maximumOptional2 = max(list2); Integer maximum1 = 0, maximum2; if (maximumOptional1.isPresent()) { maximum1 = maximumOptional1.get(); } System.out.println(maximumOptional1);//Optional[6] System.out.println(maximum1);//6 maximum2 = maximumOptional2.isPresent() ? maximumOptional2.get() : 0; System.out.println(maximumOptional2);//Optional.empty System.out.println(maximum2);//0 } public static <T extends Comparable<T>> Optional<T> max(Collection<? extends T> coll) { try { return Optional.of(Collections.max(coll)); } catch (NoSuchElementException e) { return Optional.empty(); } } public static void main(String[] args) { new GetAndIsPresent().show(); } }
-
T orElse(T other)
: si un valor está presente, devuelve el valor, de lo contrario devuelve el valor suministrado como argumento T orElseGet(Supplier<? extends T> supplier)
: si hay un valor, devuelve el valor, en caso contrario devuelve el resultado producido por la función suministradora.T orElseThrow()
: si un valor está presente, devuelve el valor, de lo contrario lanza la excepción NoSuchElementException.-
<X extends Throwable> T orElseThrow(Suppler<? extends X> exceptionSupplier) throws x
: si hay un valor, devuelve el valor, en caso contrario, lanza una excepción producida por la función suministradora.import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; import java.util.Random; public class OrElse { public void show() { List<Integer> list1 = List.of(3, 2, 6, 1, 5, 4); List<Integer> list2 = new ArrayList<>(); Optional<Integer> maximumOptional1 = max(list1); Optional<Integer> maximumOptional2 = max(list2); Integer maximum1, maximum2; maximum1 = maximumOptional1.orElse(0); maximum2 = maximumOptional2.orElse(0); System.out.println(maximum1);//6 System.out.println(maximum2);//0 maximum1 = maximumOptional1.orElseGet(() -> obtainRandomNumber()); maximum2 = maximumOptional2.orElseGet(() -> obtainRandomNumber()); System.out.println(maximum1);//6 System.out.println(maximum2);//Número aleatorio entre 1 y 10 maximum1 = maximumOptional1.orElseThrow(); System.out.println(maximum1);//6 try { maximum2 = maximumOptional2.orElseThrow(); } catch (NoSuchElementException e) { System.out.println("Valor no presente"); } maximum1 = maximumOptional1.orElseThrow(IllegalStateException::new); System.out.println(maximum1);//6 try { maximum2 = maximumOptional2.orElseThrow(IllegalStateException::new); } catch (IllegalStateException e) { System.out.println("Valor no presente"); } } public Integer obtainRandomNumber() { return new Random().nextInt(10) + 1;//Devuelve un número aleatorio entre 1 y 10 } public static <T extends Comparable<T>> Optional<T> max(Collection<? extends T> coll){ try { return Optional.of(Collections.max(coll)); } catch (NoSuchElementException e) { return Optional.empty(); } } public static void main(String[] args) { new OrElse().show(); }
-
Optional<T> or(Supplier<? extends Optional<? extends T>> supplier)
: se introdujo en Java 9 para encadenar Optionals. Si el Optional original contiene un valor, el método retorna un nuevo Optional con dicho valor. Si el Optional original no contiene el valor, el método retornará el Optional producido por la función suministradora. <U> Optional<U> map(Function <? super T, ? extends U> mapper)
: sirve para transformar el valor contenido en un Optional. Si el Optional original está vacío, devuelve un nuevo Optional vacío y si tiene valor, devolverá el Optional que contendrá como valor el resultado de aplicar la función de transformación al valor contenido en el Optional original. Si dicha función devuelve un resultado nulo, entonces se devuelve un Optional vacío.-
Optional<T> filter(Predicate <? super T> predicate)
: retorna un nuevo Optional que estará vacío si el Optional original estaba vacío o si no se cumple el predicado recibido como argumento (su métodotest()
retornafalse
). Si el Optional original contenía un valor y dicho valor cumple con el predicado, el nuevo Optional retornado contendrá el valor del Optional original.import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; import java.util.Random; public class OrMapFilter { public void show() { List<Integer> list1 = List.of(3, 2, 6, 1, 5, 4); List<Integer> list2 = new ArrayList<>(); Optional<Integer> maximumOptional1 = max(list1); Optional<Integer> maximumOptional2 = max(list2); Optional<Integer> maximumOptional3, maximumOptional4, maximumOptional5; maximumOptional3 = maximumOptional1.or(() -> Optional.of(obtainRandomNumber())); maximumOptional4 = maximumOptional2.or(() -> Optional.of(obtainRandomNumber())); System.out.println(maximumOptional3);//Optional[6] System.out.println(maximumOptional4);//Optional de un número aleatorio maximumOptional3 = maximumOptional1.map(n -> n * 2); maximumOptional4 = maximumOptional2.map(n -> n * 2); System.out.println(maximumOptional3);//Optional[12] System.out.println(maximumOptional4);//Optional.empty maximumOptional3 = maximumOptional1.filter(n -> n % 2 == 0); maximumOptional4 = maximumOptional1.filter(n -> n % 2 != 0); maximumOptional5 = maximumOptional2.filter(n -> n % 2 == 0); System.out.println(maximumOptional3);//Optional[6] System.out.println(maximumOptional4);//Optional.empty System.out.println(maximumOptional5);//Optional.empty } public Integer obtainRandomNumber() { return new Random().nextInt(10) + 1;//Devuelve un número aleatorio entre 1 y 10 } public static <T extends Comparable<T>> Optional<T> max(Collection<? extends T> coll) { try { return Optional.of(Collections.max(coll)); } catch (NoSuchElementException e) { return Optional.empty(); } } public static void main(String[] args) { new OrMapFilter().show(); } }
-
void ifPresent(Consumer <? super T> action)
: permite consumir (usar) directamente el valor contenido en Optional si es que éste contiene un valor. Si el Optional está vacío, no hace nada. void ifPresentOrElse(Consumer <? super T> action, Runnable emptyAction)
: se incorporó este método en Java 9. Ejecuta el consumidor pasado como argumento si el Optional posee un valor o ejecuta el Runnable pasado como argumento si el Optional no contiene ningún valor.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
public class IfPresent {
public void show() {
List<Integer> list1 = List.of(3, 2, 6, 1, 5, 4);
List<Integer> list2 = new ArrayList<>();
Optional<Integer> maximumOptional1 = max(list1);
Optional<Integer> maximumOptional2 = max(list2);
maximumOptional1.ifPresent(System.out::println);//6
maximumOptional2.ifPresent(System.out::println);//No hace nada
maximumOptional1.ifPresentOrElse(System.out::println,() -> System.out.println("No hay máximo porque la lista está vacía"));//6
maximumOptional2.ifPresentOrElse(System.out::println,() -> System.out.println("No hay máximo porque la lista está vacía"));//No hay máximo porque la lista está vacía
}
public static <T extends Comparable<T>> Optional<T> max(Collection<? extends T> coll){
try {
return Optional.of(Collections.max(coll));
} catch (NoSuchElementException e) {
return Optional.empty();
}
}
public static void main(String[] args) {
new IfPresent().show();
}
}
Desafortunadamente, no podemos garantizar que la referencia al objeto Optional en sí no sea null
. De hecho, en todos los ejemplos anteriores estamos confiando en que el método max()
nunca va a retornar un null
. Otros lenguajes de programación más modernos, como Kotlin o Swift, incorporan el concepto de Optional al sistema de tipos, de manera que existirán dos tipos distintos, uno que incorpora la posibilidad de tener el valor null
y otro tipo que no lo permite. Por ejemplo, en Kotlin existe el tipo String
, que no puede contener el valor null
y el tipo String?
, que sí puede contenerlo. Al establecer el tipo de retorno de una función tendremos que decidir si el tipo de retorno del método es String?
, el cliente deberá comprobar si realmente se ha retornado un valor o no, pero siempre podrá realizar la comprobación. El problema de Java es que al encapsular un valor en un objeto Optional, el propio objeto Optional podría ser null
y deberíamos fiarnos de que eso nunca puede suceder o realizar la comprobación cada vez. Evidentemente, cualquier desarrollador que realiza un método que retorna un Optional debería asegurarse y documentar que dicho método nunca retornará null
, sino siempre un Optional.
Dónde no usar Optional¶
Debemos tener en cuenta que la clase Optional<T>
ha sido diseñada específicamente para ser usada como tipo de retorno de los métodos. No se recomienda su uso en los siguientes casos:
- No se recomienda usar Optional como tipo de los atributos de una clase.
- No se recomienda usar Optional como tipo de los parámetros de un método porque ensucian mucho el código y realmente no hacen que el parámetro sea opcional.
- No se recomienda usar Optional como tipo de una colección, como por ejemplo en una lista.