Opcional ouElse Opcional em Java

137

Eu tenho trabalhado com o novo tipo opcional no Java 8 e me deparei com o que parece ser uma operação comum sem suporte funcional: um "ouElseOptional"

Considere o seguinte padrão:

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

Existem muitas formas desse padrão, mas tudo se resume a querer um "orElse" em um opcional que assume uma função que produz um novo opcional, chamado apenas se o atual não existir.

Sua implementação ficaria assim:

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

Estou curioso para saber se existe um motivo para que esse método não exista, se estou apenas usando o Opcional de maneira não intencional e de que outras maneiras as pessoas inventaram para lidar com esse caso.

Devo dizer que acho que as soluções que envolvem classes / métodos de utilitários personalizados não são elegantes porque as pessoas que trabalham com meu código não necessariamente sabem que elas existem.

Além disso, se alguém souber, esse método será incluído no JDK 9, e onde devo propor esse método? Isso parece uma omissão bastante flagrante para a API para mim.

Yona Appletree
fonte
13
Veja esta edição . Para esclarecer: isso já vai estar em Java 9 - se não em uma atualização futura do Java 8.
Obicere
Isto é! Obrigado, não encontrou isso na minha pesquisa.
precisa saber é o seguinte
2
@ Obicere Esse problema não se aplica aqui porque se trata de comportamento em opcional vazio, e não de um resultado alternativo . O opcional já possui o orElseGet()que o OP precisa, mas não gera uma sintaxe em cascata agradável.
Marko Topolnik
1
Grandes tutoriais sobre Java Opcional: codeflex.co/java-optional-no-more-nullpointerexception
John Detroit

Respostas:

86

Isso faz parte do JDK 9 na forma de or, que leva a Supplier<Optional<T>>. Seu exemplo seria então:

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

Para detalhes, consulte o Javadoc ou este post que escrevi.

Nicolai
fonte
Agradável. Essa adição deve ter um ano e eu não percebi. Com relação à pergunta em seu blog, alterar o tipo de retorno quebraria a compatibilidade binária, pois as instruções de chamada do bytecode se referem à assinatura completa, incluindo o tipo de retorno, portanto, não há chance de alterar o tipo de retorno ifPresent. Mas de qualquer maneira, acho que o nome ifPresentnão é bom de qualquer maneira. Para todos os outros métodos não tendo “senão” no nome (como map, filter, flatMap), fica implícito que eles não fazem nada se nenhum valor estiver presente, então por que ifPresent...
Holger
Portanto, adicione um Optional<T> perform(Consumer<T> c)método para permitir o encadeamento perform(x).orElseDo(y)( orElseDocomo uma alternativa à proposta ifEmpty, para ter consistência no elsenome de todo método que possa fazer algo por valores ausentes). Você pode imitar isso performno Java 9, stream().peek(x).findFirst()apesar de ser um abuso da API e ainda não há como executar um Runnablesem especificar um Consumerao mesmo tempo ...
Holger
65

A abordagem mais limpa de "experimentar serviços", dada a API atual, seria:

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

O aspecto importante não é a cadeia (constante) de operações que você precisa escrever uma vez, mas a facilidade de adicionar outro serviço (ou modificar a lista de serviços é geral). Aqui, adicionando ou removendo um único()->serviceX(args) é suficiente.

Devido à avaliação lenta dos fluxos, nenhum serviço será chamado se um serviço anterior retornar um não vazio Optional.

Holger
fonte
13
apenas usei isso em um projeto, graças a Deus não fazemos revisões de código.
Ilya Smagin
3
É muito mais limpo do que uma cadeia de "orElseGet", mas também é muito mais difícil de ler.
slartidan
4
Isso certamente está correto ... mas, honestamente, levo um segundo para analisá-lo e não saio com a confiança de que está correto. Lembre-se, percebo que esse exemplo é, mas eu poderia imaginar uma pequena alteração que não faria com que ela fosse avaliada preguiçosamente ou tivesse algum outro erro que seria indistinguível à primeira vista. Para mim, isso se enquadra na categoria de função que seria decente como uma utilidade.
Yona Appletree
3
Gostaria de saber se alternar .map(Optional::get)com .findFirst()tornará mais fácil a "leitura", por exemplo, .filter(Optional::isPresent).findFirst().map(Optional::get)pode ser "lido" como "encontrar o primeiro elemento no fluxo para o qual Optional :: isPresent é verdadeiro e achatá-lo aplicando Optional :: get"?
schatten
3
Engraçado, publiquei uma solução muito semelhante para uma pergunta semelhante há alguns meses atrás. Esta é a primeira vez que me deparei com isso.
Shmosel 25/05
34

