CodeGym /Cursos /JAVA 25 SELF /Closures (fechamentos) em Java: particularidades

Closures (fechamentos) em Java: particularidades

JAVA 25 SELF
Nível 48 , Lição 3
Disponível

1. Introdução a closures

Closure — é uma função (ou objeto-função) que não apenas usa seus próprios parâmetros, mas também “se lembra” das variáveis do contexto ao redor onde foi criada. Simplificando, se uma expressão lambda ou uma classe anônima dentro de um método usa variáveis desse método — ela se torna um closure.

Exemplo simples

public class ClosureDemo {
    public static void main(String[] args) {
        String greeting = "Olá, ";
        Runnable sayHello = () -> System.out.println(greeting + "mundo!");
        sayHello.run(); // Saída: Olá, mundo!
    }
}

Aqui a lambda “capturou” a variável greeting do método externo e a usa dentro de si. Isso é um closure!

2. Variáveis efetivamente final: o que são e por quê?

Em Java, expressões lambda (e classes anônimas) podem usar apenas variáveis do método externo que são declaradas como final ou que não mudam após a inicialização. Tais variáveis são chamadas de efetivamente final.

Por que isso?

Essa restrição está relacionada ao fato de que uma expressão lambda pode ser chamada após o término do método em que foi criada. Se a variável pudesse mudar, haveria confusão: qual versão da variável usar? Para evitar surpresas, Java exige que a variável seja imutável (ou ao menos pareça imutável).

Exemplo: uso correto

public static void main(String[] args) {
    int number = 42; // number — efetivamente final
    Runnable r = () -> System.out.println(number);
    r.run(); // 42
}

Exemplo: tentativa de alterar a variável após o uso

public static void main(String[] args) {
    int number = 42;
    Runnable r = () -> System.out.println(number);
    number++; // ERRO: a variável number deve ser final ou efetivamente final
    r.run();
}

O compilador exibirá um erro: Variable used in lambda expression should be final or effectively final.

Efetivamente final — é... a variável à qual se atribui exatamente uma vez e não é alterada depois. Não é obrigatório escrever final; o compilador percebe sozinho.

3. Como as expressões lambda capturam variáveis?

Quando você escreve uma lambda que usa uma variável externa, o Java “empacota” essa variável junto com a lambda. Mesmo que o método onde a lambda foi criada já tenha terminado, a variável não desaparece — ela vive dentro do closure.

Ilustração: a lambda “se lembra” da variável

public static Runnable createGreeter(String name) {
    // name — parâmetro do método; ele será capturado pela lambda
    return () -> System.out.println("Olá, " + name + "!");
}

public static void main(String[] args) {
    Runnable greeter = createGreeter("Vasya");
    greeter.run(); // Olá, Vasya!
}

Aqui, a variável name já não existe mais na pilha do método main, mas greeter ainda “se lembra” do seu valor.

Como isso é implementado por baixo dos panos?

O compilador Java cria um objeto auxiliar especial (também chamado de “capture/display class”), que armazena todas as variáveis capturadas. A expressão lambda se torna um objeto que tem uma referência a esse “container” com as variáveis.

4. Exemplo de closure: retornando uma função que usa uma variável

Vamos escrever uma função que retorna uma lambda usando uma variável do seu contexto:

import java.util.function.IntSupplier;

public class ClosureFactory {
    public static IntSupplier makeAdder(int x) {
        // x — é capturado pela lambda
        return () -> x + 10;
    }

    public static void main(String[] args) {
        IntSupplier adder = makeAdder(5);
        System.out.println(adder.getAsInt()); // 15
    }
}

Aqui a variável x já “saiu” da pilha do método, mas a lambda ainda pode usá-la.

5. Por que não é permitido modificar variáveis capturadas?

public static void main(String[] args) {
    int base = 100;
    Runnable printer = () -> System.out.println(base);
    base = 200; // ERRO!
    printer.run();
}

O compilador não permitirá isso. Se pudéssemos mudar base, ficaria incerto qual versão da variável usar dentro da lambda: a antiga ou a nova? Por isso, o Java proíbe a alteração de variáveis locais capturadas por uma lambda.

O que pode ser usado em uma lambda?

  • Variáveis locais que não mudam após a inicialização (efetivamente final).
  • Campos de classe (tanto static quanto não estáticos) — eles podem ser alterados, mas isso já é outro mecanismo (acesso ao estado do objeto), e não captura de variável local.

6. Comparação com classes anônimas

Antes do surgimento das expressões lambda em Java, era possível fazer closures usando classes anônimas:

public static void main(String[] args) {
    String word = "Java";
    Runnable r = new Runnable() {
        public void run() {
            System.out.println(word);
        }
    };
    r.run(); // Java
}

As regras são as mesmas: a variável word deve ser final ou efetivamente final.

Diferença: escopo de this

  • Em uma classe anônima, this se refere à instância da própria classe anônima.
  • Em uma expressão lambda, this se refere ao objeto externo (por exemplo, a instância atual da classe).

7. Closures e campos de classe

Se a lambda usa um campo de classe, isso não é “captura de variável local” no sentido estrito — o campo está sempre acessível e pode ser alterado.

public class Counter {
    private int count = 0;

    public Runnable makeCounter() {
        return () -> {
            count++;
            System.out.println("Contador: " + count);
        };
    }

    public static void main(String[] args) {
        Counter c = new Counter();
        Runnable r = c.makeCounter();
        r.run(); // Contador: 1
        r.run(); // Contador: 2
    }
}

8. Erros comuns e particularidades de closures em Java

Erro nº 1: tentar modificar uma variável capturada por uma lambda. O erro mais comum é tentar modificar uma variável local depois que ela foi usada em uma lambda. O compilador informará: Variable used in lambda expression should be final or effectively final.

Erro nº 2: esperar que a variável esteja “congelada”. Em Java, a variável capturada — quando se trata de um campo de classe — não é uma cópia do valor, mas uma referência ao original. Se o campo da classe mudar, a lambda verá o novo valor. Mas variáveis locais em lambdas devem ser apenas efetivamente final.

Erro nº 3: esperar que a lambda crie um novo escopo para this. Em uma lambda, this é o objeto externo (a classe que a contém). Em uma classe anônima, this é a própria classe anônima.

Erro nº 4: uso de objetos mutáveis. Se você captura uma referência a um objeto mutável (por exemplo, uma lista), pode alterar seu conteúdo dentro da lambda, mesmo que a própria variável seja efetivamente final:

public static void main(String[] args) {
    java.util.List<String> list = new java.util.ArrayList<>();
    Runnable r = () -> list.add("Hello");
    r.run();
    System.out.println(list); // [Hello]
}

Aqui a variável list não muda (não fazemos list = ...), mas o objeto dentro dela muda.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION