ecsimsw

왜 inner class, Lambda는 effectively final만 접근할 수 있을까. 본문

왜 inner class, Lambda는 effectively final만 접근할 수 있을까.

JinHwan Kim 2021. 2. 12. 02:40

Variable 'i' should be final or effectively final

자바에서 람다식과 inner class에서는 final 변수 또는 effectively final 변수만 접근된다. 

충분히 가능할 것만 같은 코드가 왜 컴파일 에러를 만드는지, effectively final 변수는 뭔지 정리해보려고 한다.

public static void testLamda(String[] args) {
    int i = 0;
    Runnable testExpression = () -> i++;
}

// Variable used in lambda expression should be final or effectively final

 

람다 캡쳐링

아래 코드를 한번 보자.

class test {
    public static void main(String[] args) {
        List<Car> cars = Arrays.asList(new Car(), new Car(), new Car());

        Supplier<Integer> generator = getExpression();

        for (Car car : cars) {
            car.setNumber(generator.get());
        }
    }

    private static Supplier getExpression() {
        int i = 1;
        return () -> i;
    }
}

class Car {
    int number;

    public void setNumber(int number) {
        this.number = number;
    }

    public void printNumber() {
        System.out.println(number);
    }
}

자동차를 세 대 만들어서 리스트에 넣고, 넘겨받은 generator으로 각 차들의 number에 getExpression 메서드의 i를 넣어줬다.

 

이 말이 이상하지 않는가? getExpression 메소드의 변수 i는 ()->i 라는 식을 반환하고 스택 프레임이 삭제되면서 소멸될 텐데 어떻게 그 이후에 자동차에 그 값을 넣을 수 있는지 궁금하다. 

 

이게 가능한 이유는 람다식이 사실은 i를 참조하고 있는 것이 아닌, i가 갖고 있던 값을 현재 람다식이 동작하고 있는 스레드의 스택에서 들고 있기 때문이다. 이것을 람다 캡쳐링이라고 한다.

 

이 람다 캡쳐링 덕분에 스택이 서로 다른 아닌 다른 멀티 스레드 작업에서도 식을 넘기고 사용할 수 있는 것이다.

 

Final 또는 Effectively final 

Effectively final은 'final틱'한 변수를 말한다. 값이 재정의되지 않는 변수 말이다.

 

조금 더 힌트가 필요하다면 아래 코드를 한번 보자. 이번에는 getExpression() 코드를 이렇게 바꿔 봤다. 

private static Supplier getExpression() {
    int i = 1;
    return () -> i++;
}

이 코드는 컴파일 에러를 낸다. 알고 보니 당연해 보일 수도 있겠다.

 

i 참조가 아닌 값을 복사하는 람다 캡쳐링의 원리로 람다식이 동작하는데, 어떻게 변하는 값을 복사할 수 있겠나. 그렇기 때문에 람다식은 변하지 않는 값, final이나 effectively final 값을 요구하는 것이다.

 

Inner class도 마찬가지

Inner class도 마찬가지다. 아래 코드의 setCarsNumber처럼 메소드가 종료되고 index가 소멸되어도 자동차들의 number는 유지될 수 있도록 람다식의 캡쳐링처럼 변수 참조가 아니라 값을 복사한다.

 

Anonymous inner classes require final variables because of the way they are implemented in Java. An anonymous inner class (AIC) uses local variables by creating a private instance field which holds a copy of the value of the local variable. The inner class isn’t actually using the local variable, but a copy.

 

그래서 변하지 않은 값을 요구하는 것이다.

class test {
    public static void main(String[] args) {
        List<Car> cars = new ArrayList<>();
        setCarsNumber(cars);
    }

    private static void setCarsNumber(List<Car> cars) {
        int index = 0;
        for (int i = 0; i < 3; i++) {
            cars.add(new Car() {
                @Override
                public void setNumber(int number) {
                    this.number = index++;
                }
            });
        }
    }
}

 

그럼 스택이 아닌 다른 영역에 저장하는 값이면?

그렇다면 상관없다. 다른 스레드에서도, 또는 해당 스택 프레임이 사라져도 그 값을 참조할 수 있다면 상관없다. 

class test {
    public static void main(String[] args) {
        List<Car> cars = Arrays.asList(new Car(), new Car(), new Car());
        setCarsNumber(cars);

        cars.stream()
                .forEach(Car::printNumber);;
    }

    private static void setCarsNumber(List<Car> cars) {
        MyInteger myInteger = new MyInteger(1);
        cars.stream()
                .forEach(car->car.setNumber(myInteger.value++));
    }
}

class MyInteger{
    public int value = 1;

    public MyInteger(int value){
        this.value = value;
    }
}

위 코드는 에러를 발생하지 않는다.

 

Comments