Optional em Java: usando corretamente e anti-patterns
1. Visão geral
Optional nasceu em Java 8 para representar um valor “que pode não existir” de forma mais clara que null. Mas muitos devs usam errado: abusam de Optional como field, parâmetro, ou chamam .get() como se nunca estivesse vazio. Este artigo ajuda você a entender quando usar Optional, os padrões certos, e lista os anti-patterns comuns para evitar.
Ao terminar, você saberá como projetar APIs que retornam Optional de forma sensata, lidar com valores vazios de forma elegante com map/filter/orElseGet, e reconhecer imediatamente os trechos de código “pseudo-Optional” que precisam ser refatorados.
2. O que é Optional e quando usar
Optional<T> é um container que pode conter ou não um valor do tipo T. Seu propósito principal: deixar claro na assinatura do método que o valor retornado pode não existir, obrigando quem chama a tratar esse caso em vez de esquecer de verificar null.
public class CustomerRepository {
private final Map<Long, Customer> store = new HashMap<>();
public Optional<Customer> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
}
findById retorna Optional<Customer> em vez de Customer (que pode ser null). Quem chama só de olhar a assinatura já sabe que precisa tratar o caso de não encontrar, sem precisar ler Javadoc.
Nota: Optional foi projetado para valores de retorno, não para todo lugar que pode ser null no sistema.
3. Formas de criar Optional
Optional<String> present = Optional.of("Hello"); // valor garantidamente diferente de null
Optional<String> empty = Optional.empty(); // vazio
Optional<String> nullable = Optional.ofNullable(null); // pode ser null, converte automaticamente para vazio
Optional.of(value)lançaNullPointerExceptionimediatamente sevaluefor null — use quando tiver certeza de que o valor não é null.Optional.ofNullable(value)é mais seguro, adequado quando o valor vem de uma fonte que você não controla (resultado de query, map.get…).Optional.empty()cria uma instância vazia, use quando sabe de antemão que não há valor.
4. Lidando com valores de forma correta
4.1. map, filter, flatMap
public record Order(Long id, BigDecimal amount, String couponCode) {}
public class DiscountService {
public Optional<BigDecimal> calculateDiscount(Order order) {
return Optional.ofNullable(order.couponCode())
.filter(code -> code.startsWith("SALE"))
.map(code -> order.amount().multiply(BigDecimal.valueOf(0.1)));
}
}
filter mantém o valor apenas se a condição for verdadeira, caso contrário retorna Optional.empty(). map transforma o valor dentro se ele existir, não faz nada se estiver vazio. Essa sequência evita completamente if-else aninhados checando null.
4.2. orElse, orElseGet, orElseThrow
public class PaymentService {
private final CustomerRepository repository;
public PaymentService(CustomerRepository repository) {
this.repository = repository;
}
public Customer resolveCustomer(Long id) {
return repository.findById(id)
.orElseThrow(() -> new CustomerNotFoundException(id));
}
public String resolveCustomerName(Long id) {
return repository.findById(id)
.map(Customer::name)
.orElse("Guest");
}
}
orElseThrow(supplier)lança uma exception significativa quando não encontra — use onde o valor é obrigatório.orElse("Guest")use quando o valor padrão é uma constante simples, sempre é calculado mesmo que Optional tenha valor.orElseGet(supplier)use quando o valor padrão custa caro para calcular — só chama o supplier quando Optional estiver vazio.
// Errado: sempre chama buildDefaultCustomer() mesmo tendo valor ou não
repository.findById(id).orElse(buildDefaultCustomer());
// Correto: só chama quando realmente precisa
repository.findById(id).orElseGet(this::buildDefaultCustomer);
4.3. ifPresent e ifPresentOrElse
public class NotificationService {
public void notifyCustomer(Optional<Customer> customer) {
customer.ifPresentOrElse(
c -> System.out.println("Enviar email para " + c.email()),
() -> System.out.println("Nenhum cliente para enviar")
);
}
}
ifPresentOrElse (do Java 9) combina os dois ramos com/sem valor em uma única chamada, em vez de if (optional.isPresent()) {...} else {...} verboso.
5. Testando comportamento com JUnit 5 + AssertJ
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<scope>test</scope>
</dependency>
class DiscountServiceTest {
private final DiscountService service = new DiscountService();
@Test
void shouldReturnDiscountWhenCouponValid() {
Order order = new Order(1L, BigDecimal.valueOf(100), "SALE10");
Optional<BigDecimal> discount = service.calculateDiscount(order);
assertThat(discount)
.isPresent()
.get()
.isEqualTo(BigDecimal.valueOf(10.0));
}
@Test
void shouldReturnEmptyWhenNoCoupon() {
Order order = new Order(2L, BigDecimal.valueOf(100), null);
Optional<BigDecimal> discount = service.calculateDiscount(order);
assertThat(discount).isEmpty();
}
}
AssertJ tem assertions dedicadas para Optional: isPresent(), isEmpty(), hasValue(...). Use-as em vez de chamar .get() e comparar, o código de teste fica mais natural de ler.
6. Anti-patterns comuns
6.1. Chamar .get() sem verificar
// Errado: se Optional estiver vazio lança NoSuchElementException,
// completamente elimina a razão de usar Optional
Customer customer = repository.findById(id).get();
Chamar .get() diretamente faz Optional ser nada mais que null embrulhado em uma camada — você ainda pode ter crash em runtime, mas ainda gasta uma linha a mais. Use orElseThrow() (com exception clara) ou trate via map/ifPresent.
6.2. Usar Optional como field em entity/DTO
// Errado: Optional não é Serializable, não deve aparecer em entity JPA
@Entity
public class Customer {
private Optional<String> middleName; // evite
}
Optional não implementa Serializable, não foi projetado para ser field ou parâmetro de constructor. Se middleName pode ser null, deixe como String e null — só envolva com Optional quando retornar do getter se quiser:
// Correto: field ainda é String, só envolva Optional no getter se quiser
public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
6.3. Usar Optional como parâmetro de método
// Errado: obriga quem chama a embrulhar Optional.of/ofNullable antes de chamar
public void updateAddress(Optional<String> newAddress) { ... }
Assim empurra a complexidade para quem chama e não traz benefício nenhum comparado a overload ou @Nullable. Use overload ou passe String com verificação null comum:
// Correto
public void updateAddress(String newAddress) {
this.address = newAddress; // quem chama decide se passa null ou não
}
6.4. Verificar isPresent() depois chamar get() como null-check antigo
// Errado: reescreve verificação null estilo antigo, não aproveita nada do Optional
if (repository.findById(id).isPresent()) {
Customer c = repository.findById(id).get(); // chama query 2 vezes!
System.out.println(c.name());
}
Este trecho é verboso e chama findById duas vezes se for um query real. Substitua por:
// Correto
repository.findById(id).ifPresent(c -> System.out.println(c.name()));
6.5. Usar Optional para collection
// Errado: collection já tem conceito de 'vazio' (empty list), não precisa de Optional
public Optional<List<Order>> findOrdersByCustomer(Long customerId) { ... }
Retornar Optional<List<Order>> obriga quem chama a tratar dois níveis de vazio: Optional vazio e list vazio — redundante. Sempre retorne List vazio quando não há dados:
// Correto
public List<Order> findOrdersByCustomer(Long customerId) {
return orders.getOrDefault(customerId, List.of());
}
6.6. Usar Optional em hot path com desempenho crítico
Optional cria um wrapper de object adicional, não é grande custo na maioria das aplicações, mas em código que roda milhões de vezes/segundo (exemplo: loop processando stream de dados grande), boxing contínuo pode afetar GC. Nesses casos, considere verificar null diretamente ou usar sentinel value.
Nota: Não faça premature-optimization — só considere isso quando já tiver feito profile e visto que Optional é realmente um gargalo.
7. Tabela resumida: quando usar ou não usar
| Situação | Usar Optional? |
|---|---|
| Valor de retorno do método, pode não existir | ✅ Sim |
| Field em entity/DTO | ❌ Não |
| Parâmetro de constructor/método | ❌ Não |
| Retornar collection (List, Set, Map) | ❌ Não — retorne collection vazia |
| Variável local no método para evitar null check repetido | ⚠️ Depende — considere, às vezes if/else é mais claro |
8. Conclusão
Optional é uma ferramenta boa para deixar claro o contrato da API e evitar NullPointerException na camada de chamada, mas só tem valor quando usado no lugar certo: valor de retorno do método. Evite transformá-lo em field, parâmetro, ou wrapper para collection — nesses lugares Optional só bagunça o código sem resolver nada. Ao escrever código novo, pergunte a si mesmo: “Optional aqui ajuda quem chama a tratar de forma mais clara, ou é apenas null embrulhado?” — a resposta dirá se deve manter ou remover.