Opcional o Else Opcional en Java

137

He estado trabajando con el nuevo tipo Opcional en Java 8 , y me he encontrado con lo que parece una operación común que no es compatible funcionalmente: un "orElseOptional"

Considere el siguiente patrón:

Optional<Result> resultFromServiceA = serviceA(args);
if (resultFromServiceA.isPresent) return result;
else {
    Optional<Result> resultFromServiceB = serviceB(args);
    if (resultFromServiceB.isPresent) return resultFromServiceB;
    else return serviceC(args);
}

Hay muchas formas de este patrón, pero se reduce a querer un "o Else" en un opcional que toma una función que produce un nuevo opcional, llamado solo si el actual no existe.

Su implementación se vería así:

public Optional<T> orElse(Supplier<Optional<? extends T>> otherSupplier) {
    return value != null ? this : other.get();
}

Tengo curiosidad por saber si hay una razón por la que dicho método no existe, si solo estoy usando Opcional de forma no intencionada, y qué otras formas han encontrado las personas para tratar este caso.

Debo decir que creo que las soluciones que involucran clases / métodos de utilidad personalizados no son elegantes porque las personas que trabajan con mi código no necesariamente sabrán que existen.

Además, si alguien sabe, ¿se incluirá dicho método en JDK 9, y dónde podría proponerlo? Esto me parece una omisión bastante evidente de la API.

Yona Appletree
fuente
13
Ver este número . Para aclarar: esto ya va a estar en Java 9, si no en una futura actualización de Java 8.
Obicere
¡Es! Gracias, no encontré eso en mi búsqueda.
Yona Appletree
2
@Obicere Ese problema no se aplica aquí porque se trata del comportamiento en vacío Opcional, no de un resultado alternativo . Opcional ya tiene orElseGet()lo que OP necesita, solo que no genera una buena sintaxis en cascada.
Marko Topolnik
1
Excelentes tutoriales en Java Opcional: codeflex.co/java-optional-no-more-nullpointerexception
John Detroit

Respuestas:

86

Esto es parte de JDK 9 en forma de or, que toma a Supplier<Optional<T>>. Su ejemplo sería entonces:

return serviceA(args)
    .or(() -> serviceB(args))
    .or(() -> serviceC(args));

Para más detalles, consulte el Javadoc o esta publicación que escribí.

Nicolai
fuente
Agradable. Esa adición debe tener un año y no me di cuenta. Con respecto a la pregunta en su blog, cambiar el tipo de retorno rompería la compatibilidad binaria, ya que las instrucciones de invocación de bytecode se refieren a la firma completa, incluido el tipo de retorno, por lo que no hay posibilidad de cambiar el tipo de retorno de ifPresent. Pero de todos modos, creo que el nombre ifPresentno es bueno de todos modos. Para todos los otros métodos que no lleva “si no” en el nombre (como map, filter, flatMap), se da a entender que no hacen nada si no hay valor está presente, así que ¿por qué ifPresent...
Holger
Por lo tanto, agregar un Optional<T> perform(Consumer<T> c)método para permitir el encadenamiento perform(x).orElseDo(y)( orElseDocomo alternativa a su propuesta ifEmpty, para ser coherente en tener elseen el nombre de todos los métodos que podrían hacer algo por los valores ausentes). Puede imitar eso performen Java 9 a través de stream().peek(x).findFirst()aunque es un abuso de la API y todavía no hay forma de ejecutar un Runnablesin especificar un Consumeral mismo tiempo ...
Holger
65

El enfoque más limpio de "probar servicios" dada la API actual sería:

Optional<Result> o = Stream.<Supplier<Optional<Result>>>of(
    ()->serviceA(args), 
    ()->serviceB(args), 
    ()->serviceC(args), 
    ()->serviceD(args))
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();

El aspecto importante no es la cadena (constante) de operaciones que tiene que escribir una vez, sino cuán fácil es agregar otro servicio (o modificar la lista de servicios es general). Aquí, agregar o eliminar un solo ()->serviceX(args)es suficiente.

Debido a la evaluación diferida de las secuencias, no se invocará ningún servicio si un servicio anterior devuelve un no vacío Optional.

Holger
fuente
13
solo usé eso en un proyecto, gracias a Dios no hacemos revisiones de código.
Ilya Smagin
3
Es mucho más limpio, que una cadena de "o ElseGet", pero también es mucho más difícil de leer.
slartidan
44
Esto es ciertamente correcto ... pero honestamente, me lleva un segundo analizarlo y no salgo con la seguridad de que es correcto. Eso sí, me doy cuenta de que este ejemplo es, pero podría imaginar un pequeño cambio que haría que no se evaluara perezosamente o que tuviera algún otro error que no se distinguiera de un vistazo. Para mí, esto cae en la categoría de ser-de-decente como una función de utilidad.
Yona Appletree
3
Me pregunto si cambiar .map(Optional::get)con .findFirst()hará que sea más fácil "leer", por ejemplo, ¿ .filter(Optional::isPresent).findFirst().map(Optional::get)se puede "leer" como "encontrar el primer elemento en la secuencia para la cual Opcional :: isPresent es verdadero y luego aplanarlo aplicando Opcional :: obtener"?
schatten
3
Es curioso, publiqué una solución muy similar para una pregunta similar hace unos meses. Esta es la primera vez que me encuentro con esto.
shmosel
34

