W3docs

Programação Funcional em Java

Visão geral dos conceitos de programação funcional em Java — funções de primeira classe, imutabilidade, funções puras e composição.

A parte anterior — Collections Framework — tratava de contêineres: estruturas de dados que armazenam elementos e as operações (add, remove, iterate, sort, binarySearch) que operam sobre eles. Esta parte aborda uma camada diferente do mesmo problema. Em vez de onde os dados vivem, vamos focar em como expressar transformações sobre eles — de forma clara, composicional, sem loops redundantes ou variáveis acumuladoras.

Essa mudança tem um nome. Programação funcional é o estilo em que a computação é expressa como a aplicação de funções a valores, onde as próprias funções são valores de primeira classe e onde os dados normalmente são tratados como imutáveis. Java não foi projetado como uma linguagem funcional — classes, mutação e loops explícitos estão em seu núcleo — mas desde o Java 8 todo programa Java moderno toma emprestado muito do conjunto de ferramentas funcional. Você já escreveu algo assim na parte de coleções: list.sort(Comparator.comparing(Person::name)), map.getOrDefault(k, 0), List.copyOf(source). O que a Parte 12 faz é nomear o estilo explicitamente e fornecer o restante das ferramentas — lambdas, interfaces funcionais, os tipos java.util.function, referências de método, Optional e a API Stream — para que os padrões que você tem imitado se tornem movimentos de primeira classe.

Quatro ideias que definem o estilo

Linguagens puramente funcionais (Haskell, Erlang, F#) levam as quatro ao extremo. Java as aplica com moderação. As quatro ideias:

  1. Funções são valores de primeira classe. Você pode passar uma função como argumento, retornar uma de um método, armazená-la em um campo ou construí-la em tempo de execução.
  2. Funções puras. Uma função pura depende apenas de suas entradas e não altera nada observável no mundo. Dado o mesmo input, retorna o mesmo output. Sem I/O, sem mutação de campos, sem ramificação dependente de tempo.
  3. Imutabilidade por padrão. Estruturas de dados não são modificadas no lugar; transformações retornam novos valores. Referências antigas permanecem válidas.
  4. Composição. Funções maiores são construídas combinando funções menores (f.andThen(g), pred.and(other), cmp.thenComparing(...)), não editando-as.

Nenhuma dessas é específica de Java. São uma forma de pensar que a linguagem agora suporta por meio de lambdas, referências de método, a API Stream e as coleções imutáveis que você acabou de conhecer.

1. Funções como valores

Antes do Java 8, não era possível ter uma variável cujo valor fosse uma função. Era possível passar um objeto cuja classe acontecia de ter um único método — é o que Runnable, Comparator e ActionListener eram — mas a sintaxe era desajeitada:

list.sort(new Comparator<String>() {
  @Override
  public int compare(String a, String b) {
    return a.length() - b.length();
  }
});

O único método era envolvido em uma declaração de classe anônima. O Java 8 introduziu expressões lambda como sintaxe concisa para a mesma ideia:

list.sort((a, b) -> a.length() - b.length());

O lambda é o valor. Ele compila para uma instância de qualquer interface funcional necessária no ponto de chamada (aqui, Comparator<String>). O próximo capítulo trata inteiramente da sintaxe; por ora, o ponto é que funções em Java agora são valores que você pode nomear, armazenar e passar adiante.

2. Funções puras

Uma função pura é aquela cujo valor de retorno depende apenas de seus argumentos e cuja execução não tem efeitos colaterais observáveis. Math.sqrt(2) é pura. System.currentTimeMillis() não é — retorna valores diferentes entre chamadas. list.add(x) não é — ela muta list.

Funções puras são valiosas porque:

  • São fáceis de testar — sem setup, sem mocks, apenas assertEquals(expected, f(input)).
  • São fáceis de paralelizar — duas chamadas puras podem rodar em threads diferentes sem sincronização.
  • São fáceis de armazenar em cache — memoize uma vez, retorna a mesma resposta para sempre.
  • Compõem sem surpresasf(g(x)) faz o que a leitura sugere.

A maioria dos programas reais úteis não é 100% pura (alguém precisa escrever em um banco de dados). A disciplina funcional é tornar a computação central pura e empurrar as partes impuras — I/O, tempo, aleatoriedade, mutação — para as bordas. Streams incentivam isso: um pipeline de operações puras é correto por construção; um impuro (stream().peek(x -> counter++)...) é um campo minado de bugs.

3. Imutabilidade

Você conheceu isso no último capítulo. List.of(...), Set.of(...), Map.of(...) e List.copyOf(...) produzem coleções que não podem ser modificadas. Records (abordados mais tarde) fornecem classes de dados imutáveis:

record Point(double x, double y) {
  Point translated(double dx, double dy) {
    return new Point(x + dx, y + dy);     // returns a NEW Point — does not mutate this
  }
}

Valores imutáveis são inerentemente thread-safe. Nunca apresentam um estado intermediário "rasgado". Podem ser compartilhados livremente sem cópia defensiva. E tornam as funções puras práticas — se os valores não podem mudar, uma função que retorna um deles tem garantia de ser determinística para essa parte do mundo.

4. Composição

Composição é "construir uma função grande a partir de funções pequenas." Em Java, Function, Predicate e Comparator fornecem operadores composicionais:

Function<String, String> trim  = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> clean = trim.andThen(upper);   // trim, then upper

Predicate<Integer> positive = n -> n > 0;
Predicate<Integer> even     = n -> n % 2 == 0;
Predicate<Integer> posEven  = positive.and(even);

Comparator<String> byLength    = Comparator.comparingInt(String::length);
Comparator<String> lengthThenA = byLength.thenComparing(Comparator.naturalOrder());

A API composicional é parte do valor. Você não escreve um auxiliar que recebe dois predicados e os une com && — você escreve a.and(b). O estilo escala: uma transformação de seis etapas pode ser lida de cima para baixo como uma única expressão, em vez de seis loops aninhados com acumuladores intermediários.

O que Java mantém do lado imperativo

Java é multiparadigma. Os recursos funcionais adicionados no Java 8+ convivem com os recursos imperativos que estão lá desde a versão 1.0. Algumas coisas permanecem imperativas por design:

  • Instruções e fluxo de controle. if, for, while, try ainda são os blocos de construção básicos; lambdas não os substituem, eles substituem o boilerplate de classes anônimas.
  • Variáveis locais mutáveis. Dentro de um corpo de método, int sum = 0; for (int x : xs) sum += x; ainda é idiomático.
  • Campos mutáveis quando fazem sentido. Builders, caches e componentes de UI com estado ainda sofrem mutação.

O princípio: use estilo funcional onde ele torna o código mais claro, não como dogma. Um stream().mapToInt(Integer::intValue).sum() puro é mais claro do que um loop manual. Um pipeline de composição de lambdas de seis etapas que ninguém na sua equipe consegue ler não é.

Um exemplo prático: imperativo vs funcional, lado a lado

O programa abaixo computa o comprimento médio das strings não em branco em uma lista, duas vezes. A primeira versão é imperativa — um acumulador mutável, um loop explícito, uma proteção contra divisão por zero. A segunda versão é funcional — um pipeline de stream de operações puras que é lido de cima para baixo. O terceiro trecho constrói valores compostos de Predicate e Function a partir de menores, mostrando a composição em ação.

java— editable, runs on the server

O que aprender com a execução:

  • Ambas as versões calculam a mesma média. A versão imperativa declara dois contadores mutáveis e um corpo de loop; a funcional encadeia cinco operações nomeadas que cada uma descreve o quê, não como.
  • Predicate.and construiu um teste composto (notNull.and(notBlank)) a partir de dois predicados menores — sem necessidade de um novo método auxiliar. Essa é a composição em ação.
  • Function.andThen fez o mesmo para um pipeline produtor de valor: trim então length, expresso como uma Function<String, Integer> composta.
  • Cada operação no stream é pura: String::trim, o lambda s -> !s.isEmpty(), String::length — nenhuma muta estado. Chamar trimmedLen.apply(\" hi \") duas vezes produziu a mesma resposta; essa é a garantia de determinismo que torna as funções puras seguras para memoizar e paralelizar.

O que vem a seguir

O modelo mental está estabelecido: funções são valores, transformações puras se compõem, a imutabilidade libera você de uma classe de bugs. O próximo capítulo, Expressões Lambda em Java, apresenta a sintaxe concreta(params) -> body — que torna esse estilo ergonômico em Java, além das regras sobre captura de variáveis, tipagem por alvo e onde um lambda pode aparecer.

Prática

Prática
Uma função 'pura' no sentido de programação funcional é aquela que...
Uma função 'pura' no sentido de programação funcional é aquela que...
Was this page helpful?