Chega de NullPointerException, trabalhe com o java.util.Optional!
Já vimos aqui no blog as principais novidades do Java 8, detalhes da nova API de datas e como aplicar algumas dessas novidades em nosso dia a dia. Mas há muito o que explorar nas diversas introduções dessa nova versão da linguagem.
Quem nunca escreveu um código parecido com esse?
List<Matricula> matriculas = turma.getMatriculas();
for (Matricula matricula : matriculas) { System.out.println(matricula.getCurso().getNome()); }
Tudo parece certo, mas esse código pode ser bastante problemático. A simples ausência de algum desses valores pode resultar no tão frequente NullPointerException
. Claro, você pode contornar esse problema de diversas formas, como por exemplo inicializar suas listas ou sempre verificar que os valores são diferentes de null
antes de fazer qualquer operação com eles.
Nosso código pode ficar parecido com:
List<Matricula> matriculas = turma.getMatriculas();
for (Matricula matricula : matriculas) { if (matricula.getCurso() != null) { System.out.println(matricula.getCurso().getNome()); } }
Nada mal, mas e os outros atributos? A quantidade de if
s perdidos pelo nosso código para fazer esse tipo de verificação só tende a aumentar, isso faz com que ele fique cada vez mais ilegível e difícil de manter. E esse nem é o pior dos problemas, esquecer de fazer uma verificação como essa é um risco muito grande... em algum momento isso vai acontecer e você receberá um NullPointerException
.
Como resolver o problema? A mais nova versão do Java nos prove uma forma muito mais interessante de representar esses atributos que podem ou não estar presentes em nosso código, utilizando o java.util.Optional
.
Você pode pensar em um Optional
como uma classe que pode ou não conter um valor não nulo. Repare como fica o código da classe Matricula
declarando o atributo Curso
como Optional
:
public class Matricula {
private Optional<Curso> curso = Optional.empty();
// outros atributos, getters e setters }
Utilizamos o factory method empty
para criar e inicializar o atributo com um Optional
vazio. Há outras formas de se criar um Optional
, você pode por exemplo utilizar seu método of
:
Optional<Curso> cursoOpcional = Optional.of(algumCurso);
Mas é importante saber que caso o valor da variável algumCurso
seja null
, esse código irá lançar um NullPointerException
. Para evitar isso podemos utilizar o método ofNullable
:
Optional<Curso> cursoOpcional = Optional.ofNullable(algumCurso);
Agora que o método getCurso
retorna um Optional
, precisamos chamar seu método get
para recuperar o curso que esse Optional
está guardando:
for (Matricula matricula : matriculas) { if (matricula.getCurso() != null) { System.out.println(matricula.getCurso().get().getNome()); } }
Mas dessa forma, se o curso não existir vamos receber um java.util.NoSuchElementException: No value present. Como prevenir isso? Uma das possíveis formas seria verificando se o valor desse Optional
está presente, algo como:
for (Matricula matricula : matriculas) { Optional<Curso> cursoOpcional = matricula.getCurso(); if(cursoOpcional.isPresent()) { System.out.println(cursoOpcional.get().getNome()); } }
Mas isso ainda deixa nosso código muito verboso! Que tal fazer algo como:
for (Matricula matricula : matriculas) { matricula.getCurso() .ifPresent(c -> System.out.println(c.getNome())); }
Bem mais interessante, não acha? Esse é um dos vários métodos funcionais que essa classe possui. O método ifPresent
recebe um Consumer
que pode ser traduzido em uma expressão lambda c -> System.out.println(c.getNome())
. Mas note que você não precisa se preocupar com nenhum desses detalhes, não importa o nome da interface funcional por traz desse código, não importa o nome de seu único método, tudo que importa nesse momento é a expressão: tenho um curso e quero imprimir seu nome.
Se você está se perguntando, podemos sim fazer o for da maneira nova! Utilizando o já conhecido forEach
:
matriculas.forEach(m -> { m.getCurso() .ifPresent(c -> System.out.println(c.getNome())); });
Outro método interessante da classe Optional
é o orElseThrow
. Caso neste contexto o curso seja obrigatório, podemos fazer algo como:
matriculas.forEach(m -> { Curso curso = m.getCurso() .orElseThrow(IllegalArgumentException::new); });
Quer mais? Considere que a classe Curso
possui uma descrição (String
). É comum escrever um código parecido com esse para mostrar a descrição ou alguma outra mensagem caso ela não esteja presente:
matriculas.forEach(m -> { m.getCurso().ifPresent(c -> { if (c.getDescricao() != null){ System.out.println(c.getDescricao()); } else { System.out.println("sem descrição"); } }); });
Mas agora com Optional
podemos escrever da seguinte forma:
matriculas.forEach(m -> { m.getCurso().ifPresent(c -> { System.out.println(c.getDescricao().orElse("sem descrição")); }); });
O método orElse
nos retorna o curso caso presente, ou pelo contrário o valor passado como argumento.
Para imprimir apenas quando a descrição existir e não for vazia, poderíamos escrever um código parecido com:
matriculas.forEach(m -> { m.getCurso() .ifPresent(c -> { Optional<String> descricao = c.getDescricao(); if (descricao.isPresent() && !descricao.get().isEmpty()) { System.out.println(descricao.get()); } }); });
Resolvemos o problema, mas o código não está nem um pouco legível!
Para evitar esse encadeamento desnecessário, podemos utilizar o método map
para fazer uma projeção do curso para sua descrição. Isso mesmo, assim como o Stream
, um Optional
também possui o método map
! Vamos ver o resultado:
matriculas.forEach(m -> { Optional<Optional<String>> map = m.getCurso().map(Curso::getDescricao); });
Pois é... o map
vai nos retornar um Optional>
. Ainda não é o que estamos esperando. Se você já conhece a API de Stream
s ou programa com alguma linguagem funcional, já deve ter pensado em uma solução. Sim, o Optional
também possui o método flatMap
! Utilizando esse método podemos achatar o Optional>
para um Optional
com a descrição:
matriculas.forEach(m -> { Optional<String> flatMap = m.getCurso().flatMap(Curso::getDescricao); });
Estamos quase lá. Para imprimir a descrição apenas quando ela não for vazia, podemos utilizar o método filter
seguido pelo ifPresent
que já conhecemos:
matriculas.forEach(m -> { m.getCurso() .flatMap(Curso::getDescricao) .filter(d -> !d.isEmpty()) .ifPresent(System.out::println); });
Está pronto! Muito mais interessante que a solução anterior, não acha?
Há muito mais o que aprender sobre as novas APIs da linguagem. Você pode ver mais das novidades em nosso livro e no mais novo curso online do Alura.