No es bonito, pero esto funcionará:

return serviceA(args)
  .map(Optional::of).orElseGet(() -> serviceB(args))
  .map(Optional::of).orElseGet(() -> serviceC(args))
  .map(Optional::of).orElseGet(() -> serviceD(args));

.map(func).orElseGet(sup)es un patrón bastante útil para usar con Optional. Significa "Si esto Optionalcontiene valor v, dame func(v), de lo contrario dame sup.get()".

En este caso, llamamos serviceA(args)y recibimos un Optional<Result>. Si eso Optionalcontiene valor v, queremos obtenerlo Optional.of(v), pero si está vacío, queremos obtenerlo serviceB(args). Enjuague y repita con más alternativas.

Otros usos de este patrón son

  • .map(Stream::of).orElseGet(Stream::empty)
  • .map(Collections::singleton).orElseGet(Collections::emptySet)
Misha
fuente
1
eh, cuando uso esta estrategia, eclipse dice: "El método orElseGet (Proveedor <? extiende String>) en el tipo Opcional <String> no es aplicable para los argumentos (() -> {})" No parece considerar devolver un Opcional una estrategia válida, en la Cadena ??
chrismarx
@chrismarx () -> {}no devuelve un Optional. ¿Qué estás tratando de lograr?
Misha
1
Solo estoy tratando de seguir el ejemplo. Encadenar las llamadas del mapa no funciona. Mis servicios devuelven cadenas y, por supuesto, no hay una opción .map () disponible entonces
chrismarx
1
Excelente alternativa legible a la próxima versión or(Supplier<Optional<T>>)de Java 9
David M.
1
@ Sheepy estás equivocado. .map()en un vacío Optionalproducirá un vacío Optional.
Misha
27

Quizás esto es lo que buscas: obtener valor de uno Opcional u otro

De lo contrario, es posible que desee echar un vistazo Optional.orElseGet. Aquí hay un ejemplo de lo que creo que buscas:

result = Optional.ofNullable(serviceA().orElseGet(
                                 () -> serviceB().orElseGet(
                                     () -> serviceC().orElse(null))));
aioobe
fuente
2
Esto es lo que suelen hacer los genios. Evalúe lo opcional como anulable y envolverlo ofNullablees lo mejor que he visto.
Jin Kwon
5

Asumiendo que todavía estás en JDK8, hay varias opciones.

Opción # 1: crea tu propio método auxiliar

P.ej:

public class Optionals {
    static <T> Optional<T> or(Supplier<Optional<T>>... optionals) {
        return Arrays.stream(optionals)
                .map(Supplier::get)
                .filter(Optional::isPresent)
                .findFirst()
                .orElseGet(Optional::empty);
    }
}

Para que puedas hacer:

return Optionals.or(
   ()-> serviceA(args),
   ()-> serviceB(args),
   ()-> serviceC(args),
   ()-> serviceD(args)
);

Opción # 2: usa una biblioteca

Por ejemplo, Google guava's Opcional admite una or()operación adecuada (al igual que JDK9), por ejemplo:

return serviceA(args)
  .or(() -> serviceB(args))
  .or(() -> serviceC(args))
  .or(() -> serviceD(args));

(Donde cada uno de los servicios regresa com.google.common.base.Optional, en lugar de java.util.Optional).

Sheepy
fuente
No encontré Optional<T>.or(Supplier<Optional<T>>)en los documentos de guayaba. ¿Tienes un enlace para eso?
Tamas Hegedus
.orElseGet devuelve T pero Opcional :: vacío devuelve Opcional <T>. Opcional. De Anulable (........ o Else (nulo)) tiene el efecto deseado según lo descrito por @aioobe.
Miguel Pereira
@TamasHegedus ¿Quieres decir en la opción # 1? Es una implementación personalizada que se encuentra en la parte superior cortada. PD: perdón por la respuesta tardía
Sheepy
@MiguelPereira .orElseGet en este caso devuelve Opcional <T> porque está funcionando en Opcional <Opcional <T>>
Sheepy
2

Esto parece un buen ajuste para la coincidencia de patrones y una interfaz de opción más tradicional con algunas y ninguna implementaciones (como las de Javaslang , FunctionalJava ) o una implementación perezosa Quizás en cyclops-react Soy el autor de esta biblioteca.

Con cyclops-react también puede usar la coincidencia de patrones estructurales en tipos JDK. Para Opcional, puede coincidir en los casos presentes y ausentes a través del patrón de visitante . se vería algo así:

  import static com.aol.cyclops.Matchables.optional;

  optional(serviceA(args)).visit(some -> some , 
                                 () -> optional(serviceB(args)).visit(some -> some,
                                                                      () -> serviceC(args)));
John McClean
fuente