Java: Execução preguiçosa com Lambdas
Outro post curtinho! Esta semana eu peguei um bug interessante que achei bacana compartilhar com vocês. O bug em questão me fez lembrar como lambdas são executados de forma “lazy” no Java.
Vamos para o exemplo?
Execução preguiçosa da forma errada Vamos analisar o seguinte exemplo. Imagine que você tem um stream em Java que você só quer executar dentro de um bloco com lock.
public class LazyStreamExample {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Execução do Stream fora do Supplier
System.out.println("Execução do Stream fora do Supplier");
List<Integer> doubledNumbers =
numbers.stream()
.map(n -> {
System.out.println("Dobro de: " + n);
return n * 2;
})
.collect(Collectors.toList());
writeToFileWithLock("output.txt", () -> doubledNumbers);
System.out.println("Doubled Numbers: " + doubledNumbers);
}
private static void writeToFileWithLock(String filePath, Supplier<List<Integer>> supplier) {
lock.lock();
System.out.println("Lock adquirido!");
try {
List<Integer> result = supplier.get();
String content = result.stream()
.map(String::valueOf)
.collect(Collectors.joining(", "));
Files.write(Paths.get(filePath), content.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
System.out.println("Resultado escrito no arquivo: " + content);
} catch (IOException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("Lock released!");
}
}
}
Se execurtamos o código acima veja o resultado:
Execução do Stream fora do Supplier
Dobro de: 1
Dobro de: 2
Dobro de: 3
Dobro de: 4
Dobro de: 5
Lock adquirido!
Resultado escrito no arquivo: 2, 4, 6, 8, 10
Lock released!
Doubled Numbers: [2, 4, 6, 8, 10]
O nosso código executou fora do nosso lock 😱
Execução preguiçosa do jeito certo!
A correção do problema acima é trivial; basta entendermos que, para usarmos o poder “lazy” dos lambdas, precisamos executar o stream inteiro como parte do Supplier e não em uma variável separada.
Podemos fazer isso encapsulando o código em um método e chamando o método a partir do Supplier OU passando o bloco de código diretamente no Supplier. Vou utilizar a segunda opção aqui:
public class LazyLambdaExampleCorreto {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Execução do Stream fora do Supplier
System.out.println("Execução do Stream fora do Supplier");
writeToFileWithLock("output.txt", () ->
numbers.stream()
.map(n -> {
System.out.println("Dobro de: " + n);
return n * 2;
})
.collect(Collectors.toList()));
}
...
}
O código de escrita em arquivo continua o mesmo. A única coisa que mudamos foi passarmos o stream dentro do próprio supplier. Isso garante que só vamos executar o stream quando o supplier.get()
for invocado.
O nosso novo resultado será:
Execução do Stream fora do Supplier
Lock adquirido!
Dobro de: 1
Dobro de: 2
Dobro de: 3
Dobro de: 4
Dobro de: 5
Resultado escrito no arquivo: 2, 4, 6, 8, 10
Lock released!
Conclusão
Esse é um “erro bobo” que me pegou desprevenido. Naquele momento, tentando manter o código mais legível, eu isolei o meu stream em uma variável. Isso fez com que meu código rodasse fora do lock 😅.
Espero que este texto sirva de lembrete para vocês evitarem o mesmo problema.