Não é bonito, mas isso 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)é um padrão bastante útil para uso com Optional. Significa "Se isso Optionalcontém valor v, me dê func(v), caso contrário, me dêsup.get() ".

Nesse caso, chamamos serviceA(args)e obtemos um Optional<Result>. Se isso Optionalcontém valor v, queremos obter Optional.of(v), mas se estiver vazio, queremos obterserviceB(args) . Repita a lavagem com mais alternativas.

Outros usos desse padrão são

  • .map(Stream::of).orElseGet(Stream::empty)
  • .map(Collections::singleton).orElseGet(Collections::emptySet)
Misha
fonte
1
huh, quando uso essa estratégia, o eclipse diz: "O método orElseGet (Supplier <? extends String>) do tipo Optional <String> não é aplicável aos argumentos (() -> {})" Não parece considerar devolver uma estratégia opcional válida, na String ??
chrismarx
@chrismarx () -> {}não retorna um Optional. O que você está tentando realizar?
Misha
1
Eu só estou tentando seguir o exemplo. Encadear as chamadas do mapa não está funcionando. Os meus serviços retornar cordas, e, claro, não há nenhuma .map opção () disponível, então
chrismarx
1
Excelente alternativa legível para o próximo or(Supplier<Optional<T>>)de Java 9
David M.
1
@ Shehepy você está enganado. .map()em um vazio Optionalproduzirá um vazio Optional.
Misha
27

Talvez seja isso que você procura: obtenha valor de um opcional ou de outro

Caso contrário, você pode querer dar uma olhada Optional.orElseGet. Aqui está um exemplo do que eu acho que você procura:

result = Optional.ofNullable(serviceA().orElseGet(
                                 () -> serviceB().orElseGet(
                                     () -> serviceC().orElse(null))));
aioobe
fonte
2
É isso que os gênios costumam fazer. Avalie o opcional como anulável e envolvê-lo ofNullableé a coisa mais legal que eu já vi.
Jin Kwon
5

Supondo que você ainda esteja no JDK8, existem várias opções.

Opção 1: crie seu próprio método auxiliar

Por exemplo:

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 você possa fazer:

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

Opção 2: use uma biblioteca

Por exemplo, o opcional do google guava suporta uma or()operação adequada (como o JDK9), por exemplo:

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

(Onde cada um dos serviços retorna com.google.common.base.Optional, em vez de java.util.Optional).

Sheepy
fonte
Não encontrei Optional<T>.or(Supplier<Optional<T>>)nos documentos da Goiaba. você tem um link para isto?
Tamas Hegedus
.orElseGet retorna T, mas Opcional :: vazio retorna Opcional <T>. Optional.ofNullable (........ ouElse (null)) tem o efeito desejado, conforme descrito por @aioobe.
Miguel Pereira
@TamasHegedus Você quer dizer na opção 1? É uma implementação personalizada que está no snippit acima dela. ps: desculpe pela resposta tardia
Sheepy
@MiguelPereira .orElseGet, nesse caso, retorna Opcional <T> porque está operando em Opcional <Opcional <T>>
Sheepy
2

Este é parece um bom ajuste para correspondência de padrão e uma interface opção mais tradicional com alguns e nenhum implementações (tais como aqueles em Javaslang , FunctionalJava ) ou um preguiçoso Talvez implementação em cyclops-reagir .Eu estou o autor desta biblioteca.

Com o cyclops-react, você também pode usar a correspondência de padrões estruturais nos tipos JDK. Para Opcional, você pode corresponder nos casos presentes e ausentes por meio do padrão de visitante . seria algo parecido com isto -

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

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