Optional: Uso Correto e os Próprios Avisos da API
Objective
Optional<T> é um objeto contêiner que ou contém um único valor não nulo, ou não contém nada. Sua única razão de existir é tornar "isso pode não produzir um valor" parte da assinatura de um método, em vez de um fato escondido no Javadoc (ou em lugar nenhum) que quem chama precisa lembrar de checar com um null-check. Optional<User> findByEmail(String email) diz a toda chamada, em tempo de compilação, que um resultado vazio é um resultado normal; User findByEmail(String email) retornando null não diz nada, e a NullPointerException aparece três frames de distância de onde o null nasceu. O que Optional não é é uma caixa genérica para valores anuláveis: o próprio Javadoc do JDK afirma que ele é "primariamente destinado ao uso como tipo de retorno de método," e a equipe do JDK tem sido explícita ao dizer que campos, parâmetros de método e elementos de coleção nunca foram o alvo. O restante deste conceito trata de por que essas regras existem, em vez de apenas afirmá-las, além dos métodos com os quais as pessoas confiavelmente erram. Você já viu Optional de passagem como o tipo de retorno de findFirst() em Stream API Fundamentals — aquele conceito o trata como "o que uma operação terminal específica acontece de retornar"; aqui ele é o assunto principal.
Use Cases
- Uma busca que pode legitimamente não encontrar nada —
Optional<User> findByEmail(String)— onde retornarnullé uma armadilha e lançar uma exceção seria errado, porque "não encontrado" é um resultado comum, não uma falha. - Forçar quem chama a tomar uma decisão explícita no ponto de chamada (
orElse,orElseThrow,ifPresentOrElse) em vez de deixar umnullviajar silenciosamente por três camadas antes de explodir em algum lugar sem relação. - Encadear uma sequência de transformações que devem interromper limpo assim que algo estiver ausente (
map/flatMap), em vez de escrever uma pirâmide de checagens de null aninhadas. - Fornecer um fallback caro de calcular, mas só computá-lo quando de fato necessário (
orElseGet). - Reduzir uma coleção de buscas possivelmente ausentes apenas aos resultados presentes, via
flatMap(Optional::stream).
Deep Dive
Construindo um Optional: três fábricas, duas das quais lançam ou não exceção
Não existe construtor público — Optional é criado apenas através de métodos de fábrica estáticos, e escolher o errado é o primeiro erro comum.
javaOptional<String> a = Optional.of("hello"); // value MUST be non-null
Optional<String> b = Optional.ofNullable(maybe); // null -> empty, non-null -> present
Optional<String> c = Optional.empty(); // explicitly nothingof(value) chama Objects.requireNonNull internamente: é uma afirmação de que você sabe que o valor está presente, e falha ruidosamente se você estiver errado. ofNullable(value) é a fábrica que aceita uma entrada possivelmente nula e converte silenciosamente null em Optional.empty(). Use of quando o valor for um literal ou algo já validado; use ofNullable na fronteira onde uma API legada que retorna null te entrega alguma coisa.
Uma referência Optional nunca deveria ser null em si mesma. Retornar null de um método declarado para retornar Optional<T> é estritamente pior do que retornar null de um método declarado para retornar T, porque a quem chama foi dito que não precisa de um null-check:
javapublic Optional<User> findByEmail(String email) {
User u = repo.lookup(email);
return u == null ? null : Optional.of(u); // WRONG — never return a null Optional
}
public Optional<User> findByEmail(String email) {
return Optional.ofNullable(repo.lookup(email)); // right
}O anti-padrão isPresent() / get()
Esse é o formato que aparece em quase todo código base que acabou de adotar Optional, e é aquele que vale nomear explicitamente:
java// BROKEN — Optional buys you nothing here
public String displayName(Long id) {
Optional<User> user = findById(id);
if (user.isPresent()) {
return user.get().name();
} else {
return "anonymous";
}
}Compila, funciona, e é exatamente a checagem manual de null que Optional foi introduzido para eliminar — com uma alocação extra de objeto por cima. O tipo tem um método para exatamente isso:
java// FIXED
public String displayName(Long id) {
return findById(id).map(User::name).orElse("anonymous");
}
// or, when the fallback is expensive to build
public String displayName(Long id) {
return findById(id).map(User::name).orElseGet(() -> loadDefaultNameFromConfig());
}A regra prática: se get() (ou orElseThrow() usado como substituto de get()) aparece dentro de um bloco if (x.isPresent()), existe um combinador que expressa a mesma coisa sem desembrulhar.
orElse(x) vs. orElseGet(supplier) — eager vs. lazy
Essa é a única armadilha do mundo real mais comum de todas. orElse recebe um valor, então seu argumento é avaliado antes mesmo de orElse ser chamado — a avaliação eager comum de argumentos do Java. O valor é então descartado se o Optional acabou estando presente. orElseGet recebe um Supplier, que só é invocado quando o Optional está de fato vazio.
javastatic String expensiveDefault() {
System.out.println("expensiveDefault() ran");
return "default";
}
public static void main(String[] args) {
Optional<String> present = Optional.of("actual value");
String a = present.orElse(expensiveDefault());
// prints: expensiveDefault() ran <- ran anyway, result discarded
// a == "actual value"
String b = present.orElseGet(Main::expensiveDefault);
// prints nothing <- never invoked
// b == "actual value"
}Com uma constante simples (orElse(""), orElse(0)) o eagerness não custa nada e orElse fica mais legível. No momento em que o fallback é uma chamada de método — uma leitura em banco, uma consulta de configuração, a construção de um objeto, uma escrita de log — orElse paga esse custo em toda invocação, incluindo o caso, esmagadoramente comum, em que o valor estava presente. Pior, se o fallback tem efeitos colaterais (inserir uma linha padrão, incrementar um contador), orElse os executa mesmo quando nada estava faltando, o que é um bug de correção, não só de performance.
O mesmo par eager/lazy existe para exceções: orElseThrow(IllegalStateException::new) constrói a exceção apenas no caminho vazio, motivo pelo qual não existe um overload orElseThrow(SomeException) que receba um valor.
map e flatMap: encadeando sem desembrulhar
map(fn) aplica fn ao valor contido e reembrulha o resultado — e embrulha com semântica de ofNullable, então um mapper que retorna null produz um Optional vazio em vez de uma NullPointerException. É isso que permite que uma cadeia interrompa em qualquer elo. Compare com a versão em pirâmide de checagens de null:
java// null-check pyramid
public String zipOf(Long userId) {
User user = repo.findById(userId);
if (user != null) {
Address addr = user.getAddress();
if (addr != null) {
String zip = addr.getZip();
if (zip != null) {
return zip;
}
}
}
return "UNKNOWN";
}com a mesma lógica em forma de cadeia:
javapublic String zipOf(Long userId) {
return findById(userId) // Optional<User>
.map(User::getAddress) // Optional<Address> (empty if getAddress() returns null)
.map(Address::getZip) // Optional<String>
.orElse("UNKNOWN");
}flatMap é para o caso em que o próprio mapper já retorna um Optional. Usar map ali produz um Optional aninhado, o que quase nunca é o que você quer:
javaclass User { Optional<Address> getAddress() { ... } }
// BROKEN — double-wrapped
Optional<Optional<Address>> nested = findById(id).map(User::getAddress);
// FIXED — flatMap unwraps one level
Optional<Address> addr = findById(id).flatMap(User::getAddress);filter(predicate) se encaixa na mesma cadeia e transforma um valor presente mas indesejado em um vazio:
javaOptional<User> activeAdmin = findById(id) .filter(User::isActive) .filter(u -> u.role() == Role.ADMIN);
Encerrando uma cadeia: orElseThrow, ifPresent, ifPresentOrElse
orElseThrow() sem argumentos (adicionado no Java 10) é o substituto moderno de get(): comportamento idêntico — NoSuchElementException quando vazio —, mas o nome diz em voz alta que pode lançar exceção, enquanto get() soa como um acessor inofensivo. O overload com supplier escolhe o tipo da exceção:
javaUser u = findById(id).orElseThrow(); // NoSuchElementException: No value present
User v = findById(id).orElseThrow(
() -> new UserNotFoundException("no user " + id)); // your exception, built only when emptyQuando o objetivo é um efeito colateral em vez de um valor, use as formas com consumer em vez de um if:
javafindById(id).ifPresent(user -> auditLog.record(user)); // do nothing when empty
findById(id).ifPresentOrElse( // Java 9+
user -> auditLog.record(user),
() -> auditLog.recordMissing(id)); // Runnable for the empty branchor(supplier) (Java 9) encadeia fallbacks que são eles próprios opcionais, mantendo você dentro do mundo de Optional:
javaOptional<Config> cfg = fromEnv()
.or(this::fromFile)
.or(this::fromDefaults);Optional.stream(): reduzindo uma coleção de buscas
stream() (Java 9) transforma um Optional<T> em um Stream<T> de exatamente zero ou um elemento. Sozinho isso parece inútil; seu propósito é ser usado como um mapper de flatMap, o que descarta os vazios e desembrulha os presentes em um único passo:
javaList<User> found = emails.stream()
.map(this::findByEmail) // Stream<Optional<User>>
.flatMap(Optional::stream) // Stream<User> — empties vanish, presents unwrap
.toList();Compare com a dança pré-Java 9 que ele substitui, que é a mesma lógica escrita três vezes:
javaList<User> found = emails.stream()
.map(this::findByEmail)
.filter(Optional::isPresent)
.map(Optional::get) // safe only because of the filter above — the compiler can't tell
.toList();A forma flatMap(Optional::stream) não tem nenhum get() nela, então não há linha alguma cuja segurança dependa de uma checagem que aconteceu antes no pipeline.
Trade-offs
- O JDK explicitamente não recomenda
Optionalcomo tipo de campo.Optionalnão éSerializable, então um único campoOptionaltorna toda a classe envolvente não serializável pela serialização padrão; também custa uma alocação extra de objeto e um nível de indireção para algo que um campo anulável comum já modela, em cada instância em vez de uma vez por chamada.javaclass Account implements Serializable { private Optional<String> nickname = Optional.of("ace"); // compiles fine } new ObjectOutputStream(out).writeObject(new Account()); // java.io.NotSerializableException: java.util.Optional - Como tipo de parâmetro de método, ele deixa toda chamada pior e ainda não fecha o buraco do null. Chamadas que têm um valor simples precisam envolvê-lo só para te chamar, e nada impede uma chamada de passar
nullcomo a própria referênciaOptional— então o parâmetro ainda precisa de um null-check, exatamente o que ele deveria remover. Um overload ou um parâmetro anulável documentado faz o trabalho sem a cerimônia.javavoid register(String name, Optional<String> nickname) { if (nickname.isPresent()) { ... } // NPE if the caller passed a null Optional } register("ana", Optional.of("ace")); // every caller must wrap register("ana", null); // compiles — the hole is still open List<Optional<T>>é quase sempre um sinal de design ruim. Carregar ausência dentro de uma coleção significa que todo consumidor dessa coleção precisa desembrulhar elemento por elemento; as entradas ausentes quase sempre deveriam ter sido filtradas enquanto a coleção estava sendo construída.javaList<Optional<User>> bad = ids.stream().map(this::findById).toList(); List<User> good = ids.stream().map(this::findById).flatMap(Optional::stream).toList();Optional.of(null)lança exceção imediatamente —ofnão é a fábrica "segura". As pessoas recorrem aofporque é o nome mais curto e assumem que ele trata nulls; éofNullablequem faz isso.javaString maybeNull = System.getenv("NOT_SET"); // null Optional.of(maybeNull); // NullPointerException, thrown right here Optional.ofNullable(maybeNull); // Optional.empty- As especializações primitivas são deliberadamente limitadas.
OptionalInt,OptionalLongeOptionalDoubleexistem para evitar boxing, mas não têmmap,flatMapoufilter— então um resultado primitivo que precisa de mais encadeamento tem que ser boxado de volta comstream().boxed()ou tratado comorElseno final, e o estilo fluente não se mantém.javaOptionalInt count = IntStream.of(1, 2, 3).max(); count.map(n -> n * 2); // does not compile: cannot find symbol - method map(...) Optionalé uma value-based class, então operações de identidade sobre ela não fazem sentido. Comparar doisOptionalcom==, sincronizar em um deles, ou depender do seu identity hash é comportamento não especificado que uma futura JVM tem liberdade de mudar;equalscompara os valores contidos e é a única comparação correta.javaOptional.of("x") == Optional.of("x"); // unspecified — may be false Optional.of("x").equals(Optional.of("x")); // true — the contract you can rely on- Envolver todo valor de retorno em
Optionalé seu próprio tipo de ruído. Um método que genuinamente não pode falhar em produzir um resultado deveria retornar o resultado, não umOptionaldele; e uma equipe que não combinou onde está o limite acaba com pontos de chamada que misturam cadeias deOptional, checagens de null e guardas defensivas deisPresent()para os mesmos dados, o que é mais difícil de ler do que qualquer uma das duas convenções aplicada de forma consistente.