Intellij가 알아서 고쳐주던 아래 에러에 대해서 좀 더 깊숙히 알고자 정리하는 글입니다.
Variable used in lambda expression should be final or effectively final
에러에 대한 결론부터
- 람다 실행시에 실행되던 메소드의 스택 영역에 저장되는 외부 변수들에 대해서는 참조만 가능하고 값 변경은 불가
→ final or effectively final 변수만 사용 가능한 이유 - 외부 Reference type 변수에 대한 변경은 힙 메모리 데이터를 변경하는것이기 때문에 가능
(변수 초기화 같은 스택 참조값 변경은 동일하게 불가) - 위 현상들의 이유는 람다가 실행될때 람다 캡처링이 일어나면서 발생하는 현상
- 람다 캡처링이 일어나게되면
- 람다의 새로운 스택을 생성
- 실행되고있던 메소드의 스택 데이터들을 그대로 가져와서 람다의 스택에 그대로 복사 (call by value)
- 근데 왜 변수 값 변경은 불가하지?
- 람다의 스택에 있는 Primitive type 변수를 변경하게 되면 실행되던 기존 메소드의 스택에는 변경한 값을 반영할 방법이 없음
- Reference type변수들은 데이터를 변경해도 참조를 변경하는것이 아니라 힙 영역의 데이터를 변경하는것이기 때문에 가능
- 람다 캡처링이 일어나게되면
예제를 이용한 테스트
예제
람다를 사용하여 임의의 컬렉션을 순회하면서 count를 증가시키고, 컬렉션의 첫번째 요소 remove
위 예제를 해결하기 위해 아래와 같은 코드를 작성하게 됩니다.
(size()등의 함수는 사용하지 못한다는 가정입니다.)
public void lambdaTest(){
int listCount = 0;
List<Integer> numList = Arrays.asList(1, 2, 3);
numList.stream().forEach(integer -> {
listCount++;
numList.remove(0);
});
System.out.println(listCount);
}
하지만 이 코드는 compile단계에서부터 error가 발생하게 됩니다.
Variable used in lambda expression should be final or effectively final
labmda 안에서는 final변수, effectively final변수만 사용 가능합니다.
람다의 메모리 구조로 인한 현상
왜 이런현상이 발생하는지 람다가 실행될때의 메모리 구조를 떠올려보면 알 수 있습니다.
일단 아래 코드까지 메모리가 어떤 구조일지 그려보겠습니다.
public void lambdaTest(){
int listCount = 0;
List<Integer> numList = Arrays.asList(1, 2, 3);
일반적으로 생각할 수 있는 현재까지의 메모리 구조입니다.
위 메모리 구조에서 람다가 실행될 경우 메모리구조는 아래와 같이 구성되게 됩니다.
public void lambdaTest(){
int listCount = 0;
List<Integer> numList = Arrays.asList(1, 2, 3);
numList.stream().forEach(integer -> {
listCount++;
numList.remove(0);
});
새로운 스택이 생겨났습니다.
lambda가 실행될때 lambda는 새로운 스택을 생성해서 사용하게 됩니다.
- 이 새로운 스택은 기존 실행되던 스택의 변수들을 그대로 똑같이 들고 있게 됩니다.
- 여기서 주의할점은 이 새로운 스택은 같은 변수를 똑같이 들고만 있는다는 점입니다.
- 들고만 있기 때문에 변수값에 대한 수정은 할 수 없게됩니다.
- 하지만 어쨋든 스택에 있기 때문에 변수에 대한 참조는 가능합니다.
lambda내에서 값을 수정할 수 없는 final변수, effectively final변수만 사용 가능한 이유는 위 제약 사항때문입니다.
그리고 위 제약사항이 발생한 근본적인 원인은 lambda가 실행되면서 람다 캡처링이 발생하기 때문입니다.
람다 캡처링
람다 캡처링 : 기존 메소드의 스택 변수들에 대해 참조가 가능하도록 람다의 스택에 복사하는 과정
람다의 스택에 똑같이 복사해와서 사용하는것이기 때문에 람다의 스택에서 값을 변경하더라도 기존 스택의 값은 변경할수 없게 됩니다.
이런 이유로 컴파일 단계에서 error를 나타내주고 있습니다.
근데 컬렉션이 설정된 변수는 수정 가능한데?
numList.remove(0);
생각해보면 위 코드도 변수를 수정하는건데 에러가 발생하지 않습니다.
그 이유는 그려진 메모리 구조에서도 알 수 있듯이 컬렉션의 경우에는 애초에 힙 메모리에 있는 데이터이기 때문입니다.
람다 캡처링이 일어날때 해당 힙 메모리 참조 주소값만을 그대로 가져오게 됩니다.
그러니 해당 컬렉션의 내용을 수정한다고해도 참조하고있는 힙 메모리의 데이터를 변경하는것이지 이 참조 주소값을 변경하는것은 아니기 때문입니다.
만약 아래와 같이 null로 변경하게 된다면 참조 주소값을 변경하는것이기 때문에 listCount 를 변경할때와 같은 에러가 발생하게 됩니다.
int listCount = 0;
List<Integer> numList = Arrays.asList(1, 2, 3);
numList.stream().forEach(integer -> {
numList = null;
});
'Java' 카테고리의 다른 글
@SpringBootTest / @DataJpaTest 차이점 과 JPA 영속성 컨텍스트 (5) | 2021.09.12 |
---|---|
람다(Lambda)와 익명 클래스 :: Lambda can be replaced with method reference (0) | 2021.08.01 |
Mockito @Mock @MockBean @Spy @SpyBean 차이점 (27) | 2020.09.13 |
@NoargsConstructor(AccessLevel.PROTECTED) 와 @Builder (5) | 2020.07.31 |
[Java8 비동기] CompletableFuture (0) | 2020.07.27 |