Uso de Java 8 opcional con Stream :: flatMap


240

El nuevo marco de transmisión Java 8 y sus amigos crean un código Java muy conciso, pero me he encontrado con una situación aparentemente simple que es difícil de hacer de manera concisa.

Considere un List<Thing> thingsy método Optional<Other> resolve(Thing thing). Quiero mapear los Things a Optional<Other>s y obtener el primero Other. La solución obvia sería usar things.stream().flatMap(this::resolve).findFirst(), pero flatMaprequiere que devuelva una secuencia, y Optionalno tiene un stream()método (o es Collectiono proporciona un método para convertirlo o verlo como a Collection).

Lo mejor que se me ocurre es esto:

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .findFirst();

Pero eso parece terriblemente largo para lo que parece un caso muy común. Alguien tiene una mejor idea?


Después de codificar un poco con su ejemplo, en realidad encuentro que la versión explícita es más legible que la relativa, si hubiera existido .flatMap(Optional::toStream), con su versión realmente ve lo que está sucediendo.
skiwi

19
@skiwi Bueno, ahora Optional.streamexiste en JDK 9 ...
Stuart Marks

Tengo curiosidad por saber dónde está documentado y cuál fue el proceso para obtenerlo. Hay algunos otros métodos que realmente parecen existir, y tengo curiosidad por saber dónde se están llevando a cabo los cambios de API.
Yona Appletree


10
¡Lo curioso es que JDK-8050820 en realidad se refiere a esta pregunta en su descripción!
Didier L

Respuestas:


265

Java 9

Optional.stream se ha agregado a JDK 9. Esto le permite hacer lo siguiente, sin la necesidad de ningún método auxiliar:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(Optional::stream)
          .findFirst();

Java 8

Sí, este fue un pequeño agujero en la API, ya que es un poco incómodo convertir un Optional<T>en una longitud de cero o uno Stream<T>. Podrías hacer esto:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(o -> o.isPresent() ? Stream.of(o.get()) : Stream.empty())
          .findFirst();

Sin flatMapembargo, tener el operador ternario dentro es un poco engorroso, por lo que podría ser mejor escribir una pequeña función auxiliar para hacer esto:

/**
 * Turns an Optional<T> into a Stream<T> of length zero or one depending upon
 * whether a value is present.
 */
static <T> Stream<T> streamopt(Optional<T> opt) {
    if (opt.isPresent())
        return Stream.of(opt.get());
    else
        return Stream.empty();
}

Optional<Other> result =
    things.stream()
          .flatMap(t -> streamopt(resolve(t)))
          .findFirst();

Aquí, he incluido la llamada en resolve()lugar de tener una map()operación separada , pero esto es cuestión de gustos.


2
No creo que la API pueda cambiar hasta Java 9 ahora.
Assylias

55
@Hypher Gracias. La técnica .filter (). Map () no es tan mala y evita las dependencias de los métodos auxiliares. Sería bueno si hubiera una forma más concisa. Investigaré si se agrega Optional.stream ().
Stuart Marks el

43
Prefiero:static <T> Stream<T> streamopt(Optional<T> opt) { return opt.map(Stream::of).orElse(Stream.empty()); }
kubek2k

55
Desearía que simplemente agreguen una Optionalsobrecarga a Stream#flatMap... de esa manera podrías escribirstream().flatMap(this::resolve)
hojuelas

44
@flkes Sí, hemos discutido esta idea, pero no parece agregar tanto valor ahora que (en JDK 9) la hay Optional.stream().
Stuart Marks el

69

Estoy agregando esta segunda respuesta basada en una edición propuesta por el usuario srborlongan a mi otra respuesta . Creo que la técnica propuesta fue interesante, pero no fue realmente adecuada como una edición de mi respuesta. Otros estuvieron de acuerdo y la edición propuesta fue rechazada. (Yo no era uno de los votantes). Sin embargo, la técnica tiene mérito. Hubiera sido mejor si srborlongan hubiera publicado su propia respuesta. Esto aún no ha sucedido, y no quería que la técnica se perdiera en las brumas del historial de edición rechazado de StackOverflow, así que decidí sacarlo a la superficie como una respuesta separada.

Básicamente, la técnica es usar algunos de los Optionalmétodos de una manera inteligente para evitar tener que usar un operador ternario ( ? :) o una instrucción if / else.

