Chega de NullPointerException, trabalhe com o java.util.Optional!

Chega de NullPointerException, trabalhe com o java.util.Optional!
rodrigo.turini
rodrigo.turini

Compartilhe

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 ifs 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 Streams 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.

Veja outros artigos sobre Programação