Lambda表达式是Java 8引入的重要特性,主要用于函数式编程和简化代码。然而Lambda表达式的使用也有一些限制,比如:无法直接修改外部定义的局部变量。

一、问题分析

请看下面例子:

int count = 0;
List<String> list = Arrays.asList("a", "b", "c");
list.forEach(s -> {
    count++; // 这行代码有问题吗?
    System.out.println(s);
});

答案:不能编译通过。编译错误信息:

Local variable count defined in an enclosing scope must be final or effectively final。

很明显,代码中存在编译错误,原因就是Lambda表达式捕获的局部变量必须是finaleffectively final。那么什么是effectively final,这样限制又有什么好处呢?

Java 8+中,局部变量如果没有被修改,即使没有显式声明为final,编译器也会自动将其视为final,这就是"effectively final"(即事实上不可变)。Lambda表达式只能访问其作用域内的finaleffectively final的局部变量。这样设计的原因:

  • 线程安全: Lambda可能在另一个线程中执行,如果允许修改局部变量,会导致数据竞争和不一致。
  • 变量生命周期: 局部变量在方法执行完毕后会被销毁,而Lambda可能在方法返回后仍被使用(如异步执行)。

我们可以这样理解count是局部变量存在栈中,属于方法线程,而Lambda在运行时可能会开启一个新的线程。因为两个线程执行的生命周期不一样,count销毁时,Lambda可能还在执行,如果允许修改,很可能引发各种各种问题。

二、解决方案

1.使用对象属性(实例变量)

public class Example {
    private int count = 0;  // 实例变量,不限制修改
    
    public void processList() {
        List<String> list = Arrays.asList("a", "b", "c");
        list.forEach(s -> {
            count++;  // 可以修改实例变量
            System.out.println(s);
        });
        System.out.println("Count: " + count);
    }
}

实际上,就是用全局变量来代替局部变量,可能会导致不必要的开销。

2.使用数组(技巧性方案,不推荐)

int[] count = {0};  // 数组引用是effectively final,但数组内容可修改
list.forEach(s -> {
    count[0]++;  // 可以修改数组元素
    System.out.println(s);
});
System.out.println("Count: " + count[0]);  // 输出: Count: 3

一种hack方法,因为数组引用是final,但数组内容可修改。虽然这样可以编译,但破坏了线程安全,不推荐。

3.使用AtomicInteger(推荐,线程安全)

import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger count = new AtomicInteger(0);
list.forEach(s -> {
    count.incrementAndGet();  // 原子操作,线程安全
    System.out.println(s);
});
System.out.println("Count: " + count.get());  // 输出: Count: 3

原子变量是Java并发包(java.util.concurrent.atomic)中提供的一组类,用于在多线程环境下进行原子操作的变量。它们提供了无锁的线程安全操作,底层基于CAS(Compare-And-Swap)​ 机制实现。

4.避免在Lambda中修改,改用其他方式

// 使用Stream计算元素数量
List<String> list = Arrays.asList("a", "b", "c");
long count = list.stream().count();  // 正确的方式获取数量
System.out.println("Count: " + count);

// 或者使用reduce
int result = list.stream()
    .map(s -> 1)  // 将每个元素映射为1
    .reduce(0, Integer::sum);  // 求和
System.out.println("Total: " + result);

三、使用不限

我们虽然不可以直接修改外部的局部变量,但使用时没问题的。

 List<String> list = Arrays.asList("a", "b", "c");
 list.forEach(s -> {
     System.out.println(s+":"+count);//直接使用时没问题的
 });

更多推荐