Usando o Java 8 opcional com Stream :: flatMap


240

A nova estrutura de fluxo do Java 8 e os amigos criam um código java muito conciso, mas me deparei com uma situação aparentemente simples que é difícil de fazer de forma concisa.

Considere a List<Thing> thingse método Optional<Other> resolve(Thing thing). Quero mapear os Thingto Optional<Other>s e obter o primeiro Other. A solução óbvia seria usar things.stream().flatMap(this::resolve).findFirst(), mas flatMaprequer que você retorne um fluxo e Optionalnão tenha um stream()método (ou é um Collectionou fornece um método para convertê-lo ou visualizá-lo como a Collection).

O melhor que posso apresentar é o seguinte:

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

Mas isso parece muito longo para o que parece ser um caso muito comum. Alguem tem uma ideia melhor?


Depois de codificar um pouco com o seu exemplo, acho que a versão explícita é mais legível do que a que diz respeito, se existisse .flatMap(Optional::toStream), à sua versão e você realmente vê o que está acontecendo.
skiwi

19
@skiwi Bem, Optional.streamexiste no JDK 9 agora ....
Stuart Marcas

Estou curioso para saber onde isso está documentado e qual foi o processo para obtê-lo. Existem alguns outros métodos que realmente parecem existir, e estou curioso para saber onde as discussões sobre mudanças na API estão ocorrendo.
Yona Appletree


10
O engraçado é que o JDK-8050820 realmente se refere a essa pergunta em sua descrição!
Didier L

Respostas:


265

Java 9

Optional.stream foi adicionado ao JDK 9. Isso permite que você faça o seguinte, sem a necessidade de qualquer método auxiliar:

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

Java 8

Sim, esse foi um pequeno furo na API, pois é um pouco inconveniente transformar um Optional<T>em um comprimento zero ou um Stream<T>. Você pode fazer isso:

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

flatMapPorém, ter o operador ternário dentro dele é um pouco complicado, portanto, talvez seja melhor escrever uma pequena função auxiliar para fazer isso:

/**
 * 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();

Aqui, indiquei a chamada em resolve()vez de ter uma map()operação separada , mas isso é uma questão de gosto.


2
Eu não acho que a API possa mudar até o Java 9 agora.
assylias 29/03

5
@Hypher Obrigado. A técnica .filter (). Map () não é muito ruim e evita dependências nos métodos auxiliares. Seria bom se houvesse uma maneira mais concisa. Investigarei a adição de Optional.stream ().
Stuart Marks

43
Eu prefiro:static <T> Stream<T> streamopt(Optional<T> opt) { return opt.map(Stream::of).orElse(Stream.empty()); }
kubek2k 25/02

5
Gostaria que gostaria apenas de acrescentar uma Optionalsobrecarga para Stream#flatMap... de que maneira você poderia apenas escreverstream().flatMap(this::resolve)
flocos

4
@ Flkes Sim, contornamos essa ideia, mas ela não parece agregar tanto valor agora que existe (no JDK 9) Optional.stream().
Stuart Marks

69

Estou adicionando esta segunda resposta com base em uma edição proposta pelo usuário srborlongan à minha outra resposta . Eu acho que a técnica proposta foi interessante, mas não foi realmente adequada como uma edição da minha resposta. Outros concordaram e a edição proposta foi rejeitada. (Eu não era um dos eleitores.) A técnica tem mérito, no entanto. Teria sido melhor se srborlongan tivesse postado sua própria resposta. Isso ainda não aconteceu, e eu não queria que a técnica fosse perdida nas brumas do histórico de edição rejeitado do StackOverflow, então decidi apresentá-la como uma resposta separada.

Basicamente, a técnica é usar alguns dos Optionalmétodos de maneira inteligente para evitar a necessidade de usar um operador ternário ( ? :) ou uma declaração if / else.

Meu exemplo embutido seria reescrito desta maneira:

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

Um exemplo meu que usa um método auxiliar seria reescrito desta maneira:

/**
 * 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();

COMENTÁRIO

Vamos comparar as versões originais vs modificadas diretamente:

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

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

O original é uma abordagem direta, se for de trabalhador: obtemos um Optional<Other>; se tiver um valor, retornamos um fluxo contendo esse valor e, se não tiver valor, retornamos um fluxo vazio. Muito simples e fácil de explicar.

A modificação é inteligente e tem a vantagem de evitar condicionais. (Eu sei que algumas pessoas não gostam do operador ternário. Se mal utilizado, pode realmente dificultar a compreensão do código.) No entanto, às vezes as coisas podem ser muito inteligentes. O código modificado também começa com um Optional<Other>. Em seguida, chama o Optional.mapque é definido da seguinte forma:

Se um valor estiver presente, aplique a função de mapeamento fornecida a ele e, se o resultado for não nulo, retorne um Opcional descrevendo o resultado. Caso contrário, retorne um opcional opcional.

A map(Stream::of)chamada retorna um Optional<Stream<Other>>. Se um valor estava presente na entrada Opcional, o Opcional retornado contém um Fluxo que contém o único Outro resultado. Mas se o valor não estava presente, o resultado é um opcional vazio.

Em seguida, a chamada para orElseGet(Stream::empty)retorna um valor de tipo Stream<Other>. Se o seu valor de entrada estiver presente, ele obtém o valor, que é o elemento único Stream<Other>. Caso contrário (se o valor de entrada estiver ausente), ele retornará um vazio Stream<Other>. Portanto, o resultado está correto, o mesmo que o código condicional original.

Nos comentários discutindo minha resposta, sobre a edição rejeitada, eu descrevi essa técnica como "mais concisa, mas também mais obscura". Eu mantenho isso. Demorei um pouco para descobrir o que estava fazendo e também demorei um pouco para escrever a descrição acima do que estava fazendo. A sutileza principal é a transformação de Optional<Other>para Optional<Stream<Other>>. Uma vez que você grunhe, faz sentido, mas não foi óbvio para mim.

Reconhecerei, porém, que coisas inicialmente obscuras podem se tornar idiomáticas ao longo do tempo. Pode ser que essa técnica acabe sendo a melhor maneira na prática, pelo menos até Optional.streamser adicionada (se for o caso).

UPDATE: Optional.stream foi adicionado ao JDK 9.


16

Você não pode fazer isso de forma mais concisa, como você já está fazendo.

Você afirma que não quer .filter(Optional::isPresent) e .map(Optional::get) .

Isso foi resolvido pelo método que @StuartMarks descreve, no entanto, como resultado, você agora o mapeia para um Optional<T>, então agora você precisa usar .flatMap(this::streamopt)e a get()no final.

Portanto, ele ainda consiste em duas instruções e agora você pode obter exceções com o novo método! Porque, e se cada opcional estiver vazio? Então o findFirst()retornará um opcional vazio e o seu get()falhará!

Então, o que você tem:

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

é realmente a melhor maneira de realizar o que você deseja, e é isso que você deseja salvar o resultado como um T, não como um Optional<T>.

Tomei a liberdade de criar uma CustomOptional<T>classe que envolva Optional<T>e forneça um método extra flatStream(),. Observe que você não pode estender 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();
    }
}

Você verá que eu adicionei flatStream(), como aqui:

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();

Você ainda precisará retornar um Stream<T>aqui, pois não pode retornar T, porque se !optional.isPresent(), então, T == nullse você o declarar, mas você .flatMap(CustomOptional::flatStream)tentaria adicionar nulla um fluxo e isso não é possível.

Como exemplo:

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();

Agora lançará um NullPointerExceptiondentro das operações de fluxo.

Conclusão

O método que você usou é realmente o melhor método.


6

Uma versão um pouco mais curta usando reduce:

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

Você também pode mover a função de redução para um método de utilitário estático e, em seguida, ela se torna:

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

6
Eu gosto disso, mas vale ressaltar que isso avaliará todos os itens no Stream, enquanto o findFirst () avaliará apenas até encontrar um item atual.
Duncan McGregor

1
E, infelizmente, executar cada resolução é uma quebra de negócio. Mas é inteligente.
Yona Appletree

5

Como minha resposta anterior parecia não ser muito popular, darei outra chance.

Uma resposta curta:

Você está principalmente no caminho certo. O código mais curto para obter a saída desejada que eu poderia criar é o seguinte:

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

Isso atenderá todos os seus requisitos:

  1. Ele encontrará a primeira resposta que resolve para um não vazio Optional<Result>
  2. Chama this::resolvepreguiçosamente conforme necessário
  3. this::resolve não será chamado após o primeiro resultado não vazio
  4. Vai voltar Optional<Result>

Resposta mais longa

A única modificação comparada à versão inicial do OP foi a remoção .map(Optional::get)antes da chamada .findFirst()e adicionada .flatMap(o -> o)como a última chamada na cadeia.

Isso tem um bom efeito de se livrar do double-Optional, sempre que o fluxo encontrar um resultado real.

Você não pode realmente ficar mais curto que isso em Java.

O trecho de código alternativo usando a fortécnica de loop mais convencional será sobre o mesmo número de linhas de código e terá mais ou menos a mesma ordem e número de operações que você precisa executar:

  1. Chamando this.resolve,
  2. filtragem com base em Optional.isPresent
  3. retornando o resultado e
  4. alguma maneira de lidar com resultado negativo (quando nada foi encontrado)

Apenas para provar que minha solução funciona como anunciada, escrevi um pequeno programa de teste:

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();
        }
    }

}

(Ele possui poucas linhas extras para depuração e verificação de que somente o número de chamadas a serem resolvidas é necessário ...)

Executando isso em uma linha de comando, obtive os seguintes 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

Eu penso o mesmo que Roland Tepp. Por que alguém faria stream <stream <? >> e flat quando você pode apenas flat com um opcional <opcional <? >>
Young Hyun Yoo

3

Se você não se importa em usar uma biblioteca de terceiros, pode usar o Javaslang . É como Scala, mas implementado em Java.

Ele vem com uma biblioteca de coleções imutável completa, muito semelhante à conhecida na Scala. Essas coleções substituem as coleções do Java e o Stream do Java 8. Ele também possui sua própria implementação do Option.

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);

Aqui está uma solução para o exemplo da pergunta 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 + ")"; }
}

Disclaimer: Eu sou o criador de Javaslang.


3

Tarde para a festa, mas e quanto a

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

Você pode se livrar do último get () se criar um método util para converter opcional em stream manualmente:

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

Se você retornar o fluxo imediatamente da sua função de resolução, salve mais uma linha.


3

Gostaria de promover métodos de fábrica para criar ajudantes para APIs funcionais:

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

O 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);
}

Raciocínio:

  • Como nas referências de método em geral, comparado às expressões lambda, você não pode capturar acidentalmente uma variável do escopo acessível, como:

    t -> streamopt(resolve(o))

  • É compositável, você pode, por exemplo, chamar Function::andTheno resultado do método de fábrica:

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

    Considerando que, no caso de um lambda, você precisará lançá-lo primeiro:

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


3

Nulo é suportado pelo Stream fornecido Minha biblioteca AbacusUtil . Aqui está o código:

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

3

Se você está preso ao Java 8, mas tem acesso ao Guava 21.0 ou mais recente, pode usar Streams.streampara converter um opcional em um fluxo.

Assim, dado

import com.google.common.collect.Streams;

você pode escrever

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

0

Que tal?

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 que fazer isso quando você pode transmitir e coletar?
OneCricketeer

return list.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())), assim como a pergunta (e sua resposta vinculada) tem ...
OneCricketeer 20/02

Posso estar errado, mas considero usar isPresent () e, em seguida, get () não é uma boa prática. Então eu tento me afastar disso.
rastaman

Se você usar .get() sem isPresent() , receberá um aviso no IntelliJ
OneCricketeer

-5

Provavelmente você está fazendo errado.

O Java 8 opcional não se destina a ser usado dessa maneira. Geralmente, é reservado apenas para operações de fluxo de terminal que podem ou não retornar um valor, como localizar, por exemplo.

No seu caso, talvez seja melhor tentar primeiro encontrar uma maneira barata de filtrar os itens que podem ser resolvidos e, em seguida, obter o primeiro item como opcional e resolvê-lo como uma última operação. Melhor ainda - em vez de filtrar, localize o primeiro item resolvível e resolva-o.

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

A regra geral é que você deve se esforçar para reduzir o número de itens no fluxo antes de transformá-los em outra coisa. YMMV é claro.


6
Eu acho que o método resolve () do OP retornando <Out> opcional é um uso perfeitamente sensato do Opcional. Não posso falar com o domínio do problema do OP, é claro, mas pode ser que a maneira de determinar se algo possa ser resolvido seja tentar resolvê-lo. Nesse caso, Opcional mescla um resultado booleano de "foi resolvível" com o resultado da resolução, se bem-sucedido, em uma única chamada de API.
Stuart Marks

2
Stuart está basicamente correto. Eu tenho um conjunto de termos de pesquisa em ordem de conveniência e estou procurando encontrar o resultado do primeiro que retorne alguma coisa. Então basicamente Optional<Result> searchFor(Term t). Isso parece se encaixar na intenção de opcional. Além disso, stream () s deve ser avaliado preguiçosamente, para que não ocorra um trabalho extra de resolução de termos após o primeiro correspondente.
Yona Appletree 31/03

A questão é perfeitamente sensata e o uso do flatMap com opcional é frequentemente praticado em outras linguagens de programação semelhantes, como o Scala.
dzs 31/03
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.