문제의 역사:
자바 8 이전에는 내부 클래스나 익명 클래스에서 외부 범위의 변수를 사용하려면 이러한 변수가 반드시 final로 선언되어야 했습니다. 자바 8에서는 요구 사항이 완화되어, 이제 변수는 실제로 변경되지 않는 경우 final로 선언되지 않아도 됩니다 (effectively final).
문제:
람다 표현식과 내부 클래스는 외부 블록의 변수에 대한 클로저를 사용합니다. 그러나 이러한 변수가 값을 변경하면 혼란과 잘못된 동작이 발생하며, 어떤 값을 사용할지 이해할 수 없습니다.
해결 방법:
컴파일러는 변수가 내부 클래스나 람다에서 사용될 수 있도록 허용하지만, 이는 effectively final일 때만 가능합니다. 즉, 초기화 이후 변경되지 않아야 하며, 명시적으로 final로 선언되어 있지 않더라도 가능합니다.
코드 예제:
public void demo() { int x = 10; Runnable r = () -> System.out.println(x); // x는 effectively final r.run(); }
x를 변경하려고 하면:
public void demo() { int x = 10; x = 20; // 이제 x는 effectively final이 아님 Runnable r = () -> System.out.println(x); // 컴파일 오류 }
주요 특징:
링크가 effectively final이라면 변경 가능한 객체를 사용할 수 있는가?
네, 링크가 변경되지 않는다면 객체가 해당 링크를 통해 변경되는 것은 허용됩니다. 예를 들어,
List<String> list = new ArrayList<>(); list.add("A"); Runnable r = () -> System.out.println(list.get(0)); // OK list = new ArrayList<>(); // 만약 그렇다면, 컴파일 오류
변수를 final로 선언할 수 있지만, 객체의 내용을 여전히 변경할 수 있는가?
네. final은 링크에 적용되며 객체의 내용에는 적용되지 않습니다. 링크를 통해 객체의 상태를 변경하는 것은 허용됩니다. 예를 들어,
final List<Integer> nums = new ArrayList<>(); nums.add(5); // OK nums = new ArrayList<>(); // 오류
메서드 매개변수(파라미터)를 람다에서 사용할 수 있는가?
네, 이들이 또한 effectively final이라면 사용 가능합니다. 즉, 초기화 이후 메서드 내에서 변경되지 않아야 합니다.
메서드에서 변수들이 람다 생성 후 우연히 재할당되면, 프로그램이 컴파일되지 않고 원인을 찾는 데 시간이 소요됩니다.
장점:
단점:
개발자가 AtomicInteger(또는 다른 홀더 객체)를 통해 증가 가능한 값을 사용하여 링크가 아닌 값을 저장하므로 람다가 커지더라도 올바르게 작동할 수 있게 합니다.
장점:
단점: