Capa do artigo: Optional em Java: monad e ausência computacional

Optional em Java: monad e ausência computacional

Antes de falar sobre Optional, vale deixar explícito um conceito da programação funcional que aparece por trás do tipo. Esse conceito é o de monad.

Monads em poucas palavras

Uma monad é um padrão que envolve um valor em um contexto e oferece uma forma padronizada de encadear operações sobre esse valor sem precisar "abrir" o contexto a cada passo. O contexto pode ser "um valor que talvez exista", "um valor que pode falhar", "uma lista de valores" ou "um efeito colateral adiado". O que importa é que as operações se compõem de forma previsível e o contexto se preserva ao longo do encadeamento.

Em linguagens como Haskell, monads são parte central do sistema de tipos. Em Java, o conceito aparece de forma implícita. Stream, CompletableFuture e Optional seguem o mesmo padrão. Cada um envolve um valor em um contexto diferente (coleção, computação assíncrona, presença ou ausência) e oferece map e flatMap para encadear transformações sem sair do contexto.

flowchart LR
    subgraph Contexto
        A[valor] --> B[map / flatMap]
        B --> C[novo valor no mesmo contexto]
    end

O que torna o padrão útil na prática é que o código que consome a monad não precisa verificar o contexto a cada operação. A verificação acontece dentro do próprio tipo. Se o Optional está vazio, o map não executa a função. Se o Stream não tem elementos, o pipeline simplesmente não produz resultado. O contexto cuida de si mesmo.

Optional como a monad Maybe

Optional, desde o Java 8, materializa no Java a monad que em outras linguagens costuma ser chamada de Maybe (Haskell) ou Option (Scala, Rust). Um container que ou guarda um valor de tipo T ou representa a ausência desse valor. A presença ou a ausência fazem parte do próprio tipo, e o código que consome um Optional lida com as duas possibilidades por meio da API, em vez de depender de null e de verificações manuais espalhadas.

Optional<Usuario> usuarioOpt = repositorio.buscarPorId(id);

String nomeFormatado = usuarioOpt
    .map(Usuario::getNome)
    .filter(nome -> !nome.isBlank())
    .map(String::toUpperCase)
    .orElse("DESCONHECIDO");

O map aplica uma função ao valor interno apenas quando o Optional está presente. O filter transforma um Optional presente em vazio quando o predicado falha. O orElse resolve o valor final ou fornece um padrão. Nenhum null aparece no fluxo. Quem lê o código vê que a ausência é um caso tratado pela própria estrutura do tipo.

Quando a próxima etapa da composição retorna outro Optional, entra o flatMap. Sem ele, um map que devolve Optional produziria Optional<Optional<T>>. O flatMap achata um nível, mantendo um único Optional no encadeamento.

Optional<Endereco> endereco = usuarioOpt
    .flatMap(Usuario::getEnderecoPrincipal);

O diagrama abaixo resume o fluxo. Se o Optional está vazio, o resultado de qualquer operação continua vazio. Se está presente, a função é aplicada e o resultado segue no encadeamento.

flowchart LR
    subgraph Entrada
        A[Optional vazio]
        B[Optional com valor]
    end

    A --> C[map / flatMap / filter]
    B --> C

    C --> D[Optional vazio]
    C --> E[Optional com valor]

Em termos de teoria das categorias, Optional cumpre as leis que se esperam de uma monad (identidade à esquerda e à direita, associatividade da composição). Para o dia a dia em Java, basta ter em mente que Optional é uma ferramenta funcional de composição que mantém o contexto "valor presente ou ausente" ao longo das operações.

Onde Optional se encaixa

A documentação oficial da plataforma Java é objetiva sobre o uso pretendido.

"Optional destina-se principalmente ao uso como tipo de retorno de método, quando há necessidade clara de representar 'nenhum resultado' e quando o uso de null provavelmente causaria erros. Uma variável cujo tipo é Optional nunca deve ser null em si mesma. Ela deve sempre apontar para uma instância de Optional."
— java.util.Optional, API Note (Java 21)

