Optional no Java
Se você já programou em Java, é quase certo que encontrou o famoso NullPointerException
, ou como alguns preferem chamar, “o terror das noites de deploy”. É um erro tão comum que virou meme, e a mensagem “Cannot invoke method on a null object” já fez muita gente questionar suas escolhas de carreira. 😂
Por décadas, o null foi a resposta padrão do Java para “não tenho um valor aqui”. Parece simples, mas, na prática, tornou-se uma fonte inesgotável de bugs. Quem nunca escreveu um if (obj != null)
para evitar desastres? E pior: quantos de nós já esqueceram essa verificação, só para encontrar o erro em produção? Pois é, acontece com os melhores.
A verdade é que o uso desenfreado de null é, na maior parte das vezes, um desastre anunciado. Ele não diz nada sobre a intenção do código. Por exemplo, quando um método retorna null, o que isso significa? O valor não foi encontrado? Houve um erro? Ou simplesmente esqueceram de implementar a lógica? Null é ambíguo e perigoso.
Vamos falar muito sobre o Optional e como utilizar seus métodos e operadores. Você pode conferir a documentação completa aqui na página oficial da Oracle.
A Origem
O Optional
foi projetado como parte das várias melhorias introduzidas no Java 8 pelo time liderado por Brian Goetz, arquiteto-chefe da linguagem Java na Oracle. Ele é uma das figuras no design de APIs modernas no Java.
Goetz e a equipe viram a necessidade de um modelo mais seguro e expressivo para lidar com a ausência de valores, algo que já era comum em linguagens mais recentes, e adaptaram a ideia para o ecossistema Java.
Em várias oportunidades, Brian Goetz destacou que o Optional não foi projetado para ser usado em todos os lugares. O objetivo era preencher lacunas em cenários onde a ausência de valores precisava ser comunicada explicitamente, de forma clara e consistente.
Um Antídoto para o NullPointerException?
Inspirado em ideias de outras linguagens como Scala e Haskell, o Optional
é como um contêiner. Ele pode guardar um valor ou não guardar nada. Mas, diferentemente de null, ele nos força a lidar explicitamente com a ausência de valor.
Com o Optional
, o código deixa de ser null e passa a ser mais claro e seguro. Veja a diferença. Sem Optional:
String resultado = meuMetodo();
if (resultado != null) {
System.out.println(resultado);
}
Com Optional:
Optional<String> resultado = meuMetodo();
resultado.ifPresent(System.out::println);
O segredo está em como o Optional
muda a maneira de pensar: ele nos obriga a considerar, desde o início, que o valor pode ou não estar presente. Não tem mais desculpa para esquecer de verificar! Mas essa é apenas a superfície do assunto, vamos mergulhar mais?
Um Contêiner Confiável
Pense no Optional
como uma caixa. Às vezes, a caixa tem algo dentro; às vezes, não. O ponto principal é que você não pode ignorar a possibilidade de a caixa estar vazia.
Vamos dar mais vida a essa ideia. Imagine que você está lidando com uma caixa misteriosa: às vezes, ela contém algo dentro (um gato); outras vezes, está vazia.
Mas o mais importante é que, independentemente de estar cheia ou vazia, você sempre sabe que está lidando com uma caixa confiável – sem surpresas ou pegadinhas desagradáveis. Ela não está furada, rasgada ou com buracos por onde o valor poderia escapar. Essa confiabilidade é o que torna o Optional
tão poderoso: ele garante que você sempre terá algo consistente para trabalhar, mesmo quando o conteúdo inicial for null.
O que é o ofNullable()
?
Agora, é aqui que entra o ofNullable(). Pense nele como o empacotador oficial dessa caixa. Ele pega qualquer coisa que você tenha – seja um valor válido ou até mesmo o vazio absoluto (null) – e a transforma em uma caixa legítima.
Escolhi comparar ofNullable()
a um empacotador porque ele pega qualquer coisa – seja um valor válido ou null – e a coloca dentro de uma caixa legítima (Optional
). Mas ele vai além de apenas empacotar: também verifica o estado da caixa. Se o valor for válido, ele cria uma “caixa cheia”; se for null, ele garante que você receba uma “caixa vazia”. Isso torna o ofNullable()
muito mais do que um simples empacotador – ele é um empacotador inteligente, que organiza e rotula o conteúdo, garantindo sempre uma representação segura e consistente do valor.
É como se o ofNullable()
comunicasse ao compilador e a quem lê o código o seguinte:
“Eu tenho algo aqui, mas pode ser que não seja nada. Então, por favor, me entregue uma caixa que represente isso. Se houver algo, quero que esteja lá dentro. Se não houver, me dê uma caixa vazia.”
Essa é a grande sacada desse método: ele garante que você sempre terá uma caixa (um Optional
), mesmo que o valor inicial seja null. Isso elimina a necessidade de verificações manuais e torna seu código muito mais seguro e expressivo.
Quando o valor é null, a caixa não é exatamente inexistente – ela está lá, mas vazia, graças ao Optional.empty()
. Isso significa que nunca ficaremos sem uma caixa, o que é crucial para evitar erros inesperados no código.
import java.util.Optional;
public class OptionalExample {
public static void main(String[] args) {
// Um valor válido (não nulo)
String valorNaoNulo = "Eu sou um valor válido!";
Optional<String> caixaComValor = Optional.ofNullable(valorNaoNulo);
System.out.println("Caixa com valor: " + caixaComValor.orElse("Caixa vazia"));
// Um valor nulo
String valorNulo = null;
Optional<String> caixaVazia = Optional.ofNullable(valorNulo);
System.out.println("Caixa vazia: " + caixaVazia.orElse("Caixa vazia"));
// Verificando se o valor está presente na caixa
if (caixaComValor.isPresent()) {
System.out.println("O valor dentro da caixa é: " + caixaComValor.get());
} else {
System.out.println("A caixa está vazia.");
}
if (caixaVazia.isPresent()) {
System.out.println("O valor dentro da caixa é: " + caixaVazia.get());
} else {
System.out.println("A caixa está vazia.");
}
// Demonstrando o uso de Optional.empty()
Optional<String> caixaVaziaUsandoEmpty = Optional.empty();
System.out.println("Caixa criada como vazia: " + caixaVaziaUsandoEmpty.orElse("Caixa vazia"));
}
}
Você pode executar esse código em um compilador Java Online e ver o resultado no Output do console!
Como funciona ofNullable()
?
Então apenas resumindo agora sem analogias, o método Optional.ofNullable()
cria um Optional
a partir de um valor que pode ou não ser null. Se o valor for diferente de null, ele cria um Optional
que contém esse valor. Caso contrário, ele retorna um Optional.empty()
, que é uma caixa vazia.
Exemplo simples:
String valor = null;
Optional<String> optional = Optional.ofNullable(valor);
System.out.println(optional.isPresent()); // print: false
Se você trocasse o valor para "Java é incrível", o Optional
conteria o valor e você poderia manipulá-lo com segurança:
String valor = "Java is awesome";
Optional<String> optional = Optional.ofNullable(valor);
optional.ifPresent(System.out::println);
Como o ofNullable()
se compara a outros métodos do Optional
?
O ofNullable()
não é o único jeito de criar um Optional
, mas ele é o mais versátil. Vamos compará-lo com outros métodos de fábrica do Optional
para entender as diferenças:
1. Optional.of()
Este método assume que você está absolutamente certo de que o valor não é null. Se você passar um valor null, ele vai jogar um NullPointerException
na sua cara sem dó 😂. Veja só a prova!
String valor = null;
Optional<String> optional = Optional.of(valor); // NullPointerException
Então, o of()
deve ser usado apenas quando você tem certeza absoluta de que o valor nunca será null. Caso contrário, você estará adicionando mais risco ao comportamento do código.
2. Optional.empty()
Esse método é super simples: ele cria uma caixa vazia, ou seja, um Optional
que não contém nenhum valor. Não importa o que aconteça, o resultado será um Optional.empty()
. Olha como funciona:
Optional<String> optional = Optional.empty();
System.out.println(optional.isPresent()); // Print: false
Porém, o empty()
é usado apenas quando você já sabe de antemão que não há valor algum a ser representado. Ele não lida com um valor que pode ser null, diferentemente do ofNullable()
.
Por que usar esse método?
O ofNullable()
é o equilíbrio entre segurança e flexibilidade. Ele brilha especialmente nos casos em que:
• Você está lidando com dados que vêm de fontes incertas, como APIs externas, bancos de dados ou métodos legados.
• Não tem certeza se o valor será null ou não, mas quer evitar manipular isso manualmente.
Mas tem mais coisa para explorar sobre esse assunto! 👇🏼
Métodos Relacionados: Explorando o Poder do Optional
Agora que já sabemos o que o ofNullable()
faz, é hora de colocar a mão na massa e ver como ele pode ser usado com outros métodos do Optional
. Vamos trazer exemplos mais próximos do dia a dia, como trabalhar com listas, objetos e métodos que retornam valores opcionais. Sem mais bora lá!
1. Verificando se o valor está presente: .isPresent()
O método .isPresent(
) é a forma mais simples de verificar se a caixa (o Optional) contém algo ou está vazia. Vamos imaginar um cenário:
Você tem uma lista de nomes e quer encontrar um nome específico. O método de busca pode retornar null se o nome não for encontrado, mas vamos envolver isso no Optional
para lidar com a ausência de valor:
List<String> nomes = List.of("Ana", "Bruno", "Carlos");
Optional<String> resultado = nomes.stream()
.filter(nome -> nome.equals("Bruno"))
.findFirst();
if (resultado.isPresent()) {
System.out.println("Nome encontrado: " + resultado.get());
} else {
System.out.println("Nome não encontrado!");
}
O ofNullable()
entraria em cena aqui caso o método de busca retornasse diretamente um valor que pode ser null. Por exemplo:
String nomeBuscado = buscarNomeNoBanco(); // Pode retornar null
Optional<String> nomeOptional = Optional.ofNullable(nomeBuscado);
if (nomeOptional.isPresent()) {
System.out.println("Nome encontrado: " + nomeOptional.get());
} else {
System.out.println("Nome não encontrado!");
}
Juntando isso você pode executar e visualizar até fazer um debbug:
import java.util.List;
import java.util.Optional;
public class Main {
public static void main(String[] args) {
// Simulating a list of names
List<String> names = List.of("Anna", "Bruno", "Carlos");
// Searching for a name in the list
Optional<String> result = names.stream()
.filter(name -> name.equals("Bruno"))
.findFirst();
if (result.isPresent()) {
System.out.println("Name found in the list: " + result.get());
} else {
System.out.println("Name not found in the list!");
}
// Simulating a database search
String searchedName = searchNameInDatabase(); // May return null
Optional<String> nameOptional = Optional.ofNullable(searchedName);
if (nameOptional.isPresent()) {
System.out.println("Name found in the database: " + nameOptional.get());
} else {
System.out.println("Name not found in the database!");
}
}
// Method simulating a database search
public static String searchNameInDatabase() {
// Return a value or null to simulate different scenarios
return null; // Simulating an absent value
}
}
Bora para o próximo método? 👇🏼
2. Executando algo quando o valor está presente: .ifPresent()
Diferente do .isPresent()
, que apenas verifica se há algo dentro do Optional, o .ifPresent()
vai além: ele executa uma ação diretamente, mas somente se o valor estiver presente. Ou seja, ele combina a verificação e a execução em um único passo, tornando seu código mais conciso e fácil de entender.
Por exemplo, envie um e-mail para um cliente apenas se o endereço de e-mail estiver preenchido:
String customerEmail = fetchCustomerEmail(); // May return null
// Using isPresent()
Optional<String> emailOptional = Optional.ofNullable(customerEmail);
if (emailOptional.isPresent()) {
System.out.println("Sending email to: " + emailOptional.get());
}
// Using ifPresent()
Optional.ofNullable(customerEmail)
.ifPresent(email -> System.out.println("Sending email to: " + email));
Note como o .ifPresent()
elimina a necessidade de chamar manualmente .get()
e encapsula o que você quer fazer em uma única linha. É uma abordagem mais limpa e funcional.
O .isPresent()
tem seu lugar, recomendo em algumas situações, especialmente quando você precisa apenas verificar a presença do valor sem agir sobre ele. Mas, sempre que for necessário executar algo, o .ifPresent()
é a escolha ideal no meu ponto de vista. Afinal, quanto mais conciso e direto for o código, melhor!
3. Definindo um valor padrão: .orElse()
Imagine que você tem uma lista de produtos em um carrinho de compras e quer exibir o nome do produto. Se o nome não estiver disponível, um valor padrão como “Produto sem nome” deve ser exibido. Aqui entra o .orElse()
:
String nomeProduto = null;
String resultado = Optional.ofNullable(nomeProduto)
.orElse("Produto sem nome");
System.out.println(resultado); // Imprime: Produto sem nome
Um ponto importante ao usar o .orElse()
é que o valor passado como argumento será sempre avaliado, mesmo que o Optional
contenha um valor. Isso pode causar impacto em termos de desempenho ou até efeitos colaterais inesperados, dependendo do que está no argumento.
String nomeProduto = "Notebook";
String resultado = Optional.ofNullable(nomeProduto)
.orElse(pegarNomePadrao());
System.out.println(resultado);
// Método de exemplo
private static String pegarNomePadrao() {
System.out.println("Executando lógica desnecessária...");
return "Produto sem nome";
}
Mesmo que o nomeProduto seja "Notebook", o método pegarNomePadrao()
será executado. Isso é algo a se evitar, principalmente se o valor padrão envolve operações custosas, como buscar dados no banco de dados ou fazer chamadas a serviços externos. A solução? Se o valor padrão for “pesado” para calcular, prefira usar .orElseGet()
. Ele recebe um Supplier
, ou seja, só executa a lógica quando necessário:
String resultado = Optional.ofNullable(nomeProduto)
.orElseGet(() -> pegarNomePadrao());
Isso garante que o valor padrão será gerado apenas se o Optional
estiver vazio. Assim, você evita desperdício de recursos e possíveis efeitos colaterais indesejados.
Então, quando usar .orElse()
e quando usar .orElseGet()
?
Aqui a recomendação não é só minha mas da comunidade em geral.
Use .orElse()
: Quando o valor padrão é simples, estático ou barato de fornecer. Por exemplo, valores literais ou constantes.
Use .orElseGet()
: Quando o valor padrão é custoso de calcular ou envolve lógica mais complexa, como chamadas de métodos, consultas ao banco ou cálculos intensivos.
O .orElse()
não é “ruim” — ele é perfeito para casos simples. Mas é importante entender que ele sempre avalia o argumento fornecido, mesmo que o valor do Optional
já exista.
4. Lançando uma exceção se o valor não estiver presente: .orElseThrow()
Agora, digamos que você está lidando com algo que precisa obrigatoriamente estar presente, como o ID de um pedido. Se o valor não estiver presente, é melhor lançar uma exceção. Para isso, usamos o .orElseThrow()
:
String idPedido = buscarIdPedido(); // Pode retornar null
String id = Optional.ofNullable(idPedido)
.orElseThrow(() -> new IllegalArgumentException("Pedido não encontrado"));
System.out.println("ID do pedido: " + id);
Com isso, você lida com a ausência do valor de forma explícita, sem deixar exceções aparecerem de surpresa. Mas tem dois pontos de alerta 🚨, que gostaria de comentar com vocês.👇🏼
Evite lógica custosa no lambda
É importante garantir que o código dentro do lambda seja simples e eficiente, pois ele será executado durante a exceção. Então por favor! Não faça isso:
String id = Optional.ofNullable(idPedido)
.orElseThrow(() -> {
// Lógica pesada no lambda
registrarErroNoBanco("ID ausente");
return new IllegalArgumentException("Pedido não encontrado");
});
Em vez disso, registre o erro separadamente antes de chamar o método, ou use um lambda direto e simples:
String id = Optional.ofNullable(idPedido)
.orElseThrow(() -> new IllegalArgumentException("Pedido não encontrado"));
Escolha a exceção apropriada
Sempre use uma exceção que faça sentido no contexto do problema. No exemplo fornecido:
String id = Optional.ofNullable(idPedido)
.orElseThrow(() -> new IllegalArgumentException("Pedido não encontrado"));
Aqui, IllegalArgumentException
é uma boa escolha porque estamos indicando que a ausência do ID é um problema com o argumento fornecido (ou sua ausência). Porém, em outros casos, uma exceção mais específica pode ser melhor:
• NoSuchElementException
: Quando você está lidando com um valor que deveria existir, mas não foi encontrado.
• CustomException
: Criar uma exceção personalizada, como PedidoNaoEncontradoException
, pode tornar o código mais claro e descritivo.
Acho que essas são os principais pontos de atenção. Mas se conhece outros pontos deixe nos comentários!
Transformando o valor com Map
Aqui é onde o Optional
começa a brilhar ainda mais! Você pode transformar o valor contido nele usando o .map()
. Vamos supor que você tem um objeto Cliente, e quer exibir o nome em letras maiúsculas, mas só se o cliente existir:
import java.util.Optional;
class Customer {
private String name;
public Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Main {
public static void main(String[] args) {
Customer customer = new Customer("Carlos");
Optional.ofNullable(customer) // Checks if the customer is not null
.map(Customer::getName) // Transforms the customer into their name
.map(String::toUpperCase) // Converts the name to uppercase
.ifPresent(System.out::println); // If present, prints the name
}
}
O .map()
é MUITO útil nessas situações porque permite encadear operações de forma limpa, sem verificações explícitas de null.
Pontos de Atenção no Uso do .map()
Certifique-se de que o valor no Optional
não seja null
após a transformação. O map
é usado para transformar o valor encapsulado, mas o resultado da transformação pode acabar sendo null se você não tomar cuidado. Como boa prática, sempre assegure que a lógica dentro do map
não produza valores nulos.
Optional<Cliente> clienteOptional = Optional.ofNullable(cliente);
Optional<String> name = clienteOptional.map(cliente -> cliente.getName())
.map(name -> name.toUpperCase()); // What if getName() returns null?
Se getName()
retornar null, o encadeamento resultará em um Optional.empty()
. É importante estar ciente disso e garantir que o código dentro do map
seja seguro.
Evite lógica muito complexa dentro do map
O map deve ser usado para transformações simples e diretas. Se você colocar lógica mais complexa, como loops ou operações encadeadas, então não concordo em estar utilizando esse recurso, acredito que seja melhor delegar isso para um método auxiliar. Eu já vi muito isso em bases de código em várias empresas:
Optional<Cliente> clienteOptional = Optional.ofNullable(cliente);
Optional<String> name = clienteOptional.map(cliente -> {
// Complex logic here (not recommended)
if (cliente.getName().length() > 10) {
return cliente.getName().substring(0, 10).toUpperCase();
}
return cliente.getName().toUpperCase();
});
Isso dificulta a manutenção no futuro e aumenta o risco de introduzir comportamentos inesperados! Seria melhor nesse caso criar um método para isso.
Considere o impacto de transformar valores complexos
Quando o valor dentro do Optional
é algo mais complexo, como uma lista, um mapa, ou outra estrutura de dados, é importante pensar no que exatamente você deseja alcançar com a transformação. O Optional
em si não tem mecanismos automáticos para lidar com listas vazias, mapas nulos, ou evitar erros que podem surgir quando você manipula esses objetos.
Por exemplo, vamos supor que temos um cliente e queremos acessar sua lista de pedidos. A lista de pedidos pode ser null ou simplesmente vazia. Com o uso do map
, conseguimos acessar a lista, mas ela ainda precisa ser manipulada com cuidado:
Optional<Cliente> clienteOptional = Optional.ofNullable(cliente);
// Transformamos o cliente em sua lista de pedidos
Optional<List<Pedido>> pedidosOptional = clienteOptional.map(Cliente::getPedidos);
// Precisamos lidar com o caso onde a lista pode ser null ou vazia
List<Pedido> pedidos = pedidosOptional.orElse(Collections.emptyList());
System.out.println("Pedidos do cliente: " + pedidos);
Tá mas qual o problema e onde quero chegar com esse ponto de atenção? O Optional
transforma o cliente em uma lista de pedidos usando map
, mas ele não resolve automaticamente o que fazer com uma lista nula ou vazia. Nesse exemplo, usamos .orElse(Collections.emptyList())
para garantir que mesmo que a lista seja null, o código não lance uma exceção e retorne uma lista vazia.
Se não tomarmos cuidado, isso pode levar a resultados inesperados:
• Se a lógica de transformação assume que sempre haverá valores na lista, você pode acabar processando uma lista vazia sem perceber.
• Uma lista nula não será convertida automaticamente para uma lista vazia. Você precisa garantir isso manualmente.
Dica: Sempre defina um valor padrão para coleções
Quando transformar listas, mapas ou conjuntos em um Optional, tenha em mente:
• Use .orElse(Collections.emptyList())
para listas.
• E para mapas .orElse(Collections.emptyMap())
é recomendado.
• Lembre sempre de verificar explicitamente se a coleção está vazia antes de processá-la, se necessário.
Tá mas como esses cuidados ficam realmente na prática? Imagine que queremos calcular o total de itens de todos os pedidos de um cliente:
Optional<Cliente> clienteOptional = Optional.ofNullable(cliente);
int totalItens = clienteOptional
.map(Cliente::getPedidos) // Obtém a lista de pedidos
.orElse(Collections.emptyList()) // Garante que não seja null
.stream()
.mapToInt(Pedido::getQuantidadeItens) // Soma as quantidades de itens
.sum();
System.out.println("Total de itens nos pedidos: " + totalItens);
Aqui, tomamos os seguintes cuidados:
Garantimos que a lista de pedidos nunca seja null, substituindo-a por uma lista vazia, se necessário.
Usamos
.stream()
para processar a lista de forma declarativa, somando os itens em cada pedido.O código é seguro mesmo que o cliente não tenha pedidos (ou que os pedidos sejam inicialmente null).
Essa abordagem ajuda a evitar erros comuns que podem dar uma dor de cabeça.
Trabalhando com: .flatMap()
O método .flatMap
é essencial quando estamos lidando com situações em que o método de transformação já retorna um Optional
. Ele ajuda a evitar o aninhamento de Optional, ou seja, um Optional<Optional<T>>
, que pode complicar a lógica e deixar o código confuso.
Aninhamento de Optional
Imagine o seguinte cenário: você tem uma classe Cliente com um método buscarNomePreferido()
, que já retorna um Optional<String>
(afinal, nem todos os clientes têm um nome preferido). Você decide usar .map()
para transformar o valor, mas sem perceber, acaba criando um Optional<Optional<String>>
.
Optional<Cliente> clienteOptional = Optional.ofNullable(cliente);
// Aninhamento acidental de Optional
Optional<Optional<String>> nomeOptional = clienteOptional.map(cliente -> cliente.buscarNomePreferido());
// Agora o acesso ao valor está desnecessariamente complexo
if (nomeOptional.isPresent() && nomeOptional.get().isPresent()) {
System.out.println("Nome preferido: " + nomeOptional.get().get());
}
Complicado, né? Você começa a fazer várias verificações com .isPresent()
, e ainda precisa chamar .get()
mais de uma vez. Isso não apenas quebra a simplicidade que o Optional deveria trazer, mas também torna o código mais difícil de entender e manter.
E sabe o pior? Esse tipo de problema é mais comum do que parece, especialmente em sistemas que combinam vários métodos que trabalham com Optional
.
A Solução com FlatMap
Ele resolve esse problema ao “achatar” o Optional
, eliminando o nível extra de encapsulamento. Quando o método de transformação já retorna um Optional
, o .flatMap()
entende isso e ajusta o resultado automaticamente.
Veja como o código fica muito mais simples e direto:
Optional<Cliente> clienteOptional = Optional.ofNullable(cliente);
// Usando flatMap para evitar aninhamento
Optional<String> nomeOptional = clienteOptional.flatMap(Cliente::buscarNomePreferido);
nomeOptional.ifPresent(nome -> System.out.println("Nome preferido: " + nome));
Agora, não há Optional<Optional<String>>
para lidar. O código é mais legível, seguro e mantém o fluxo funcional sem complicações.
Código completo:
import java.util.Optional;
class Cliente {
private String nomePreferido;
public Cliente(String nomePreferido) {
this.nomePreferido = nomePreferido;
}
// Método que retorna um Optional, pois nem todos os clientes têm um nome preferido
public Optional<String> buscarNomePreferido() {
return Optional.ofNullable(nomePreferido);
}
}
public class Main {
public static void main(String[] args) {
// Cliente com nome preferido
Cliente clienteComNome = new Cliente("João");
Optional<Cliente> clienteOptional1 = Optional.ofNullable(clienteComNome);
// Cliente sem nome preferido (null)
Cliente clienteSemNome = new Cliente(null);
Optional<Cliente> clienteOptional2 = Optional.ofNullable(clienteSemNome);
// Usando .flatMap para lidar com cliente com nome preferido
Optional<String> nomePreferido1 = clienteOptional1.flatMap(Cliente::buscarNomePreferido);
nomePreferido1.ifPresent(nome -> System.out.println("Nome preferido do cliente 1: " + nome));
// Usando .flatMap para lidar com cliente sem nome preferido
Optional<String> nomePreferido2 = clienteOptional2.flatMap(Cliente::buscarNomePreferido);
nomePreferido2.ifPresentOrElse(
nome -> System.out.println("Nome preferido do cliente 2: " + nome),
() -> System.out.println("Cliente 2 não possui nome preferido.")
);
// Exemplo sem .flatMap para mostrar aninhamento
Optional<Optional<String>> nomeAninhado = clienteOptional1.map(Cliente::buscarNomePreferido);
nomeAninhado.ifPresent(nome -> nome.ifPresent(n -> System.out.println("Nome preferido (aninhado): " + n)));
}
}
Filtrando Valores
O filter é um método que permite verificar se o valor contido no optional
atende a um critério específico. Se atender, o mesmo Optional
é retornado; caso contrário, ele se transforma em um Optional.empty()
. Tudo isso sem a necessidade de verificações explícitas ou condicionais adicionais.
Por que o filter()
é tão útil?
Imagine que você está trabalhando com um valor que já foi encapsulado em um Optional
, mas você só quer continuar com ele se ele atender a uma condição específica. Em vez de usar vários if ou lógica manual para verificar o valor, o .filter()
permite expressar essa ideia de forma declarativa, deixando seu código mais limpo e fácil de entender.
Digamos que você tenha uma variável que representa a idade de uma pessoa, e você só quer processar idades maiores ou iguais a 18. Com o filter()
, isso fica incrivelmente simples:
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Integer idade = 20; // Tente trocar para 15 e veja o resultado
Optional<Integer> idadeOptional = Optional.ofNullable(idade)
.filter(i -> i >= 18);
idadeOptional.ifPresentOrElse(
i -> System.out.println("Idade válida: " + i),
() -> System.out.println("Idade não atende aos critérios")
);
}
}
Se a idade for menor que 18 ou null, o Optional
será automaticamente esvaziado, e a mensagem “Idade não atende aos critérios” será exibida.
Como isso ajuda no dia a dia?
O método .filter() é excelente para validar valores opcionais sem adicionar muitas verificações manuais. Vamos a mais um exemplo:
Imagine que você está processando o e-mail de um cliente e só quer continuar com ele se o e-mail for de um domínio específico, como “gmail.com”:
public static void main(String[] args) {
String email = "usuario@gmail.com"; // Experimente mudar o domínio
Optional<String> emailOptional = Optional.ofNullable(email)
.filter(e -> e.endsWith("@gmail.com"));
emailOptional.ifPresentOrElse(
e -> System.out.println("E-mail válido: " + e),
() -> System.out.println("E-mail rejeitado: não é um Gmail")
);
}
Se o e-mail terminar com “@gmail.com”, ele será processado; caso contrário, será rejeitado.
Dicas ao usar o filter()
1. Expressividade: Use o filter para expressar claramente as condições que o valor deve atender. Isso torna o código mais legível e fácil de entender.
2. Evite filtros pesados: Deve ser usado para condições simples. Se a lógica do filtro for complexa, considere delegá-la para um método separado.
3. Combinação com outros métodos: Funciona muito bem em conjunto com outros métodos do Optional
, como map
, flatMap
e ifPresentOrElse
.
O filter é como um porteiro para o valor encapsulado no Optional
. Ele garante que só valores que atendem a critérios específicos passem para as próximas etapas, e faz isso de forma elegante e declarativa. Ao usar o o filter, você escreve menos verificações manuais, evita duplicação de lógica e mantém o código mais limpo.
Devo Usar Optional para Tudo?
Vamos falar um pouco sobre isso. Depois de aprender o quanto ele pode salvar seu código de surpresas desagradáveis e reduzir a dor de cabeça com o famigerado NullPointer, é fácil cair na tentação de usá-lo para tudo. Afinal, quem não gosta de uma solução elegante. Mas, como tudo na programação (e na vida 😅), exageros podem trazer novos problemas. O Optional
é uma ferramenta poderosa, mas nem sempre é a melhor escolha. Vamos conversar sobre quando e por que você deve avaliar seu uso com cautela.
1. Nem todo método precisa retornar um Optional
Uma das maiores confusões ao começar a usar o Optional é achar que todos os métodos devem retornar um. Por exemplo, se o método sempre retorna um valor válido, não há motivo para encapsulá-lo no optional. Isso só vai adicionar uma camada desnecessária ao código.
Esse contêiner de objeto faz sentido quando há incerteza sobre a existência de um valor. Caso contrário, ele se torna redundante. Pense assim: o uso do Optional
deve comunicar algo para quem lê o código. Quando você vê um em um método, ele diz claramente: “Ei, esse valor pode ou não existir, então lide com isso!”. Se o valor nunca for nulo ou ausente, o uso dele perde o propósito.
2. Saiba quando usá-lo em variáveis de instância
Outro ponto importante é: o Optional
não foi projetado para ser usado como um atributo de classe. Por exemplo, ter um campo de um objeto como optional pode tornar o código confuso, especialmente se você precisar serializar essa classe ou trabalhar com frameworks como JPA, que não lidam nada bem com optional em campos.
A prática aqui recomendada é usar Optional
apenas em métodos de leitura, onde faz sentido comunicar que o valor pode estar ausente. Para atributos, prefira valores padrão ou deixe claro que o campo pode ser null e lide com isso nas camadas superiores.
3. Evite abusar do Optional em coleções
Um dos erros mais comuns é misturar Optional
com coleções. Por exemplo, ter uma lista de Optional<T>
ou mesmo um Optional<List<T>>
. Isso pode tornar o código confuso e difícil de trabalhar.
Em vez disso, pense no propósito do seu método:
• Se você está retornando uma lista, por que ela estaria encapsulada em um Optional
? Normalmente, o caso “ausente” pode ser tratado retornando uma lista vazia.
• Se você precisa representar a presença ou ausência de um único elemento, é mais adequado retornar um Optional<T> diretamente, em vez de usar uma lista que contém um Optional.
Manter a separação entre “presença de valor” e “manipulação de coleções” ajuda a evitar confusões.
4. O programador precisa saber usar o Optional
Talvez o mais importante: não adianta usar Optional se você ou sua equipe não entenderem como utilizá-lo corretamente. Muitas vezes, desenvolvedores utilizam Optional sem explorar seus benefícios ou quebrando as regras básicas de uso, como:
• Usar Optional.get() sem verificar se o valor está presente: Isso basicamente recria o problema do NullPointerException, mas com outro nome. O objetivo do Optional é evitar esse tipo de prática.
• Abusar de verificações manuais com isPresent(): Quando há métodos mais declarativos e elegantes disponíveis, como .ifPresent() (para executar uma ação se o valor estiver presente), .orElse() (para fornecer um valor padrão) ou .orElseThrow() (para lançar uma exceção se o valor estiver ausente).
• Encapsular coisas desnecessariamente em Optional: Por exemplo, retornar um Optional dentro de uma coleção (como List<Optional<T>> ou Map<Key, Optional<Value>>). Isso adiciona complexidade sem valor real.
• Usar Optional em contextos desnecessários: Não é recomendável usá-lo como argumento de métodos ou em atributos de classes, já que ele foi projetado principalmente para retorno de métodos.
Se você não está confortável com os métodos do Optional, o resultado pode ser um código mais confuso e propenso a erros do que se tivesse usado verificações manuais de null. Então, antes de adotar o Optional em larga escala, invista tempo para entender suas melhores práticas.
Então, qual é a regra de ouro?
O Optional é uma fantástico quando usada no contexto certo. Aqui estão algumas dicas rápidas para te ajudar a decidir:
• Use o contêiner para indicar valores opcionais em métodos de leitura. Se o método pode retornar ou não um valor, o Optional
comunica essa possibilidade claramente.
• Evite como atributo de classe ou em coleções. Ele não foi projetado para esses cenários e pode complicar desnecessariamente o código.
• Não encapsule valores que nunca serão null. Isso adiciona complexidade sem nenhum benefício.
• Garanta que você e sua equipe entendam como usar o Optional
. A má utilização pode introduzir novos problemas ao invés de resolvê-los.
No final das contas, o Optional
não é uma solução mágica para todos os problemas com null. É uma ferramenta, e como qualquer ferramenta, deve ser usada no momento certo.1
Existem outros assuntos para falar sobre Optional
mas para não ficar muita coisa, fico por aqui e em outra newsletter conversamos! Muito obrigado por ler até o final e pelo apoio de sempre! Grande abraço e até! ✌🏼
Conselhos de Brian Goetz sobre o Uso do Optional
Brian Goetz, arquiteto-chefe da linguagem Java, compartilhou orientações valiosas sobre o uso adequado do Optional. Vamos direto ao ponto:
1. Use Optional Principalmente como Tipo de Retorno
O Optional foi concebido para ser utilizado principalmente como tipo de retorno em métodos que podem não ter um resultado concreto. Isso torna explícita a possibilidade de ausência de valor, incentivando o tratamento adequado por parte do chamador. No entanto, seu uso em outros contextos deve ser ponderado.
2. Evite Optional em Campos de Classe
Utilizar Optional como tipo de campo em classes não é uma prática recomendada. Isso pode adicionar complexidade desnecessária e não oferece benefícios significativos em comparação com o uso tradicional de null ou outras abordagens para representar a ausência de valor.
3. Não Use Optional em Parâmetros de Métodos
Passar ele como parâmetro em métodos não é aconselhável. Se um valor é opcional para um método, é preferível sobrecarregar o método ou fornecer métodos auxiliares que lidem com a ausência de valor de maneira mais clara e direta.
4. Evite Encapsular Coleções em Optional
Retornar um optional contendo uma coleção, como uma lista ou um conjunto, pode complicar o código desnecessariamente. Em vez disso, retorne a própria coleção; uma coleção vazia já comunica a ausência de elementos de forma eficaz.
5. Seja Cauteloso com o Uso Excessivo de Optional
Embora seja uma ferramenta poderosa para indicar a ausência de valor, seu uso indiscriminado pode levar a um código mais complexo e difícil de manter. Utilize-o com moderação e apenas nos casos em que realmente agrega clareza e segurança ao código.
Para uma leitura mais aprofundada sobre as melhores práticas no uso do Optional, recomendo o artigo “Java Optional and best practices”, que discute em detalhes essas orientações.