Por que usar o Opcional no Java 8+ em vez das verificações de ponteiro nulo tradicionais?

93

Recentemente, mudamos para o Java 8. Agora, vejo aplicativos inundados com objetos Optional .

Antes do Java 8 (estilo 1)

Employee employee = employeeServive.getEmployee();

if(employee!=null){
    System.out.println(employee.getId());
}

Após o Java 8 (estilo 2)

Optional<Employee> employeeOptional = Optional.ofNullable(employeeService.getEmployee());
if(employeeOptional.isPresent()){
    Employee employee = employeeOptional.get();
    System.out.println(employee.getId());
}

Não vejo valor agregado de Optional<Employee> employeeOptional = employeeService.getEmployee(); quando o próprio serviço retorna opcional:

Vindo de um plano de fundo do Java 6, vejo o Estilo 1 como mais claro e com menos linhas de código. Existe alguma vantagem real que sinto falta aqui?

Consolidado a partir do entendimento de todas as respostas e pesquisas adicionais em blog

    
por user3198603 18.01.2018 / 15:34
fonte

10 respostas

98

O estilo 2 não está indo para o Java 8 o suficiente para ver o benefício total. Você não quer o if ... use em tudo. Consulte os exemplos da Oracle . Seguindo seus conselhos, ficamos:

Estilo 3

// Changed EmployeeServive to return an optional, no more nulls!
Optional<Employee> employee = employeeServive.getEmployee();
employee.ifPresent(e -> System.out.println(e.getId()));

Ou um trecho mais longo

Optional<Employee> employee = employeeServive.getEmployee();
// Sometimes an Employee has forgotten to write an up-to-date timesheet
Optional<Timesheet> timesheet = employee.flatMap(Employee::askForCurrentTimesheet); 
// We don't want to do the heavyweight action of creating a new estimate if it will just be discarded
client.bill(timesheet.orElseGet(EstimatedTimesheet::new));
    
por 18.01.2018 / 16:14
fonte
43

Se você estiver usando Optional como uma camada de "compatibilidade" entre uma API mais antiga que ainda pode retornar null , pode ser útil criar a Opcional (não vazia) na última etapa em que você está certeza que você tem alguma coisa. Por exemplo, onde você escreveu:

Optional<Employee> employeeOptional = Optional.ofNullable(employeeService.getEmployee());
if(employeeOptional.isPresent()){
    Employee employeeOptional= employeeOptional.get();
    System.out.println(employee.getId());
}

Eu optaria por:

Optional.of(employeeService)                 // definitely have the service
        .map(EmployeeService::getEmployee)   // getEmployee() might return null
        .map(Employee::getId)                // get ID from employee if there is one
        .ifPresent(System.out::println);     // and if there is an ID, print it

O ponto é que você sabe que existe um serviço de funcionário não nulo, para que você possa agrupá-lo em Optional with Optional.of() . Então, quando você liga para getEmployee() , pode ou não conseguir um funcionário. Esse funcionário pode (ou possivelmente não) ter um ID. Então, se você acabou com um ID, você quer imprimi-lo.

Não é necessário verificar explicitamente qualquer null, presença, etc., neste código.

    
por 18.01.2018 / 20:37
fonte
20

Não há nenhum valor agregado em ter um Optional de um único valor. Como você viu, isso apenas substitui uma verificação de null por uma verificação de presença.

Existe enorme valor agregado em ter um Collection dessas coisas. Todos os métodos de streaming introduzidos para substituir loops de baixo nível de estilo antigo entendem Optional coisas e fazem a coisa certa sobre eles (não os processando ou, finalmente, retornando outro unset Optional ). Se você lida com itens n com mais frequência do que com itens individuais (e 99% de todos os programas realistas), é uma grande melhoria ter suporte embutido para itens presentes opcionalmente. No caso ideal, você nunca precisa verificar a presença de null ou .

    
por 18.01.2018 / 15:47
fonte
14