Mi ejemplo en línea se volvería a escribir de esta manera:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty))
          .findFirst();

Un mi ejemplo que usa un método auxiliar se reescribirá de esta manera:

/**
 * Turns an Optional<T> into a Stream<T> of length zero or one depending upon
 * whether a value is present.
 */
static <T> Stream<T> streamopt(Optional<T> opt) {
    return opt.map(Stream::of)
              .orElseGet(Stream::empty);
}

Optional<Other> result =
    things.stream()
          .flatMap(t -> streamopt(resolve(t)))
          .findFirst();

COMENTARIO

Comparemos las versiones originales y modificadas directamente:

// original
.flatMap(o -> o.isPresent() ? Stream.of(o.get()) : Stream.empty())

// modified
.flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty))

El original es un enfoque directo, aunque profesional: obtenemos un Optional<Other>; si tiene un valor, devolvemos una secuencia que contiene ese valor, y si no tiene valor, devolvemos una secuencia vacía. Bastante simple y fácil de explicar.

La modificación es inteligente y tiene la ventaja de que evita los condicionales. (Sé que a algunas personas no les gusta el operador ternario. Si se usa incorrectamente, puede hacer que el código sea difícil de entender). Sin embargo, a veces las cosas pueden ser demasiado inteligentes. El código modificado también comienza con un Optional<Other>. Luego llama a lo Optional.mapque se define de la siguiente manera:

Si hay un valor presente, aplíquele la función de mapeo proporcionada, y si el resultado no es nulo, devuelva un Opcional que describa el resultado. De lo contrario, devuelva un Opcional vacío.

La map(Stream::of)llamada devuelve un Optional<Stream<Other>>. Si un valor estaba presente en la entrada Opcional, el Opcional devuelto contiene una Corriente que contiene el único resultado Otro. Pero si el valor no estaba presente, el resultado es un Opcional vacío.

A continuación, la llamada a orElseGet(Stream::empty)devuelve un valor de tipo Stream<Other>. Si su valor de entrada está presente, obtiene el valor, que es el elemento único Stream<Other>. De lo contrario (si el valor de entrada está ausente), devuelve un vacío Stream<Other>. Entonces el resultado es correcto, igual que el código condicional original.

En los comentarios que discutieron sobre mi respuesta, con respecto a la edición rechazada, describí esta técnica como "más concisa pero también más oscura". Estoy de acuerdo con esto. Me llevó un tiempo descubrir qué estaba haciendo, y también me llevó un tiempo escribir la descripción anterior de lo que estaba haciendo. La sutileza clave es la transformación de Optional<Other>a Optional<Stream<Other>>. Una vez que entiendes esto tiene sentido, pero no era obvio para mí.

Sin embargo, reconoceré que las cosas que inicialmente son oscuras pueden volverse idiomáticas con el tiempo. Puede ser que esta técnica termine siendo la mejor forma en la práctica, al menos hasta que Optional.streamse agregue (si alguna vez lo hace).

ACTUALIZACIÓN: Optional.stream se ha agregado a JDK 9.


16

No puedes hacerlo más conciso como ya lo estás haciendo.

Afirmas que no quieres .filter(Optional::isPresent) y .map(Optional::get) .

Esto se ha resuelto mediante el método que describe @StuartMarks, sin embargo, como resultado, ahora lo asigna a un Optional<T>, por lo que ahora debe usar .flatMap(this::streamopt)y get()al final.

Por lo tanto, todavía consta de dos declaraciones y ahora puede obtener excepciones con el nuevo método. Porque, ¿qué pasa si cada opcional está vacío? ¡Entonces findFirst()devolverá un opcional vacío y get()fallará!

Entonces lo que tienes:

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .findFirst();

es en realidad la mejor manera de lograr lo que quieres, y es que quieres guardar el resultado como un T, no como un Optional<T>.

Me tomé la libertad de crear una CustomOptional<T>clase que envuelve el Optional<T>y proporciona un método adicional, flatStream(). Tenga en cuenta que no puede extender Optional<T>:

class CustomOptional<T> {
    private final Optional<T> optional;

    private CustomOptional() {
        this.optional = Optional.empty();
    }

    private CustomOptional(final T value) {
        this.optional = Optional.of(value);
    }

    private CustomOptional(final Optional<T> optional) {
        this.optional = optional;
    }

    public Optional<T> getOptional() {
        return optional;
    }

    public static <T> CustomOptional<T> empty() {
        return new CustomOptional<>();
    }

    public static <T> CustomOptional<T> of(final T value) {
        return new CustomOptional<>(value);
    }

    public static <T> CustomOptional<T> ofNullable(final T value) {
        return (value == null) ? empty() : of(value);
    }

    public T get() {
        return optional.get();
    }

    public boolean isPresent() {
        return optional.isPresent();
    }

    public void ifPresent(final Consumer<? super T> consumer) {
        optional.ifPresent(consumer);
    }

    public CustomOptional<T> filter(final Predicate<? super T> predicate) {
        return new CustomOptional<>(optional.filter(predicate));
    }

    public <U> CustomOptional<U> map(final Function<? super T, ? extends U> mapper) {
        return new CustomOptional<>(optional.map(mapper));
    }

    public <U> CustomOptional<U> flatMap(final Function<? super T, ? extends CustomOptional<U>> mapper) {
        return new CustomOptional<>(optional.flatMap(mapper.andThen(cu -> cu.getOptional())));
    }

    public T orElse(final T other) {
        return optional.orElse(other);
    }

    public T orElseGet(final Supplier<? extends T> other) {
        return optional.orElseGet(other);
    }

    public <X extends Throwable> T orElseThrow(final Supplier<? extends X> exceptionSuppier) throws X {
        return optional.orElseThrow(exceptionSuppier);
    }

    public Stream<T> flatStream() {
        if (!optional.isPresent()) {
            return Stream.empty();
        }
        return Stream.of(get());
    }

    public T getTOrNull() {
        if (!optional.isPresent()) {
            return null;
        }
        return get();
    }

    @Override
    public boolean equals(final Object obj) {
        return optional.equals(obj);
    }

    @Override
    public int hashCode() {
        return optional.hashCode();
    }

    @Override
    public String toString() {
        return optional.toString();
    }
}

Verás que agregué flatStream(), como aquí:

public Stream<T> flatStream() {
    if (!optional.isPresent()) {
        return Stream.empty();
    }
    return Stream.of(get());
}

Usado como:

String result = Stream.of("a", "b", "c", "de", "fg", "hij")
        .map(this::resolve)
        .flatMap(CustomOptional::flatStream)
        .findFirst()
        .get();

Usted todavía tendrá que devolver una Stream<T>aquí, ya que no puede volver T, porque si !optional.isPresent(), a continuación, T == nullsi se declara que tales, pero entonces su .flatMap(CustomOptional::flatStream)intentaría añadir nulla una corriente y que no es posible.

Como ejemplo:

public T getTOrNull() {
    if (!optional.isPresent()) {
        return null;
    }
    return get();
}

Usado como:

String result = Stream.of("a", "b", "c", "de", "fg", "hij")
        .map(this::resolve)
        .map(CustomOptional::getTOrNull)
        .findFirst()
        .get();

Ahora lanzará un NullPointerExceptiondentro de las operaciones de transmisión.

Conclusión

El método que usó, en realidad es el mejor método.


6

Una versión ligeramente más corta que usa reduce:

things.stream()
  .map(this::resolve)
  .reduce(Optional.empty(), (a, b) -> a.isPresent() ? a : b );

También puede mover la función de reducción a un método de utilidad estática y luego se convierte en:

  .reduce(Optional.empty(), Util::firstPresent );

66
Me gusta esto, pero vale la pena señalar que esto evaluará todos los elementos de la secuencia, mientras que findFirst () evaluará solo hasta que encuentre un elemento presente.
Duncan McGregor

1
Y desafortunadamente, ejecutar cada resolución es un factor decisivo. Pero es inteligente.
Yona Appletree

5

Como mi respuesta anterior parecía no ser muy popular, le daré otra oportunidad.

Una respuesta corta:

Usted está principalmente en el camino correcto. El código más corto para llegar a la salida deseada que se me ocurre es este:

things.stream()
      .map(this::resolve)
      .filter(Optional::isPresent)
      .findFirst()
      .flatMap( Function.identity() );

