Cap 15: Java Funcional¶
🌉 Trazendo ideias funcionais para uma linguagem imperativa¶
Retomando o Capítulo 1: Java não foi projetada para ser uma linguagem híbrida, e seu núcleo continua sendo orientado a objetos baseado em classes. Mas, principalmente a partir do Java 8 (2014), a linguagem incorporou de forma consistente conceitos centrais do paradigma funcional:
- Dados imutáveis e isolamento de efeitos colaterais — não no sentido estrito de proibi-los (Java continua permitindo mutação livremente), mas de oferecer ferramentas para isolar onde eles acontecem.
- Funções como valores de primeira classe — o que possibilita funções de alta ordem como
map,filterereduce, no mesmo sentido amplo discutido no Capítulo 1.
λ Expressões Lambda, Interfaces Funcionais e Method References¶
Antes do Java 8, "passar comportamento como argumento" exigia sempre uma classe anônima implementando uma interface com um único método — verboso mesmo para uma operação trivial:
list.reduce(new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer t, Integer u) {
return t * u;
}
}).ifPresent(new Consumer<Integer>() {
@Override
public void accept(Integer t) {
System.out.println(t);
}
});
Com notação lambda, o mesmo código:
Isso só é possível porque BinaryOperator<T> e Consumer<T> são interfaces funcionais — interfaces com exatamente um método abstrato, conceito já visto no Capítulo 8. Uma lambda é, tecnicamente, uma implementação anônima e concisa de uma interface funcional; o compilador infere qual interface implementar a partir do contexto de uso.
O java.util.function fornece as interfaces funcionais mais comuns, reutilizadas por toda a biblioteca padrão:
| Interface | Método abstrato | Uso típico |
|---|---|---|
Function<T, R> |
R apply(T t) |
transformar um valor em outro |
Predicate<T> |
boolean test(T t) |
testar uma condição |
Consumer<T> |
void accept(T t) |
consumir um valor (efeito colateral) |
Supplier<T> |
T get() |
fornecer um valor, sem argumentos |
BinaryOperator<T> |
T apply(T a, T b) |
combinar dois valores do mesmo tipo |
Quando a lambda apenas chama um método já existente, uma method reference é ainda mais concisa:
list.forEach(x -> System.out.println(x)); // lambda
list.forEach(System.out::println); // method reference — mesmo efeito, mais direto
🌊 Java Stream API¶
A Stream API, também introduzida no Java 8, aplica esse repertório funcional ao processamento de coleções — reencontrando, de forma mais explícita, a comparação entre estilo imperativo e declarativo do Capítulo 1:
List<String> nomes = List.of("Ana", "Bruno", "Carla", "Davi", "Elisa");
// imperativo
List<String> resultado = new ArrayList<>();
for (String nome : nomes) {
if (nome.length() > 4) {
resultado.add(nome.toUpperCase());
}
}
// declarativo, com Stream
List<String> resultado2 = nomes.stream()
.filter(nome -> nome.length() > 4)
.map(String::toUpperCase)
.toList(); // Java 16+; antes disso, era .collect(Collectors.toList())
Uma Stream é uma sequência de operações encadeadas, dividida em duas categorias:
- Intermediárias (
filter,map,sorted,distinct...) — retornam outra Stream, permitindo encadeamento; são preguiçosas (lazy): nada é executado até que uma operação terminal seja chamada. - Terminais (
collect,toList,forEach,reduce,count,sum...) — disparam de fato o processamento e produzem um resultado (ou efeito).
double media = List.of(8.5, 7.0, 9.2, 6.5).stream()
.mapToDouble(Double::doubleValue)
.average()
.orElse(0.0);
Map<Boolean, List<String>> aprovados = List.of("Ana:8", "Bruno:5", "Carla:9").stream()
.collect(Collectors.partitioningBy(s -> Double.parseDouble(s.split(":")[1]) >= 7));
Stream não é sempre a melhor escolha
Streams brilham em pipelines de transformação — filtrar, mapear, agregar. Para um laço simples com efeitos colaterais óbvios (por exemplo, atualizar um contador externo a cada iteração), um for tradicional continua sendo mais claro e, geralmente, tão eficiente quanto. Prefira Streams quando elas deixam a intenção mais legível — não como substituição automática de todo for.
❓ Optional: modelando "pode não haver valor"¶
Um problema recorrente em Java — e em muitas linguagens com referências nulas — é NullPointerException, já citado no Capítulo 1 como um dos fatores que reduzem confiabilidade. Desde o Java 8, Optional<T> oferece um jeito explícito de modelar "esse método pode não ter um resultado", sem depender de null:
public Optional<Usuario> buscarPorId(String id) {
Usuario u = repositorio.get(id); // pode ser null internamente
return Optional.ofNullable(u);
}
Optional<Usuario> resultado = buscarPorId("u1");
// em vez de checar null manualmente:
if (resultado != null && resultado.get() != null) { /* ... */ }
// o idiomático com Optional:
String nome = resultado
.map(Usuario::getNome)
.orElse("desconhecido");
resultado.ifPresentOrElse(
u -> System.out.println("encontrado: " + u.getNome()),
() -> System.out.println("usuário não encontrado")
);
Optional não é imposto pelo compilador
Diferente do sistema de tipos de Kotlin (String? vs. String) ou Swift (String?), Optional em Java é uma convenção de biblioteca, não uma garantia do compilador — nada impede que alguém retorne null em vez de Optional.empty(), ou chame .get() sem checar .isPresent() antes (o que lança NoSuchElementException, na prática recriando o mesmo problema que Optional tenta evitar). O ganho real de Optional é de design de API: o tipo de retorno já comunica, explicitamente, que o valor pode faltar — quem chama o método é lembrado disso pela própria assinatura, mesmo sem imposição do compilador.
🔀 Conectando com o capítulo de exceções: uma via alternativa a try/catch¶
Retomando o que ficou em aberto no Capítulo 10: Optional cobre bem o caso "valor pode não existir", mas não carrega informação sobre por que faltou. Para modelar falhas com uma explicação, sem lançar exceções, um padrão comum (embora não nativo do Java, diferente de linguagens como Kotlin ou Elixir) é definir um tipo Resultado próprio usando sealed interface e record (Capítulo 3):
public sealed interface Resultado<T> permits Sucesso, Falha {}
public record Sucesso<T>(T valor) implements Resultado<T> {}
public record Falha<T>(String motivo) implements Resultado<T> {}
static Resultado<Double> dividir(double a, double b) {
if (b == 0) {
return new Falha<>("divisão por zero");
}
return new Sucesso<>(a / b);
}
Resultado<Double> r = dividir(10, 0);
String mensagem = switch (r) {
case Sucesso<Double> s -> "resultado: " + s.valor();
case Falha<Double> f -> "erro: " + f.motivo();
};
Isso não substitui exceções — continua sendo a ferramenta certa para condições verdadeiramente excepcionais e não-locais — mas ilustra bem como os recursos vistos ao longo deste livro (interfaces sealed, pattern matching, generics) se combinam para trazer, de forma idiomática, uma técnica típica de linguagens funcionais para dentro de Java moderno.
📝 Atividades¶
- Reescreva, usando Stream, um laço
forque soma apenas os números pares de umaList<Integer>. - Implemente um método
Optional<String> primeiroMaiorQue(List<String> lista, int tamanho)que retorna a primeira string com comprimento maior quetamanho, ouOptional.empty()se nenhuma existir. - Explique, com um exemplo, por que
Optional<T>não deveria ser usado como tipo de um atributo de classe ou como tipo de parâmetro de método — apenas como tipo de retorno (essa é, inclusive, a recomendação oficial da equipe do Java). - Implemente o tipo
Resultado<T>deste capítulo e use-o para modelar uma validação de cadastro que pode falhar por múltiplos motivos (nome vazio, e-mail inválido, idade negativa).
🔗 Para saber mais¶
- Documentação oficial: Java Stream API
- JEP 335 / documentação sobre
Optional - Pesquise sobre o tipo
Resultem Kotlin e o tipoEitherem Elixir/Haskell, para comparar com a modelagem deResultado<T>proposta aqui.