티스토리 뷰
06 : 변수 유효 범위
람다 표현식에 유효한 변수에 접근하고자 할때는 어떻게 할까? 다음 예제를 한번 보자
1 2 3 4 5 6 7 8 9 | public static void repeatMessage(String text, int count){ Runnable r = () ->{ for (int i = 0; i < count; i++){ System.out.println(text); Thread.yield(); } }; new Thread(r).start(); } |
그리고선
reapeatMessage("Hello", 1000);
을 호출 했다. 이제 람다 표현식 내부에 count 와 text 변수를 보자. 이 두 변수는 람다 표현식 내부에서만 정의되어 있음을 명심하자.
생각해보면 람다 표현식의 코드는 repeatMessage 메서드 호출이 리턴하여 파라미터 변수들이 사라지고 한참 후에 실행 될 수도 있따. 그런데 text 와 count는 어떻게 남아 있는 것일까?
이점은 람다표현식의 구조들 다시한번 살펴볼 필요가 있다.
1. 코드블록
2. 파라미터
3. 자유 변수(파라미터도 아니고 코드 내부에도 정의되지 않는 변수)의 값
위의 text와 count는 람다 표현식의 자유변수 이다. 람다 표현식을 나타내는 자료 구조는 이들 변수의 값을 저장해야 한다. 이 경우 람다 표현식이 이들 값을 캡쳐 했다고 말한다. 어떤 구현에서는 람다 표현식을 단일 메서드를 갖춘 객체로 변환하고, 자유 변수들의 값을 해당 객체 인스턴스 변수에 복사한다.
Note : 자유 변수들의 값을 포함하는 코드 블록의 기술 용어는 클로저(Closure)라 말한다.
람다 표현식은 해당 표현식을 감싸고 있는 유효 범위에 있는 변수의 값을 캡처할 수 있다. 자바에는 캡처한 값이 잘 정의 되어 있음을 보장하기 위한 중요한 제약이 있다. 람다 표현식에서는 값이 변하지 않는 변수만 참조 가능하다. 다음은 잘못된 참조를 한 예이다.
1 2 3 4 5 6 7 8 9 10 | public static void repeatMessage(String text, int count){ Runnable r = () -> { while(count > 0) { count--; //오류 : 캡처한 변수는 변경 불가능 System.out.println(text); Thread.yield(); } }; new Thread(r).start(); } |
람다 표현식에서 변수를 변경하는 작업은 스레드에 안전하지 않다. 일변의 병행 작업이 존재 하고, 각 작업에서 공유 카운트를 업데이트 한다면 문제가 생길 것이다.
1 2 3 4 5 | int matches = 0; for(Path p : files){ new Thread(() -> { if (p 가 어떤 프로 퍼티를 포함하면) matches++; // matches를 변경하는 일은 규칙에 어긋난다. }).start(); |
이는 matches++ 연산이 원자적이지 못하기 때문에 동시 스레드에 안전하지 못해 이러한 제약은 불가피 하다.
Note : 이너 클래스 역시 자신을 감싸고 있는 유효 범위에 있는 값들을 캡처 할 수 있다. 자바 8 이전에는 이너 클래스가 final 지역 변수만 접근 가능했다. 이 규칙은 이제 람다 표현식과 일치하도록 완화 되었다.
컴파일러가 모든 동시 접근 오류를 잡아낼것이라는 기대는 버려라. 변경 금지는 오직 지역 변수에만 해당 된다 . 만일 matches가 람다를 감싸고 있는 클래스의 인스턴스 변수 또는 정적 변수라면 오류는 보고되지 않는다.
또 공유 객체 변경은 말이 안 되는것 같지만 규칙에 맞는 일이다. (?)
1 2 3 4 5 | List<Path> matches = new ArrayList(); for(Path p : files) new Thread( () -> { if(p 가 어떤 프로퍼티를 포함한다면) matches.add(p); }).start(); //matches를 변경하는 일이 규칙에 맞지만 안전하진 않다. |
변수 matches는 사실상 final(effectively final)임을 주목하란다. 사실상 final 변수는 초기화 된 후 새론운 값을 전혀 대입받지 않는 변수이므로 matches 변수는 항상 같은 ArrayList 객체를 참조한다. 하지만 해당 ArrayList 객체 내부 데이터는 변경 되기 때문에 여러 스레드에서 add를 호출 하면 결과는 예측 불가능이다.
Multi-threaded safety 는 2장의 스트림과 6장의 컬렉션 부분에서 자세히 다루겠다.
람다 표현식의 몸체는 중첩 블록과 동일한 유효 범위를 가진다. 따라서 중첩 블록과 동일한 이름 충돌 및 가리기 규칙이 적용 된다. 지역 변수와 이름이 같은 파라미터나 다른 지역 변수를 람다 내부에 선언하는 것은 규칙에 어긋난다.
1 2 3 4 | Path first = Paths.get("/usr/bin"); Comparator<String> comp = (first, second) -> Integer.compare(first.length(), second.length()); //오류 : 변수 first 가 이미 정의 |
메서드 내부에서 이름이 같은 두 지역 변수는 보유 할 수 없기 때문에 람다 표현식 내부에도 이런 변수 도입은 불가능 하다.
람다 표현식에서 this 키워드를 사용하면, 결국 해당 람다를 생성하는 메서드의 this 파라미터를 참조한다.
1 2 3 4 5 6 7 | public class Application(){ public void doWork(){ Runnable runner = () -> { ...; System.out.println(this.toString()); ...}; ... } } |
표현식 this.toString()은 Runnable 인스턴스가 아닌, Application 객체의 toString 메서드를 호출 한다. 람다 표현식에서 this 레퍼런스 사용과 관련해 특별한 점은 없다.
람다 표현식의 유효 범위는 doWork 메서드 내부에 중첩되고, 이 메서드 내부의 어느곳 에서도 this의 의미는 같다.
<소스코드>
https://github.com/JayStevency/Java8/blob/master/src/com/java8/variable/VariableTest.java
'Language > Java' 카테고리의 다른 글
[JAVA8] 람다 표현식 (0) | 2017.01.29 |
---|---|
[JAVA8] 람다 표현식 (0) | 2017.01.29 |
[JAVA8] 람다 표현식 (0) | 2017.01.24 |
[JAVA8] 람다 표현식 (0) | 2017.01.18 |
[JAVA8] 람다 표현식 (0) | 2017.01.18 |