Contanto que você use Optional como uma API sofisticada para isNotNull() , então sim, você não encontrará nenhuma diferença com apenas a verificação de null .

Coisas que você NÃO deve usar Optional para

Usar Optional apenas para verificar a presença de um valor é Bad Code ™:

// BAD CODE ™ --  just check getEmployee() != null  
Optional<Employee> employeeOptional =  Optional.ofNullable(employeeService.getEmployee());  
if(employeeOptional.isPresent()) {  
    Employee employee = employeeOptional.get();  
    System.out.println(employee.getId());
}  

Coisas que você deve usar Optional para

Evite que um valor não esteja presente, produzindo um quando necessário ao usar os métodos da API de retorno nulo:

Employee employee = Optional.ofNullable(employeeService.getEmployee())
                            .orElseGet(Employee::new);
System.out.println(employee.getId());

Ou, se criar um novo funcionário for muito caro:

Optional<Employee> employee = Optional.ofNullable(employeeService.getEmployee());
System.out.println(employee.map(Employee::getId).orElse("No employee found"));

Além disso, conscientizar a todos de que seus métodos podem não retornar um valor (se, por qualquer motivo, você não puder retornar um valor padrão como acima):

// Your code without Optional
public Employee getEmployee() {
    return someCondition ? null : someEmployee;
}
// Someone else's code
Employee employee = getEmployee(); // compiler doesn't complain
// employee.get...() -> NPE awaiting to happen, devs criticizing your code

// Your code with Optional
public Optional<Employee> getEmployee() {
    return someCondition ? Optional.empty() : Optional.of(someEmployee);
}
// Someone else's code
Employee employee = getEmployee(); // compiler complains about incompatible types
// Now devs either declare Optional<Employee>, or just do employee = getEmployee().get(), but do so consciously -- not your fault.

E, finalmente, há todos os métodos semelhantes a fluxos que já foram explicados em outras respostas, embora eles não estejam realmente decidindo usar Optional , mas fazendo uso do Optional fornecido por outra pessoa.

    
por 19.01.2018 / 09:43
fonte
12

O ponto-chave para mim no uso de Optional é manter-se claro quando o desenvolvedor precisa verificar se o valor de retorno da função ou não.

//service 1
Optional<Employee> employeeOptional = employeeService.getEmployee();
if(employeeOptional.isPresent()){
    Employee employeeOptional= employeeOptional.get();
    System.out.println(employee.getId());
}

//service 2
Employee employee = employeeService.getEmployeeXTPO();
System.out.println(employee.getId());
    
por 18.01.2018 / 19:26
fonte
8

Seu erro principal é que você ainda está pensando em termos mais processuais. Isso não é uma crítica a você como pessoa, é apenas uma observação. Pensar em termos mais funcionais vem com o tempo e a prática, e, portanto, os métodos são apresentados e se parecem com as coisas corretas mais óbvias para você. Seu menor erro secundário é criar o seu Opcional dentro do seu método. O opcional destina-se a ajudar a documentar que algo pode ou não retornar um valor. Você pode não conseguir nada.

Isso levou você a escrever um código perfeitamente legível que parece perfeitamente razoável, mas você foi seduzido pelas vil gêmeas sedutoras que são atraídas e isPresent.

É claro que a questão rapidamente se torna "por que o isPresent e você chega lá?"

Uma coisa que muitas pessoas aqui sentem falta é que o Present () é que não é algo que é feito para um novo código escrito por pessoas com o quão desajeitadamente são os lambdas úteis e que gostam do funcional.

No entanto, ele nos dá alguns (dois) benefícios bons, ótimos, glamourosos (?):

  • Facilita a transição do código legado para usar os novos recursos.
  • Facilita as curvas de aprendizado do Opcional.

O primeiro é bastante simples.

Imagine que você tem uma API assim:

public interface SnickersCounter {
  /** 
   * Provides a proper count of how many snickers have been consumed in total.
   */
  public SnickersCount howManySnickersHaveBeenEaten();

