Por que as variáveis locais usadas nas lambdas precisam ser finais ou efetivamente finais?
1. Introdução
Java 8 nos dá lambdas e, por associação, a noção de variáveiseffectively final. Você já se perguntou por que variáveis locais capturadas em lambdas precisam ser finais ou efetivamente finais?
Bem, oJLS nos dá uma pequena dica quando diz "A restrição às variáveis efetivamente finais proíbe o acesso a variáveis locais que mudam dinamicamente, cuja captura provavelmente introduziria problemas de simultaneidade." Mas o que isso significa?
Nas próximas seções, vamos nos aprofundar nessa restrição e ver por que o Java a introduziu. Mostraremos exemplos para demonstrarhow it affects single-threaded and concurrent applications, e tambémdebunk a common anti-pattern for working around this restriction.
2. Capturando Lambdas
Expressões lambda podem usar variáveis definidas em um escopo externo. Referimo-nos a esses lambdas comocapturing lambdas. Eles podem capturar variáveis estáticas, variáveis de instância e variáveis locais, mas apenaslocal variables must be final or effectively final.
Em versões anteriores do Java, isso acontecia quando uma classe interna anônima capturava uma variável local para o método que a cercava - precisávamos adicionar a palavra-chavefinal antes da variável local para que o compilador ficasse feliz.
Como um pouco de açúcar sintático, agora o compilador pode reconhecer situações onde, enquanto a palavra-chavefinal não está presente, a referência não está mudando em nada, o que significa que éeffectively final. We could say that a variable is effectively final if the compiler wouldn’t complain were we to declare it final.
3. Variáveis locais na captura de lambdas
Simplificando,this won’t compile:
Supplier incrementer(int start) {
return () -> start++;
}
start é uma variável local, e estamos tentando modificá-la dentro de uma expressão lambda.
A razão básica para que isso não seja compilado é que o lambda écapturing the value of start, meaning making a copy of it. Forçar a variável a ser final evita dar a impressão de que incrementarstart dentro do lambda poderia realmente modificar o parâmetro do métodostart.
Mas, por que ele faz uma cópia? Bem, observe que estamos retornando o lambda do nosso método. Assim, o lambda não será executado até que o parâmetro do métodostart seja coletado como lixo. Java precisa fazer uma cópia destart para que esse lambda fique fora desse método.
3.1. Problemas de simultaneidade
Para se divertir, vamos imaginar por um momento que Javadid permite que variáveis locais de alguma forma permaneçam conectadas a seus valores capturados.
O que devemos fazer aqui:
public void localVariableMultithreading() {
boolean run = true;
executor.execute(() -> {
while (run) {
// do operation
}
});
run = false;
}
Embora isso pareça inocente, ele tem o insidioso problema de "visibilidade". Lembre-se de que cada thread obtém sua própria pilha e, portanto, como podemos garantir que nosso loopwhilesees mude para a variávelrun na outra pilha? A resposta em outros contextos pode ser usandosynchronized blocks ou avolatile keyword.
No entanto,because Java imposes the effectively final restriction, we don’t have to worry about complexities like this.
4. Variáveis estáticas ou de instância na captura de Lambdas
Os exemplos anteriores podem levantar algumas questões se os compararmos com o uso de variáveis estáticas ou de instância em uma expressão lambda.
Podemos fazer nosso primeiro exemplo compilar apenas convertendo nossa variávelstart em uma variável de instância:
private int start = 0;
Supplier incrementer() {
return () -> start++;
}
Mas, por que podemos alterar o valor destart aqui?
Simplificando, é sobre onde as variáveis de membro são armazenadas. Variáveis locais estão na pilha, mas variáveis de membro estão na pilha. Como estamos lidando com memória heap, o compilador pode garantir que o lambda terá acesso ao valor mais recente destart.
Podemos corrigir nosso segundo exemplo, fazendo o mesmo:
private volatile boolean run = true;
public void instanceVariableMultithreading() {
executor.execute(() -> {
while (run) {
// do operation
}
});
run = false;
}
A variávelrun agora está visível para o lambda, mesmo quando é executada em outra thread, pois adicionamos a palavra-chavevolatile .
De modo geral, ao capturar uma variável de instância, podemos pensar nisso como capturar a variável finalthis. De qualquer forma,the fact that the compiler doesn’t complain doesn’t mean that we shouldn’t take precautions, especialmente em ambientes multithreading.
5. Evitar soluções alternativas
Para contornar a restrição de variáveis locais, alguém pode pensar em usar suportes de variáveis para modificar o valor de uma variável local.
Vejamos um exemplo que usa uma matriz para armazenar uma variável em um aplicativo de thread único:
public int workaroundSingleThread() {
int[] holder = new int[] { 2 };
IntStream sums = IntStream
.of(1, 2, 3)
.map(val -> val + holder[0]);
holder[0] = 0;
return sums.sum();
}
Poderíamos pensar que o fluxo está somando 2 para cada valor, masit’s actually summing 0 since this is the latest value available when the lambda is executed.
Vamos dar um passo adiante e executar a soma em outro encadeamento:
public void workaroundMultithreading() {
int[] holder = new int[] { 2 };
Runnable runnable = () -> System.out.println(IntStream
.of(1, 2, 3)
.map(val -> val + holder[0])
.sum());
new Thread(runnable).start();
// simulating some processing
try {
Thread.sleep(new Random().nextInt(3) * 1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
holder[0] = 0;
}
Que valor estamos somando aqui? Depende de quanto tempo nosso processamento simulado leva. If it’s short enough to let the execution of the method terminate before the other thread is executed it’ll print 6, otherwise, it’ll print 12.
Em geral, esses tipos de soluções alternativas são propensas a erros e podem produzir resultados imprevisíveis, portanto, devemos sempre evitá-los.
6. Conclusão
Neste artigo, explicamos por que as expressões lambda só podem usar variáveis locais finais ou efetivamente finais. Como vimos, essa restrição vem da natureza diferente dessas variáveis e como o Java as armazena na memória. Também mostramos os perigos de usar uma solução alternativa comum.
Como sempre, o código-fonte completo dos exemplos está disponívelover on GitHub.