Brian Goetz, arquiteto da linguagem, reforçou o escopo pretendido de forma direta.

"É claro que as pessoas farão o que quiserem. Mas nós tínhamos uma intenção clara ao adicionar essa feature, e não era ser um tipo Maybe de propósito geral, por mais que muita gente quisesse que fosse. Nossa intenção era fornecer um mecanismo limitado para tipos de retorno de métodos de biblioteca, onde houvesse necessidade clara de representar 'nenhum resultado' e onde usar null para isso fosse muito provável de causar erros."
— Brian Goetz, StackOverflow

Um repositório que busca uma entidade por ID pode retornar Optional<Entidade>. Um serviço que consulta um recurso externo pode retornar Optional<Resposta>. A assinatura do método comunica que o chamador deve considerar o caso em que não há resultado. O mesmo vale para transformações funcionais em que cada etapa pode produzir ausência. APIs de consulta, integrações com sistemas externos e pipelines que dependem de buscas ou parsing são cenários naturais.

public interface ProdutoRepository {
    Optional<Produto> buscarPorSku(String sku);
}

public class CatalogoService {
    public String descricaoFormatada(String sku) {
        return produtoRepository.buscarPorSku(sku)
            .map(Produto::descricao)
            .map(String::trim)
            .filter(d -> !d.isBlank())
            .orElse("Produto sem descrição");
    }
}

Onde Optional não se encaixa

Stuart Marks, engenheiro da Oracle e membro do time do OpenJDK, sistematizou as práticas recomendadas em uma apresentação que se tornou referência. Entre as regras que ele documentou, uma é particularmente relevante aqui.

"Evite usar Optional em campos, parâmetros de métodos e coleções."
— Stuart Marks, Optional: The Mother of All Bikesheds (Devoxx BE, 2016)

O próprio Java desencoraja o uso de Optional como campo. A classe Optional não implementa Serializable. Colocar Optional<Algo> como atributo de uma entidade JPA ou de um DTO que vai para JSON introduz complicações que o tipo não foi desenhado para resolver. Frameworks de serialização e mapeamento objeto-relacional não nasceram pensando em Optional como parte da estrutura persistida.

// Uso problemático: Optional como campo
public class Cliente {
    private final String nome;
    private final Optional<String> telefone; // não serializa, complica JPA e JSON

    // ...
}
// Alternativa direta: null no campo, Optional no retorno
public class Cliente {
    private final String nome;
    private final String telefone; // pode ser null

    public Optional<String> getTelefone() {
        return Optional.ofNullable(telefone);
    }
}

Na segunda abordagem, o campo interno é null quando não há telefone. O Optional aparece apenas no retorno do método, exatamente onde foi desenhado para atuar. O framework de serialização lida com null normalmente, e o consumidor da API recebe um Optional que comunica a possibilidade de ausência.

Optional como parâmetro de método também gera problemas. O chamador precisa decidir se passa Optional.of(valor), Optional.empty() ou (pior) null. O método receptor precisa lidar com todas essas possibilidades. Uma sobrecarga ou um parâmetro nullable tende a ser mais simples.

// Problemático: Optional como parâmetro
public List<Produto> buscar(Optional<String> categoria) {
    // O chamador pode passar null, Optional.empty(), ou Optional.of(...)
}

// Mais direto: sobrecarga
public List<Produto> buscar() { ... }
public List<Produto> buscar(String categoria) { ... }

Para coleções, a regra é ainda mais clara. Uma coleção vazia já representa a ausência de elementos. Envolver uma lista em Optional adiciona uma camada de indireção sem ganho semântico.

Null em Java: quando faz sentido

A existência de Optional não torna null obsoleto. Null continua tendo seu lugar em Java, e entender onde é a chave para evitar tanto o uso excessivo de Optional quanto os NullPointerExceptions que motivaram sua criação.

