Java Optional
Expresse a possível ausência de um valor em Java com Optional e evite NullPointerException por design.
Optional<T> é um contêiner que armazena ou um valor do tipo T ou nada — e indica qual dos dois, no nível do tipo, para que o compilador possa forçá-lo a tratar o caso ausente. Foi adicionado no Java 8 junto com streams, e os dois são projetados para funcionar juntos: findFirst, findAny, min, max, reduce retornam Optional<T> precisamente porque a resposta pode não existir, e a API oferece formas fluentes de continuar calculando sem precisar escrever if (x != null).
Optional não é um substituto de null em todo lugar, e o JDK tem uma opinião definida sobre onde ele pertence. Este capítulo percorre a API do início ao fim, seguido pelos três lugares em que Optional é a escolha errada.
Construindo um Optional
Três construtores, cada um com um significado preciso:
Optional<String> a = Optional.of("hello"); // present; null arg throws NPE
Optional<String> b = Optional.empty(); // absent
Optional<String> c = Optional.ofNullable(maybeNull); // present if non-null, else emptyA distinção importa. Optional.of(x) é a afirmação "este valor definitivamente está aqui" — se você passar null, ele lança NullPointerException imediatamente, o que é o desejado (um bug exposto na origem, não três níveis abaixo). Optional.ofNullable(x) é o adaptador que você usa para envolver uma API legada que retorna null para "ausente."
Você quase nunca constrói um Optional manualmente dentro de um pipeline de stream — terminais como findFirst e Collectors.maxBy os produzem para você.
Verificando se um valor está presente
As duas consultas:
Optional<String> opt = lookup(id);
boolean has = opt.isPresent(); // true if a value is held
boolean none = opt.isEmpty(); // Java 11+ -- the opposite of isPresentVocê verá esses métodos em código de produção, mas geralmente são um sinal de código ruim: a maioria do código que chama isPresent e depois get ficaria melhor com um dos métodos operate-on-it abaixo. Os métodos de consulta servem para código de fronteira onde você realmente precisa de um boolean — uma cláusula de guarda, uma decisão de rota, um ramo de aviso no log.
Lendo o valor com segurança
A forma errada:
String name = opt.get(); // throws NoSuchElementException if emptyopt.get() é a leitura sem verificação. É como você transforma um Optional de volta em um valor e uma exceção em tempo de execução, exatamente o que o tipo deveria prevenir. Use-o somente depois de provar que o optional está presente (ou após findFirst().orElseThrow() de um pipeline onde o resultado vazio seria um bug de programador, não um caso esperado).
As formas corretas, em ordem de preferência:
String name1 = opt.orElse("anonymous"); // default value
String name2 = opt.orElseGet(() -> expensiveDefault()); // lazy default
String name3 = opt.orElseThrow(); // NoSuchElementException
String name4 = opt.orElseThrow(() -> new MyDomainError(id)); // custom exceptionorElse(value)— fornece um padrão. O valor é sempre avaliado, mesmo quando o optional está presente, portanto não passe uma expressão cara.orElseGet(supplier)— fornece um padrão lazily. O supplier só é executado quando o optional está vazio. Use isso para qualquer padrão que custe mais do que um literal.orElseThrow()— lançaNoSuchElementExceptionse ausente. A forma sem argumentos do Java 10+ é o equivalente moderno deopt.get()quando "isso absolutamente deveria estar presente" é a única interpretação sensata no ponto de chamada.orElseThrow(supplier)— lança uma exceção específica do domínio. A forma padrão de traduzir "ausente" em "404 não encontrado."
Transformando o valor — map
Se o optional estiver presente, aplica uma função; caso contrário, permanece vazio:
Optional<String> upper = opt.map(String::toUpperCase);
Optional<Integer> len = opt.map(String::length);A assinatura é Optional<T>.map(Function<T, R>) -> Optional<R>. A função só é executada quando um valor está presente — sem verificação de null, sem if, e sem else. Esta é a operação que torna o Optional valioso: a maioria das cadeias "se não nulo, faça isso; se não nulo, depois faça isso" se reduz a .map(...).map(...).map(...).
Há um caso especial que o JDK trata silenciosamente: se sua função map retornar null (porque encapsula uma API legada que retorna null para "sem resultado"), o Optional resultante é empty() — não Optional.of(null).
Compondo optionals — flatMap
Quando a função de mapeamento ela mesma retorna um Optional, map produziria Optional<Optional<T>>. flatMap achata isso:
record User(String id, Optional<Address> address) {}
record Address(String city) {}
Optional<String> city = userById(id)
.flatMap(User::address) // Optional<Address>
.map(Address::city); // Optional<String>flatMap é a operação que permite encadear várias consultas, cada uma das quais pode falhar, em um único pipeline. Ambos os casos de falha colapsam para Optional.empty() no final, e o consumidor os trata uma única vez com orElse / orElseThrow.
Filtrando — filter
Testa o valor contra um Predicate<T>; retorna o mesmo optional se passar, empty() se não passar:
Optional<String> nonBlank = opt.filter(s -> !s.isBlank());
Optional<Integer> positive = numberOpt.filter(n -> n > 0);Age como um guarda dentro do pipeline optional. Útil quando a pergunta é "tenho um valor, mas é o valor certo para continuar?"
Efeitos colaterais — ifPresent, ifPresentOrElse
Executa código somente quando o valor está presente:
opt.ifPresent(name -> log.info("hello, {}", name));Ou executa um ramo quando presente e outro quando vazio (Java 9+):
opt.ifPresentOrElse(
name -> log.info("hello, {}", name),
() -> log.warn("no name on the request"));Estas são as formas corretas de expressar "faça algo ao passar." Elas substituem completamente o padrão if (opt.isPresent()) { use(opt.get()); }.
Integrando com streams — Optional.stream()
(Java 9+) Transforma um Optional<T> em um Stream<T> de zero ou um elemento:
Stream<String> s = opt.stream();Útil dentro de flatMap em um Stream<Optional<T>>:
List<String> presentCities = userIds.stream()
.map(this::userById) // Stream<Optional<User>>
.flatMap(Optional::stream) // Stream<User> -- empties drop, presents pass through
.map(User::city)
.toList();Isso substitui filter(Optional::isPresent).map(Optional::get) por um único flatMap(Optional::stream). Mesmo resultado, pipeline mais limpo.
or — recorrer a outro Optional
(Java 9+) Se vazio, usa um supplier de outro Optional:
Optional<User> u = primaryLookup(id)
.or(() -> fallbackLookup(id))
.or(() -> Optional.of(User.anonymous()));Lê-se como "tente o primário; se ausente, tente o alternativo; se ausente, use anônimo." Os três são Optional<User>; a cadeia retorna o primeiro não vazio. Diferente de orElse — or mantém o resultado encapsulado; orElse o desencapsula com um padrão simples T.
Especializações primitivas
Existem OptionalInt, OptionalLong, OptionalDouble para resultados primitivos — o que IntStream.max() retorna, por exemplo:
OptionalInt max = nums.stream().mapToInt(Integer::intValue).max();
int hi = max.orElse(0);Eles têm uma API menor — sem map/flatMap/filter — porque ficam na fronteira do mundo primitivo. Use-os para ler resultados de streams primitivos; converta para Optional<Integer> se precisar da API completa.
Onde Optional não pertence
A intenção de design do JDK é restrita: Optional é um tipo de retorno para métodos cuja resposta pode não existir. Ele não é:
- Um tipo de campo. Não escreva
private Optional<String> middleName;. Não éSerializable, custa uma alocação por campo, e um camponullé mais curto e mais claro para "esta entidade não tem nome do meio." A solução correta é um campo não-Optional que pode sernull, com um getter que retornaOptional. - Um parâmetro de método. Não aceite
Optional<String>como argumento. Sobrecarregue o método, ou aceiteStringe documente quenullsignifica ausente. Parâmetros Optional exigem que o chamador encapsule, o que é ruído. - Um elemento de coleção.
List<Optional<T>>é quase sempre uma lista com elementos anuláveis e encapsulamento extra. UseList<T>e filtre os nulos na fronteira, ou useflatMap(Optional::stream)para descartar os ausentes em um pipeline. - Uma forma de evitar todo
null. Java ainda temnullem todo tipo de referência;Optionalserve para o formato de retorno de código que produz valores que podem não existir. Tipos de referência simples são adequados para todo o resto.
A regra mais curta: um Optional saindo de um método é bom design; um Optional entrando em um método está quase sempre errado.
Um exemplo completo: todos os métodos e as regras práticas em código
O programa abaixo constrói um pequeno grafo de usuário/endereço, percorre todos os métodos de Optional, demonstra o tempo de avaliação de orElse vs. orElseGet, a ponte Optional.stream(), e a cadeia or.
O que tirar da execução:
- Os três construtores
of,empty,ofNullablemapeiam para três intenções precisas: definitivamente presente, definitivamente ausente, e adaptador-legado, presente-se-não-nulo.Optional.of(null)lança — e essa é a falha desejada, não um bug a contornar. orElseavaliou seu argumento todas as vezes, mesmo quando o optional estava presente. O supplier deorElseGetsó foi executado quando necessário. UseorElsepara literais baratos eorElseGetpara qualquer coisa que aloque, consulte ou lance.mapeflatMaptornaram toda a cadeiauserById(...).flatMap(User::address).map(Address::city)legível como um único pipeline — sem verificações denull, sem ifs aninhados, e qualquer etapa vazia faz curto-circuito paraOptional.empty()no final.flatMap(Optional::stream)transformou umStream<Optional<User>>em umStream<User>com todos os ausentes descartados de uma vez. Essa é a forma limpa de converter uma lista de consultas "pode falhar" em um stream de sucessos.OptionalInté o que terminais de streams primitivos comoIntStream.findFirstretornam. Tem sua própria pequena API (getAsInt,orElse,ifPresent) e existe para que pipelines primitivos nunca precisem encaixotar.- A regra de uso incorreto apareceu implicitamente:
User.addressera um campoOptional<Address>— tudo bem porque o exemplo queria demonstrar a API, mas em código de produção o campo seria umAddresspossivelmente nulo com um getterOptional<Address> address()fazendo o encapsulamento.
O que vem a seguir
A Parte 12 cobriu o vocabulário funcional do início ao fim: interfaces funcionais, lambdas, referências de método, os built-ins, o pipeline de stream, cada fonte, cada intermediário, cada terminal, coletores, execução paralela e, finalmente, Optional como a expressão em nível de tipo da ausência. O próximo capítulo, Java Predicate Interface, faz um zoom em uma única interface funcional — Predicate<T> — e a álgebra de combinadores (and, or, negate, isEqual, not) que permite montar predicados sem precisar escrever a lógica boolean manualmente. A partir daí, a parte continua com Function, Consumer/Supplier, e a família de operadores binários — uma interface por capítulo, cada uma com o mesmo formato de exemplo trabalhado que você viu aqui.