  /**
    * returns the last snickers eaten.<br>
    * If no snickers have been eaten null is returned for contrived reasons.
    */
  public Snickers lastConsumedSnickers();
}

E você tinha uma classe legada usando isso (preencha os espaços em branco):

Snickers lastSnickers = snickersCounter.lastConsumedSnickers();
if(null == lastSnickers) {
  throw new NoSuchSnickersException();
}
else {
  consumer.giveDiabetes(lastSnickers);
}

Um exemplo inventado para ter certeza. Mas tenha paciência comigo aqui.

O Java 8 já foi lançado e estamos nos esforçando para embarcar. Então, uma das coisas que fazemos é substituir a antiga interface por algo que retorne Opcional. Por quê? Porque, como alguém já mencionou graciosamente: Isto elimina a adivinhação se algo pode ou não ser nulo Isso já foi apontado por outros. Mas agora nós temos um problema. Imagine que temos (desculpe-me enquanto eu acertei alt + F7 em um método inocente), 46 lugares onde esse método é chamado em código legado bem testado que faz um excelente trabalho de outra forma. Agora você precisa atualizar tudo isso.

ESTE é onde isPresent brilha.

Porque agora:     Snickers lastSnickers = snickersCounter.lastConsumedSnickers ();     if (null == lastSnickers) {       lançar novo NoSuchSnickersException ();     }     outro {       consumer.giveDiabetes (lastSnickers);     }

torna-se:

Optional<Snickers> lastSnickers = snickersCounter.lastConsumedSnickers();
if(!lastSnickers.isPresent()) {
  throw new NoSuchSnickersException();
}
else {
  consumer.giveDiabetes(lastSnickers.get());
}

E essa é uma mudança simples que você pode dar ao novo júnior: ele pode fazer algo útil e explorar a base de código ao mesmo tempo. ganha-ganha. Afinal, algo semelhante a esse padrão é bastante difundido. E agora você não precisa reescrever o código para usar lambdas ou qualquer outra coisa. (Neste caso em particular, seria trivial, mas deixo exemplos de pensamento onde seria difícil como um exercício para o leitor.)

Observe que isso significa que a maneira como você fez isso é essencialmente uma forma de lidar com o código legado sem fazer reescritas dispendiosas. Então, e o novo código?

Bem, no seu caso, onde você quer imprimir algo, você simplesmente faria:

snickersCounter.lastConsumedSnickers (). ifPresent (System.out :: println);

O que é bem simples e perfeitamente claro. O ponto que está lentamente subindo até a superfície, é que existem casos de uso para get () e isPresent (). Eles estão lá para permitir que você modifique o código existente mecanicamente para usar os tipos mais recentes sem pensar muito sobre isso. O que você está fazendo é, portanto, mal orientado das seguintes maneiras:

  • Você está chamando um método que pode retornar null. A ideia correta seria que o método retorna null.
  • Você está usando os métodos legados bandaid para lidar com isso como opcional, em vez de usar os novos métodos saborosos que contêm o conteúdo lambda.

Se você quiser usar o Opcional como uma simples verificação de segurança nula, o que você deveria ter feito é simplesmente isto:

new Optional.ofNullable(employeeServive.getEmployee())
    .map(Employee::getId)
    .ifPresent(System.out::println);

É claro que a boa aparência disso parece:

employeeService.getEmployee()
    .map(Employee::getId)
    .ifPresent(System.out::println);

A propósito, apesar de não ser obrigatório, recomendo usar uma nova linha por operação, para facilitar a leitura. Fácil de ler e entender, conciliação em qualquer dia da semana.

Este é, obviamente, um exemplo muito simples, onde é fácil entender tudo o que estamos tentando fazer. Nem sempre é tão simples na vida real. Mas observe como neste exemplo, o que estamos expressando são nossas intenções. Queremos obter o funcionário, obter seu ID e, se possível, imprimi-lo. Esta é a segunda grande vitória com o Opcional. Isso nos permite criar um código mais claro. Eu também acho que fazer coisas como criar um método que faça um monte de coisas para que você possa alimentá-lo em um mapa é, em geral, uma boa ideia.

    
por 19.01.2018 / 14:49
fonte
0

Não vejo benefício no Estilo 2. Você ainda precisa perceber que precisa da verificação nula e agora é ainda maior, portanto, menos legível.

Melhor estilo seria, se employeeServive.getEmployee () retornasse Opcional e então o código se tornaria:

Optional<Employee> employeeOptional = employeeServive.getEmployee();
if(employeeOptional.isPresent()){
    Employee employee= employeeOptional.get();
    System.out.println(employee.getId());
}

Desta forma, você não pode chamar o deployeeServive.getEmployee (). getId () e você percebe que deve manipular valores opcionais de alguma forma. E se em toda a sua base de código os métodos nunca retornarem nulo (ou quase nunca com exceções bem conhecidas da sua equipe a essa regra), isso aumentará a segurança contra o NPE.

    
por 18.01.2018 / 15:46
fonte
0

Acho que o maior benefício é que, se a assinatura do método retornar um Employee você não verificará o valor nulo. Você sabe, por essa assinatura, que é garantido que devolve um empregado. Você não precisa lidar com um caso de falha. Com um opcional, você sabe que faz.

No código que vi, existem verificações de nulos que nunca falharão, pois as pessoas não querem rastrear o código para determinar se um nulo é possível, portanto, elas lançam verificações de nulos em todos os lugares defensivamente. Isso torna o código um pouco mais lento, mas o mais importante é tornar o código muito mais ruidoso.

No entanto, para que isso funcione, você precisa aplicar esse padrão de maneira consistente.

    
por 18.01.2018 / 22:50
fonte
0

Minha resposta é: Só não. Pelo menos pense duas vezes se realmente é uma melhoria.

Uma expressão (extraída de outra resposta) como

Optional.of(employeeService)                 // definitely have the service
        .map(EmployeeService::getEmployee)   // getEmployee() might return null
        .map(Employee::getId)                // get ID from employee if there is one
        .ifPresent(System.out::println);     // and if there is an ID, print it

parece ser a maneira funcional correta de lidar com métodos de retorno originalmente nulos. Parece legal, nunca é lançado e nunca faz você pensar em lidar com o caso "ausente".

Parece melhor que a maneira antiga

Employee employee = employeeService.getEmployee();
if (employee != null) {
    ID id = employee.getId();
    if (id != null) {
        System.out.println(id);
    }
}

que torna óbvio que pode haver cláusulas else .

O estilo funcional

  • parece completamente diferente (e é ilegível para pessoas não usadas)
  • é difícil de depurar
  • não pode ser estendido facilmente para lidar com o caso "ausente" ( orElse nem sempre é suficiente)
  • engana-se ao ignorar o caso "ausente"

Em nenhum caso, deve ser usado como uma panacéia. Se fosse universalmente aplicável, a semântica do operador . deveria ser substituída pela semântica do ?. operator , o NPE foi esquecido e todos os problemas foram ignorados.

Apesar de ser um grande fã de Java, devo dizer que o estilo funcional é apenas um substituto pobre do homem de Java da afirmação

employeeService
.getEmployee()
?.getId()
?.apply(id => System.out.println(id));

bem conhecido de outras línguas. Concordamos que esta afirmação

  • não é (realmente) funcional?
  • é muito semelhante ao código antigo, muito mais simples?
  • é muito mais legível do que o estilo funcional?
  • é compatível com depurador?
por 24.01.2018 / 00:17
fonte
0

Opcional é um conceito (um tipo de ordem superior) proveniente da programação funcional. Usá-lo elimina a verificação nula aninhada e salva você da pirâmide da desgraça.

    
por 24.01.2018 / 10:33
fonte

Tags