Null funciona bem como estado interno de um objeto. Campos privados que podem não ter valor são um uso legítimo. O encapsulamento da classe garante que o null não vaze sem tratamento. O getter pode devolver Optional, convertendo o null interno em um tipo explícito na fronteira pública.

Null também funciona como representação de ausência em estruturas que serão serializadas. JSON tem o conceito nativo de campo ausente ou null. JPA mapeia null para colunas nullable. Essas tecnologias foram desenhadas em torno de null, e forçar Optional nesse contexto cria atrito sem benefício.

Onde null causa problemas é no retorno de métodos públicos. Um método que retorna null para indicar "não encontrado" depende de que todo chamador lembre de verificar. Se um chamador esquece, o resultado é NullPointerException em algum ponto distante do código. Optional resolve exatamente esse problema ao tornar a verificação parte do tipo.

// Null no retorno: o chamador pode esquecer de verificar
Produto produto = repositorio.buscarPorSku("ABC-123");
String nome = produto.getNome(); // NullPointerException se não encontrou

// Optional no retorno: a ausência é parte do tipo
Optional<Produto> produto = repositorio.buscarPorSku("ABC-123");
String nome = produto
    .map(Produto::getNome)
    .orElse("Não encontrado"); // ausência tratada na composição

Kotlin e a alternativa no sistema de tipos

Kotlin aborda o mesmo problema de forma diferente. Em vez de um tipo wrapper como Optional, a linguagem incorporou a distinção entre nullable e non-nullable diretamente no sistema de tipos.

"Em Kotlin, o sistema de tipos distingue entre tipos que podem conter null (tipos nullable) e aqueles que não podem (tipos non-nullable)."
— Kotlin Documentation, Null Safety

Uma variável String não aceita null. Uma variável String? aceita. O compilador verifica isso em tempo de compilação. Não há como atribuir null a um tipo non-nullable sem que o compilador rejeite o código.

var nome: String = "Ana"     // não aceita null
var apelido: String? = null  // aceita null

nome = null    // erro de compilação
apelido = null // permitido

O operador de chamada segura ?. permite encadear operações sobre tipos nullable sem risco de NullPointerException. Se o valor à esquerda é null, a expressão inteira avalia para null sem lançar exceção.

"O operador de chamada segura ?. permite lidar com nulabilidade de forma segura em uma forma mais curta. Em vez de lançar um NPE, se o objeto é null, o operador ?. simplesmente retorna null."
— Kotlin Documentation, Null Safety

O operador Elvis ?: fornece um valor padrão quando a expressão à esquerda é null. É o equivalente funcional do orElse de Optional, mas sem a camada do wrapper.

"Se a expressão à esquerda de ?: não é null, o operador Elvis a retorna. Caso contrário, o operador Elvis retorna a expressão à direita."
— Kotlin Documentation, Null Safety

val usuario: Usuario? = repositorio.buscarPorId(id)

val nomeFormatado = usuario
    ?.nome
    ?.takeIf { it.isNotBlank() }
    ?.uppercase()
    ?: "DESCONHECIDO"

Compare com o equivalente em Java.

Optional<Usuario> usuario = repositorio.buscarPorId(id);

String nomeFormatado = usuario
    .map(Usuario::getNome)
    .filter(nome -> !nome.isBlank())
    .map(String::toUpperCase)
    .orElse("DESCONHECIDO");

O resultado é o mesmo. A diferença está no nível em que a solução opera. Kotlin resolve no sistema de tipos. O compilador garante que nenhum tipo non-nullable receba null. Java resolve no nível de biblioteca. Optional é uma classe que envolve o valor e oferece uma API de composição. A garantia em Java depende da disciplina de usar Optional onde cabe, já que null continua existindo na linguagem.

Essa diferença tem consequências práticas. Em Kotlin, o campo de uma data class pode ser String? e o compilador força o tratamento em todo acesso. Em Java, um campo String em um record pode ser null e nada impede o acesso sem verificação. Optional no retorno do getter ameniza o problema na fronteira pública, mas o null interno continua existindo.