Esto se ajustará a todos sus requisitos:

  1. Encontrará la primera respuesta que se resuelva en un vacío Optional<Result>
  2. Llama this::resolveperezosamente según sea necesario
  3. this::resolve no se llamará después del primer resultado no vacío
  4. Volverá Optional<Result>

Respuesta más larga

La única modificación en comparación con la versión inicial de OP fue que la eliminé .map(Optional::get)antes de llamar .findFirst()y la agregué .flatMap(o -> o)como la última llamada de la cadena.

Esto tiene un buen efecto al deshacerse del doble Opcional, siempre que la secuencia encuentra un resultado real.

Realmente no puedes ir más corto que esto en Java.

El fragmento de código alternativo que usa la fortécnica de bucle más convencional será aproximadamente el mismo número de líneas de código y tendrá más o menos el mismo orden y número de operaciones que debe realizar:

  1. llamando this.resolve,
  2. filtrado basado en Optional.isPresent
  3. devolviendo el resultado y
  4. alguna forma de lidiar con resultados negativos (cuando no se encontró nada)

Solo para demostrar que mi solución funciona como se anuncia, escribí un pequeño programa de prueba:

public class StackOverflow {

    public static void main( String... args ) {
        try {
            final int integer = Stream.of( args )
                    .peek( s -> System.out.println( "Looking at " + s ) )
                    .map( StackOverflow::resolve )
                    .filter( Optional::isPresent )
                    .findFirst()
                    .flatMap( o -> o )
                    .orElseThrow( NoSuchElementException::new )
                    .intValue();

            System.out.println( "First integer found is " + integer );
        }
        catch ( NoSuchElementException e ) {
            System.out.println( "No integers provided!" );
        }
    }

    private static Optional<Integer> resolve( String string ) {
        try {
            return Optional.of( Integer.valueOf( string ) );
        }
        catch ( NumberFormatException e )
        {
            System.out.println( '"' + string + '"' + " is not an integer");
            return Optional.empty();
        }
    }

}

(Tiene pocas líneas adicionales para depurar y verificar que solo se resuelvan tantas llamadas como sea necesario ...)

Al ejecutar esto en una línea de comando, obtuve los siguientes resultados:

$ java StackOferflow a b 3 c 4
Looking at a
"a" is not an integer
Looking at b
"b" is not an integer
Looking at 3
First integer found is 3

Pienso lo mismo que Roland Tepp. ¿Por qué alguien haría una transmisión <stream <? >> y plana cuando se puede simplemente aplastar con una opción <opcional <? >>
Young Hyun Yoo

3

Si no le importa usar una biblioteca de terceros, puede usar Javaslang . Es como Scala, pero implementado en Java.

Viene con una biblioteca de colección inmutable completa que es muy similar a la conocida por Scala. Estas colecciones reemplazan las colecciones de Java y el Stream de Java 8. También tiene su propia implementación de Opción.

import javaslang.collection.Stream;
import javaslang.control.Option;

Stream<Option<String>> options = Stream.of(Option.some("foo"), Option.none(), Option.some("bar"));

// = Stream("foo", "bar")
Stream<String> strings = options.flatMap(o -> o);

Aquí hay una solución para el ejemplo de la pregunta inicial:

import javaslang.collection.Stream;
import javaslang.control.Option;

public class Test {

    void run() {

        // = Stream(Thing(1), Thing(2), Thing(3))
        Stream<Thing> things = Stream.of(new Thing(1), new Thing(2), new Thing(3));

        // = Some(Other(2))
        Option<Other> others = things.flatMap(this::resolve).headOption();
    }

    Option<Other> resolve(Thing thing) {
        Other other = (thing.i % 2 == 0) ? new Other(i + "") : null;
        return Option.of(other);
    }

}

class Thing {
    final int i;
    Thing(int i) { this.i = i; }
    public String toString() { return "Thing(" + i + ")"; }
}

class Other {
    final String s;
    Other(String s) { this.s = s; }
    public String toString() { return "Other(" + s + ")"; }
}

Descargo de responsabilidad: soy el creador de Javaslang.


3

Tarde a la fiesta, pero ¿qué pasa con

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .findFirst().get();

Puede deshacerse del último get () si crea un método util para convertir opcional para transmitir manualmente:

things.stream()
    .map(this::resolve)
    .flatMap(Util::optionalToStream)
    .findFirst();

Si devuelve la secuencia directamente desde su función de resolución, guarda una línea más.


3

Me gustaría promover métodos de fábrica para crear ayudantes para API funcionales:

Optional<R> result = things.stream()
        .flatMap(streamopt(this::resolve))
        .findFirst();

El método de fábrica:

<T, R> Function<T, Stream<R>> streamopt(Function<T, Optional<R>> f) {
    return f.andThen(Optional::stream); // or the J8 alternative:
    // return t -> f.apply(t).map(Stream::of).orElseGet(Stream::empty);
}

Razonamiento:

  • Al igual que con las referencias de métodos en general, en comparación con las expresiones lambda, no puede capturar accidentalmente una variable del alcance accesible, como:

    t -> streamopt(resolve(o))

  • Es composable, puede, por ejemplo, llamar Function::andThenal resultado del método de fábrica:

    streamopt(this::resolve).andThen(...)

    Mientras que en el caso de una lambda, primero deberías lanzarla:

    ((Function<T, Stream<R>>) t -> streamopt(resolve(t))).andThen(...)


3

Null es compatible con Stream proporcionado por My library AbacusUtil . Aquí está el código:

Stream.of(things).map(e -> resolve(e).orNull()).skipNull().first();

3

Si está atascado con Java 8 pero tiene acceso a Guava 21.0 o posterior, puede usarlo Streams.streampara convertir un opcional en una transmisión.

Por lo tanto, dado

import com.google.common.collect.Streams;

puedes escribir

Optional<Other> result =
    things.stream()
        .map(this::resolve)
        .flatMap(Streams::stream)
        .findFirst();

0

¿Qué hay de eso?

private static List<String> extractString(List<Optional<String>> list) {
    List<String> result = new ArrayList<>();
    list.forEach(element -> element.ifPresent(result::add));
    return result;
}

https://stackoverflow.com/a/58281000/3477539


¿Por qué hacer esto cuando puedes transmitir y recopilar?
OneCricketeer

return list.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())), al igual que la pregunta (y su respuesta vinculada) tiene ...
OneCricketeer

Puedo estar equivocado, pero considero que usar isPresent () y luego get () no es una buena práctica. Así que trato de alejarme de eso.
rastaman

Si usa .get() sin isPresent() , recibirá una advertencia en IntelliJ
OneCricketeer

-5

Lo más probable es que lo estés haciendo mal.

Java 8 Opcional no está destinado a ser utilizado de esta manera. Por lo general, solo está reservado para las operaciones de transmisión de terminal que pueden o no devolver un valor, como por ejemplo encontrar.

En su caso, podría ser mejor intentar primero encontrar una forma barata de filtrar los elementos que se pueden resolver y luego obtener el primer elemento como opcional y resolverlo como una última operación. Mejor aún: en lugar de filtrar, encuentre el primer elemento resoluble y resuélvalo.

things.filter(Thing::isResolvable)
      .findFirst()
      .flatMap(this::resolve)
      .get();

La regla general es que debe esforzarse por reducir el número de elementos en la secuencia antes de transformarlos en otra cosa. YMMV por supuesto.


66
Creo que el método resolve () del OP que devuelve Opcional <Other> es un uso perfectamente sensato de Opcional. No puedo hablar con el dominio del problema del OP, por supuesto, pero podría ser que la forma de determinar si algo se puede resolver es intentar resolverlo. Si es así, Opcional fusiona un resultado booleano de "fue esto resoluble" con el resultado de la resolución, si tiene éxito, en una sola llamada API.
Stuart Marks

2
Stuart es básicamente correcto. Tengo un conjunto de términos de búsqueda en orden de conveniencia, y estoy buscando encontrar el resultado del primero que devuelve algo. Así que, básicamente Optional<Result> searchFor(Term t). Eso parece ajustarse a la intención de Opcional. Además, stream () s debe ser evaluado de manera perezosa, por lo que no debe ocurrir ningún trabajo adicional para resolver los términos más allá del primero coincidente.
Yona Appletree

La pregunta es perfectamente sensata y el uso de flatMap con Opcional a menudo se practica en otros lenguajes de programación similares, como Scala.
dzs
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.