4. A Classe Optional: Tratando a Ausência de Valor com Elegância
Um dos erros mais comuns em Java é o NullPointerException. Ele ocorre quando tentamos usar um membro de uma referência de objeto que é null. A classe Optional<T> foi introduzida para fornecer uma solução de tipo seguro para representar a presença ou ausência de um valor.
Optional é um container: um objeto que pode ou não conter um valor não nulo. Ele nos força a pensar sobre o caso em que o valor não está presente.
4.1. Criando um Optional
Existem três maneiras principais de criar uma instância de Optional, cada uma adequada a um cenário diferente. Entender qual usar é o primeiro passo para escrever um código mais seguro.
1. Optional.of(valor)
-
Quando usar: Quando você tem um valor que, por contrato, você tem certeza de que não é nulo.
-
Comportamento: Cria um
Optionalque “envolve” o seu objeto. Se você tentar passarnullparaOptional.of(), ele falhará imediatamente (fail-fast) com umNullPointerException. Isso é útil para validar suas próprias premissas no código.// Cenário: Você acabou de criar um novo objeto. Ele NUNCA será nulo. User newUser = new User("Ana"); Optional<User> userOptional = Optional.of(newUser); // Correto e seguro. User userNulo = null; // A linha abaixo lançaria um NullPointerException, protegendo o resto do código. // Optional<User> userNuloOptional = Optional.of(userNulo);
2. Optional.ofNullable(valor)
-
Quando usar: Este é o método mais seguro e comum. Use-o quando você está lidando com um valor que pode ser nulo, como o retorno de um método de busca ou de um mapa.
-
Comportamento: Se o valor fornecido não for nulo, ele o envolve em um
Optional. Se o valor fornull, ele cria umOptionalvazio (Optional.empty()). Este método nunca lançaNullPointerException.// Cenário: Buscando um usuário que pode ou não existir em um repositório. // O método findById pode retornar um User ou null. User foundUser = userRepository.findById(1L); Optional<User> userOptional = Optional.ofNullable(foundUser); // Forma segura de lidar com o resultado.
3. Optional.empty()
-
Quando usar: Quando você precisa representar explicitamente a ausência de um valor. É frequentemente usado como valor de retorno em métodos que podem não encontrar o que procuram.
-
Comportamento: Retorna uma instância de
Optionalque está garantidamente vazia.// Cenário: Um método que retorna um Optional<User>, mas uma validação inicial falha. public Optional<User> findUser(Long id) { if (id <= 0) { return Optional.empty(); // Retorna "nada" de forma segura e explícita. } // ... continua a lógica de busca ... return Optional.ofNullable(userRepository.findById(id)); }
| Método | Quando Usar | Comportamento com null |
|---|---|---|
Optional.of(obj) | Quando obj não pode ser nulo. | Lança NullPointerException. |
Optional.ofNullable(obj) | Quando obj pode ser nulo. | Retorna Optional.empty(). |
Optional.empty() | Para retornar explicitamente um valor ausente. | N/A (já retorna um Optional vazio). |
4.2. Usando Optional de Forma Segura
A maneira errada de usar Optional é simplesmente chamar .get() sem verificar se o valor está presente, pois isso pode lançar uma NoSuchElementException.
Para entender o motivo, precisamos pensar na proposta do Optional: ele é um contrato que diz “este container pode ou não ter um valor”. O método .get() foi projetado para ser a forma mais direta de extrair o valor, mas ele opera sob uma premissa perigosa: ele assume que o valor existe.
Quando o Optional está vazio, o contrato do método .get() é lançar a exceção NoSuchElementException. Este é o seu comportamento esperado.
O problema: NoSuchElementException é uma RuntimeException (uma exceção não checada). Isso significa que o compilador Java não obriga você a tratá-la com um bloco try-catch. Se você chamar .get() em um Optional vazio sem uma verificação prévia, e não houver um tratamento de exceção, sua aplicação irá quebrar em tempo de execução.
Exemplo prático do erro:
// Cenário: Buscamos um produto que não existe no banco de dados.
Optional<Product> productOptional = productRepository.findById(999L); // Retorna Optional.empty()
// Tentativa errada de extrair o valor
try {
Product product = productOptional.get(); // <<-- ISTO VAI LANÇAR A EXCEÇÃO
System.out.println("Produto encontrado: " + product.getName());
} catch (NoSuchElementException e) {
System.err.println("ERRO: O produto não foi encontrado. A aplicação quebrou aqui!");
// Em uma API REST, isso resultaria em um erro 500 (Internal Server Error)
// se não fosse tratado, o que é uma péssima experiência para o usuário.
}
Essencialmente, chamar .get() sem uma verificação (.isPresent()) é trocar um risco (NullPointerException) por outro (NoSuchElementException), o que não resolve o problema fundamental de lidar com a ausência de valor.
É por isso que as formas idiomáticas (ifPresent, orElse, orElseThrow, etc.) são preferíveis: elas forçam o desenvolvedor a lidar explicitamente com o caso em que o Optional está vazio, resultando em um código mais seguro, robusto e previsível.
As formas seguras e idiomáticas de usar Optional são as seguintes:
-
ifPresent(Consumer<T>): Executa uma ação somente se o valor estiver presente.Optional<User> user = userRepository.findById(1L); user.ifPresent(u -> System.out.println("Usuário encontrado: " + u.getName())); -
orElse(T other): Retorna o valor se presente, senão retorna um valor padrão.String username = user.map(User::getName).orElse("Usuário Padrão"); -
orElseThrow(Supplier<X> exceptionSupplier): Retorna o valor se presente, senão lança uma exceção. Esta é a forma mais comum em APIs REST.Contact contact = contactRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Contato não encontrado: " + id));
4.3. Cuidado: orElse() vs. orElseGet()
Existe uma diferença de performance sutil, mas importante, entre orElse() e orElseGet().
orElse(valorPadrao): OvalorPadraoé sempre calculado, mesmo que oOptionalcontenha um valor.orElseGet(supplier): Osupplier(uma lambda) só é executado se oOptionalestiver vazio.
Exemplo:
// Método custoso que busca um nome padrão no banco de dados
public String getNomePadraoSistema() {
System.out.println("Executando método custoso...");
// ...lógica de banco...
return "Admin";
}
Optional<String> nomeUsuario = Optional.of("Ana");
// Mau uso: o método custoso será executado desnecessariamente.
String nome1 = nomeUsuario.orElse(getNomePadraoSistema());
// Console imprime: "Executando método custoso..."
// Bom uso: o método custoso NÃO será executado.
String nome2 = nomeUsuario.orElseGet(this::getNomePadraoSistema);
// Console não imprime nada.
Regra geral: Se o valor padrão for uma constante simples, use orElse(). Se a criação do valor padrão for custosa (envolve I/O, cálculo, etc.), use orElseGet(). Vejamos abaixo uma tabela comparativa entre esses dois método.
Tabela Comparativa: orElse() vs. orElseGet()
| Característica | orElse(T valor) | orElseGet(Supplier<T> supplier) |
|---|---|---|
| Argumento que Aceita | Um valor (T) já criado ou uma constante. | Uma função/lambda (Supplier<T>) que sabe como criar o valor. |
| Custo da Operação Padrão | O valor padrão é calculado sempre, mesmo se o Optional contiver um valor. | A função para criar o valor padrão é executada apenas se o Optional estiver vazio. |
| Cenário Ideal | Usar com valores constantes ou objetos que já existem e são baratos de obter. | Usar quando a criação do valor padrão exige cálculo, I/O ou chamadas a outros métodos custosos. |
O exemplo abaixo exemplifica novamente os conceitos, para não deixarmos nenhuma dúvida sobre quando usar um ou outro. 🤓
Mais um exemplo de Código Prático
Imagine que estamos buscando uma configuração em um mapa. Se a configuração não existir, precisamos obter um valor padrão de um método que simula uma operação lenta (como uma consulta ao banco).
import java.util.Map;
import java.util.Optional;
public class OptionalExample {
// Método que simula uma operação custosa (ex: acesso a banco, chamada de API)
public static String getExpensiveDefaultValue() {
System.out.println(">>> Executando método custoso para obter valor padrão...");
// Simula um delay
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "default_value";
}
public static void main(String[] args) {
Map<String, String> configs = Map.of("key1", "value1");
System.out.println("--- Teste com um Optional que contém valor ---");
Optional<String> optionalWithValue = Optional.of("value1");
// Usando orElse(): O método custoso SERÁ chamado desnecessariamente.
System.out.println("Usando orElse():");
String value1 = optionalWithValue.orElse(getExpensiveDefaultValue());
System.out.println("Resultado: " + value1);
System.out.println("\n"); // Separador
// Usando orElseGet(): O método custoso NÃO será chamado.
System.out.println("Usando orElseGet():");
String value2 = optionalWithValue.orElseGet(() -> getExpensiveDefaultValue()); // ou OptionalExample::getExpensiveDefaultValue
System.out.println("Resultado: " + value2);
System.out.println("\n--- Teste com um Optional vazio ---");
Optional<String> emptyOptional = Optional.empty();
// Com Optional vazio, ambos chamarão o método, mas orElseGet() continua sendo a prática mais segura.
System.out.println("Usando orElse() com Optional vazio:");
String value3 = emptyOptional.orElse(getExpensiveDefaultValue());
System.out.println("Resultado: " + value3);
}
}
Saída do Código:
--- Teste com um Optional que contém valor ---
Usando orElse():
>>> Executando método custoso para obter valor padrão...
Resultado: value1
Usando orElseGet():
Resultado: value1
--- Teste com um Optional vazio ---
Usando orElse() com Optional vazio:
>>> Executando método custoso para obter valor padrão...
Resultado: default_value
Como o resultado demonstra, orElseGet() evitou a execução desnecessária do método custoso quando o Optional já continha um valor, otimizando a performance da aplicação.
Passemos agora a um uso prático do Optional.
4.4. Exemplo Prático: Usando Optional para Conectar Camadas
Agora que conhecemos as ferramentas, vamos aplicá-las em um cenário realista. Imagine a estrutura de uma aplicação comercial dividida em camadas:
- Repository: Responsável por acessar os dados (simularemos um banco em memória).
- Service: Onde mora a lógica de negócio.
- Controller: A porta de entrada que recebe as requisições (simularemos com uma classe
main).
Nossa tarefa é criar um método que atualiza o e-mail de um usuário, mas apenas se o usuário existir e se o novo e-mail for válido. Veja como o Optional cria um contrato seguro e elegante entre essas camadas (lembrando que o código-fonte abaixo é simplificado para fins didáticos!).
Camada 1: O Repositório (Simulando o acesso a dados)
Primeiro, nosso UserRepository simula a busca no banco. Note como ele já retorna um Optional, protegendo o resto da aplicação desde a fonte dos dados.
// Simula uma entidade do banco de dados
class User {
private Long id;
private String name;
private String email;
// Construtores, Getters e Setters...
public User(Long id, String name, String email) { this.id = id; this.name = name; this.email = email; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
@Override public String toString() { return "User{id=" + id + ", name='" + name + "', email='" + email + "'}"; }
}
// Simula a classe que acessa o banco de dados
class UserRepository {
// Um mapa para simular nossa tabela de usuários no banco
private final Map<Long, User> database = new HashMap<>();
public UserRepository() {
// Populando o "banco" com dados iniciais
database.put(1L, new User(1L, "Ana", "ana@email.com"));
database.put(2L, new User(2L, "Carlos", "carlos@email.com"));
}
// O método de busca já retorna um Optional, como faz o Spring Data JPA
public Optional<User> findById(Long id) {
return Optional.ofNullable(database.get(id));
}
public User save(User user) {
database.put(user.id, user);
return user;
}
}
Camada 2: O Serviço (Onde a lógica de negócio acontece)
O UserService recebe o UserRepository e usa o Optional para criar um fluxo seguro de validação e atualização.
// Simula a camada de serviço com a lógica de negócio
class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public Optional<User> updateUserEmail(Long userId, String newEmail) {
// 1. Busca o usuário. O retorno já é um Optional<User>.
return userRepository.findById(userId)
.filter(user -> isEmailValid(newEmail)) // 2. Continua apenas se o email for válido.
.map(user -> { // 3. Se tudo estiver ok, transforma o objeto.
user.setEmail(newEmail);
return userRepository.save(user); // 4. Salva e retorna o usuário atualizado.
});
}
private boolean isEmailValid(String email) {
return email != null && email.contains("@");
}
}
- Aqui buscamos o
Optional, filtramos, e mapeamos para a nova versão salva. Se em qualquer ponto oOptionalficar vazio (usuário não encontrado ou e-mail inválido), as operações seguintes são simplesmente ignoradas.
Camada 3: O “Controller” e a Execução (main)
Finalmente, nossa classe main simula a “requisição” do cliente e mostra como o Controller interpretaria a resposta segura vinda do Service.
// Simula a camada que recebe as "requisições"
class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
public String handleUpdateEmailRequest(Long id, String newEmail) {
// O "Controller" chama o serviço e lida com o Optional retornado.
return userService.updateUserEmail(id, newEmail)
.map(user -> "200 OK - Usuário atualizado: " + user.toString()) // Se sucesso, formata a resposta OK.
.orElse("404 Not Found - Usuário não encontrado ou email inválido."); // Se vazio, formata a resposta de erro.
}
}
// Ponto de entrada da nossa aplicação simulada
public class MainApplication {
public static void main(String[] args) {
// Injeção de dependência manual: criamos e conectamos as camadas.
UserRepository userRepository = new UserRepository();
UserService userService = new UserService(userRepository);
UserController userController = new UserController(userService);
// --- Simulação das Requisições ---
// Teste 1: Caso de sucesso
System.out.println("Tentativa 1: Atualizando usuário com ID 1...");
String response1 = userController.handleUpdateEmailRequest(1L, "ana.nova@email.com");
System.out.println("Resposta: " + response1);
System.out.println("--------------------");
// Teste 2: Caso de falha (usuário não existe)
System.out.println("Tentativa 2: Atualizando usuário com ID 99...");
String response2 = userController.handleUpdateEmailRequest(99L, "fantasma@email.com");
System.out.println("Resposta: " + response2);
}
}
Este exemplo de ponta a ponta, mesmo sem uso de framework, demonstra o poder do Optional: ele atua como um contrato seguro entre as camadas, eliminando a necessidade de verificações de nulo e permitindo a criação de fluxos de dados robustos e declarativos, que são fáceis de ler e manter.
5. Method References: “Sintaxe Açucarada” para Lambdas
Em muitos casos, uma expressão lambda apenas chama um método existente. As referências de método (Method References) são uma sintaxe compacta para esses casos.
| Tipo de Referência | Exemplo Lambda | Exemplo Method Reference |
|---|---|---|
| Método Estático | str -> Integer.parseInt(str) | Integer::parseInt |
| Método de Instância (objeto específico) | () -> expensiveObject.get() | expensiveObject::get |
| Método de Instância (objeto arbitrário) | s -> s.toUpperCase() | String::toUpperCase |
| Construtor | () -> new ArrayList<>() | ArrayList::new |
Exemplo Prático:
A atualização parcial de um contato usava .ifPresent():
// Com lambda
dto.getNome().ifPresent(nome -> existingContact.setNome(nome));
// Com method reference (mais limpo e preferível)
dto.getNome().ifPresent(existingContact::setNome);
Ambas as linhas fazem a mesma coisa, mas a referência de método é mais expressiva e fácil de ler.