Ausência computacional versus estado de domínio

Vale separar dois planos que costumam se confundir quando Optional entra na conversa.

Optional modela o que se pode chamar de ausência computacional. Uma condição que surge durante o processamento. Um método que busca, consulta um repositório ou transforma uma entrada externa pode ou não produzir um resultado. Essa incerteza pertence ao fluxo da computação. Optional representa exatamente esse tipo de incerteza operacional.

No domínio de negócio, a situação é outra. O modelo de domínio descreve estados reais do sistema. Um cliente pode ou não ter telefone cadastrado. Um pedido pode ou não ter cupom aplicado. Um usuário pode ou não ter endereço. Essas situações fazem parte da representação do mundo que está sendo modelado.

Em abordagens orientadas a dados, como discutido nos artigos sobre Java multiparadigma e Data-Oriented Programming, a ausência no domínio costuma ser expressa de forma direta na estrutura do tipo. Uma hierarquia selada torna as variantes explícitas e verificáveis pelo compilador.

public sealed interface Contato
    permits ComTelefone, SemTelefone {}

public record ComTelefone(String numero) implements Contato {
    public ComTelefone {
        if (numero == null || numero.isBlank())
            throw new IllegalArgumentException("Número não pode ser vazio");
    }
}

public record SemTelefone() implements Contato {}
String mensagem = switch (cliente.contato()) {
    case ComTelefone(var numero) -> "Enviar SMS para " + numero;
    case SemTelefone() -> "Notificar por e-mail";
};

Nesse modelo, a ausência de telefone não é null nem Optional.empty(). É um tipo com nome, com semântica e com tratamento exaustivo garantido pelo compilador. A ausência faz parte do vocabulário do domínio. Compare isso com Optional<String> telefone, onde "ausente" e "presente" são os únicos estados possíveis e o nome do conceito se perde dentro de um tipo genérico.

A escolha entre as abordagens depende do contexto. Optional funciona bem quando a ausência é um detalhe operacional do fluxo (busca que pode falhar, parsing que pode não encontrar). A modelagem com tipos funciona melhor quando a ausência é parte do significado do domínio e precisa de tratamento diferenciado.

Conclusão

Optional em Java é uma monad que concretiza o padrão Maybe. Permite composição funcional por meio de map, flatMap e filter, evita null no retorno de métodos e mantém o contexto "presente ou vazio" ao longo do encadeamento. A documentação do Java e os arquitetos da linguagem são claros sobre o escopo pretendido. Optional foi desenhado para retorno de métodos. O uso como campo, parâmetro ou envoltório de coleções fica fora desse desenho e tende a criar mais problemas do que resolve.

Null continua tendo seu lugar. Campos internos, estruturas serializáveis e contextos onde o framework espera null permanecem legítimos. A existência de Optional não invalida null. Ela delimita onde cada um atua com mais clareza.

Kotlin mostra que o mesmo problema pode ser resolvido de forma diferente, com nulabilidade incorporada ao sistema de tipos em vez de tratada por uma classe wrapper. Cada abordagem reflete uma decisão de design da linguagem. Java optou por manter compatibilidade com décadas de código existente e oferecer Optional como ferramenta de composição funcional. Kotlin, sem o peso da retrocompatibilidade, incorporou a segurança contra null no compilador.

Quando a ausência é operacional (buscas, consultas, parsing), Optional encaixa. Quando a ausência faz parte do vocabulário do domínio, tipos selados e modelagem explícita tendem a expressar melhor a intenção. Entender essa distinção é o que transforma Optional de detalhe sintático em escolha de design.

Bibliografia

Brian Goetz. Resposta sobre Optional no StackOverflow. StackOverflow, 2014.

Stuart Marks. Optional: The Mother of All Bikesheds. Devoxx BE, 2016.

Oracle. java.util.Optional (Java 21). Java Platform SE Documentation.

Kotlin Documentation. Null Safety. kotlinlang.org.