2014-07-14 1 views
9

나는 약간 내가 자바 프로그램에서했던 실제 구현에서 추출 된 다음 코드를 가지고 :왜이 경우 람다에서 변수를 참조 할 수 없습니까?

여기
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); 
String line; 
while ((line = bufferedReader.readLine()) != null) { 
    String lineReference = line; 
    runLater(() -> consumeString(lineReference)); 
} 

내가 line를 사용하려고하면, 람다 표현 참조 복사본을 사용할 필요가 내가 얻을 : 람다 표현식에서 참조

지역 변수가 최종 또는 그것은 나에게 오히려 어색한 것 같다

효과적으로 최종해야합니다 모든 나는이에 할 객체에 대한 새로운 참조를 얻는 것을 수정하십시오. 이것은 컴파일러가 스스로 알아낼 수도있는 것입니다.

그래서 난 단지 다른 곳 루프의 할당을 얻고으로 line이 여기 효과적으로 최종 말할 것입니다.

누구나이 문제에 대해 더 자세히 설명하고 왜 정확히 여기에 왜 필요하며 왜 컴파일로 해결할 수 없는지 설명 할 수 있습니까? 당신은 람다 식의 line 대신 lineReference을 사용하는 경우

+2

[BufferedReader :: lines] (http://docs.oracle.com/javase/8/docs/api/java/io/BufferedReader.html#lines--)을 사용하지 않는 이유는 무엇입니까? – nosid

+0

@nosid 열어 둘 수있는 스트림에서 작동합니까? – skiwi

+2

"final"또는 "actually final"은 값이 한 번 할당되고 이후에는 변경되지 않음을 의미합니다. 'line'은 여러 번 지정됩니다 (새 행을 읽을 때마다 한 번). "실질적으로 최종적인"의미가 무엇인지에 대해 잘못 생각 했어야합니다. 할당 된 소스 코드의 * 장소 *가 아니라 프로그램 실행 중에 다시 할당 할 수 있는지 여부입니다. – ajb

답변

20

그래서 난 단지 다른 곳 루프의 할당을 얻고으로 line이 여기 효과적으로 최종 말할 것입니다.

아니요, 변수 수명 기간 동안 모든 루프 반복마다 새 값이 할당되기 때문에 최종 결과가 아닙니다. 이것은 최종의 정반대입니다.

다음과 같이 표시됩니다. '람다 식으로 참조되는 로컬 변수는 최종적이거나 유효해야합니다.'. 나에게는 다소 어색한 것처럼 보인다.

이것을 고려하십시오. 람다를 runLater(...)으로 전달 중입니다. 람다가 마침내 실행될 때 line의 값을 사용해야합니까? 람다가 생성되었을 때의 값 또는 람다가 실행되었을 때의 값?

lambda는 람다 실행시 현재 값을 사용합니다. 변수의 사본을 만들지는 않습니다. 자,이 규칙은 실제로 어떻게 구현됩니까? line가 static 필드 인 경우 람다 캡처에 대한 상태가 없기 때문에

  • , 그것은 간단합니다. 람다는 다른 코드처럼 필드의 현재 값을 필요할 때마다 읽을 수 있습니다.

  • line이 인스턴스 필드 인 경우 공평하게입니다. 람다는 각 람다 개체의 비공개 숨겨진 필드에있는 개체에 대한 참조를 캡처 할 수 있으며이를 통해 line 필드에 액세스 할 수 있습니다.

  • (그것이 당신의 예에서와 같이) line이 방법 내에서 지역 변수 인 경우, 이것은 쉽게 갑자기 하지입니다. 구현 레벨에서 람다 식 is in a completely different method을 사용하면 외부 코드가 한 메서드 내에서만 존재하는 변수에 대한 액세스를 공유하는 쉬운 방법이 없습니다. 있도록 홀더 객체 모두에서 참조 될 수있다 (예를 들면, 1 소자 어레이로)

로컬 변수에 대한 액세스를 가능하게하기 위해, 컴파일러는 일부 숨겨진 가변 홀더 객체 내로 변수 박스해야 둘러싼 메소드와 람다를 사용하여 둘 모두에 변수에 대한 액세스 권한을 부여합니다.

비록 그 솔루션이 기술적으로 작동 할지라도, 달성되는 동작은 여러 가지 이유 때문에 바람직하지 않습니다. 홀더 객체를 할당하면 지역 변수에 코드를 읽지 않아도 부 자연스러운 성능 특성이 나타납니다. (로컬 변수를 사용하는 람다를 정의하는 것은 메서드 전체에서 변수의 속도를 느리게 할 수 있습니다.) 그보다 더 나쁜 경우 람다가 실행될 때마다 간단한 코드에 미묘한 경쟁 조건이 도입됩니다. 귀하의 예제에서 람다가 실행될 때까지 루프 반복 횟수가 발생할 수 있거나 메서드가 반환되었을 수 있으므로 line 변수에 값이 있거나 정의되지 않은 값이있을 수 있으며 거의 ​​확실하게 값을 가질 수 없습니다. 원했어. 따라서 실제적으로 여전히은 변하지 않는 lineReference 변수를 필요로합니다! 유일한 차이점은 컴파일러가 그렇게 할 필요가 없다는 것입니다. 따라서 컴파일러가 깨진 코드를 작성할 수 있습니다. 람다는 궁극적으로 다른 스레드에서 실행될 수 있기 때문에 로컬 변수에 미묘한 동시성과 스레드 가시성 복잡성이 생겨 로컬 변수에 volatile 수식자를 허용하는 언어가 필요하고 다른 것도 방해하게됩니다.

그래서 람다가 지역 변수의 현재 변경 값을 보게되면 많은 호들갑이 생길 것입니다 (그리고 필요하다면 you can do the mutable holder trick manually 이후 이점이 없습니다). 대신 변수가 단순히 final (또는 실질적으로 최종) 일 것을 요구함으로써 kerfuffle 전체에 no라고 말합니다. 그런 식으로 람다는 람다 생성 시간에 로컬 변수의 값을 포착 할 수 있으며, 어떤 것도 존재할 수 없다는 것을 알고 있기 때문에 변경을 감지하는 것에 대해 걱정할 필요가 없습니다.

이 그것을 허용하지 이유입니다, 컴파일러는 그 자체로 알아낼 수있는 일

그것은 그것을 알아낼 않았다이다. lineReference 변수 은 컴파일러에게는 전혀 도움이되지 않으며 각 람다 개체의 생성 시간에 람다에서 사용하기 위해 line의 현재 값을 쉽게 캡처 할 수 있습니다. 그러나 람다가 변수에 대한 변화를 감지하지 못하기 때문에 (위에서 설명한 이유 때문에 비실용적이며 바람직하지 않음), 필드 캡처와 현지인 캡처 간의 미묘한 차이는 혼란 스러울 수 있습니다. "최종 또는 실질적으로 최종적인"규칙은 프로그래머의 이익을위한 것입니다. 변수를 변경하지 못하게하여 왜 변수가 람다 내에 나타나지 않는지 궁금하지 않게합니다.여기에 해당 규칙없이 무슨 일이 일어날 지의 예 :

람다 내에서 참조 로컬 변수 (효과적으로) 최종 경우 혼란이 사라지고
String field = "A"; 
void foo() { 
    String local = "A"; 
    Runnable r =() -> System.out.println(field + local); 
    field = "B"; 
    local = "B"; 
    r.run(); // output: "BA" 
} 

.

코드에서 lineReference은 최종적으로입니다. 그 값은 으로 정확히 한번으로 지정되고 각 루프 반복의 끝에서 범위를 벗어나기 전에 람다에서 사용할 수 있습니다. line 지금 각 루프 반복의 끝에서 범위를 벗어나 있기 때문에

for (;;) { 
    String line = bufferedReader.readLine(); 
    if (line == null) break; 
    runLater(() -> consumeString(line)); 
} 

이 허용된다

루프 본체 내부에 line를 선언하여 가능한 루프의 대안 배열이있다. 각 반복에는 사실상 한 번만 할당 된 신선한 변수가 있습니다. (그러나 낮은 수준에서 변수는 여전히 동일한 CPU 레지스터에 저장되므로 반복적으로 "생성"되고 "파괴"되어야하는 것처럼 아닙니다. 내 말은 변수를 선언 할 때 추가 비용이 들지 않는다는 것입니다. 이런 식의 루프이므로 괜찮습니다.)


참고 :이 모든 것은 람다에만 국한된 것이 아닙니다. 또한 메서드 내에서 어휘 적으로 선언 된 모든 클래스에 동일하게 적용되며 람다가 규칙을 상속합니다.

주 2 : 람다 생성시 사용하는 변수의 값을 항상 캡처하는 규칙을 따르면 람다가 더 간단 할 것이라고 주장 할 수 있습니다. 그렇다면 람다가 람다 생성 시간 이후 변경된 사항을 보지 못한다는 것이 잘 입증 될 것이므로 필드와 지역 주민간에 행동에 차이가 없으며 "최종 또는 실질적으로 최종"규칙이 필요하지 않습니다. 그러나이 규칙은 그 자체의 추함을 가질 것입니다. 예를 들어, λ 필드 내에서 액세스 된 x 인스턴스 필드의 경우, x (최종 값 캡처 x) 및 this.x (최종 값 캡처, this, 해당 필드 x 변경) 동작 간에는 차이가 있습니다. 언어 디자인은 어렵습니다.

+0

니스, +1. 이는 익명의 내부 클래스에 대한 can not-mutate-captured-nonfinal-localals 문제와 밀접한 관련이 있습니다. Java 8의 유일한 차이점은 이러한 변수가 실제로 최종적인 경우 '최종'이라고 선언 할 필요가 없다는 것입니다. (이것은 람다와 AIC 모두에게 적용됩니다.) 이와 같이, "실질적으로 최종적인"규칙은 단지 의미 론적 변화가 아니라 편리함에 불과합니다. [Jon Skeet] (http://stackoverflow.com/a/4732617/1441122) 및 [gnat] (http://stackoverflow.com/a/17736622/1441122)에서이 답변에 대한 자세한 내용이 나와 있습니다. –

1

, 당신은 당신의 runLater 방법 line에 의해 참조 문자열에 consumeString을 실행할 것 람다 식에 전달 될 것이다.

그러나 새 줄을 지정하면 line이 계속 변경됩니다. 마침내 람다 식에서 반환 된 함수 인터페이스의 메서드를 실행하면 line의 현재 값을 얻고 consumeString을 호출 할 때 사용합니다. 이 시점에서 line의 값은 람다 식을 runLater 메서드에 전달한 경우와 같지 않습니다.

+0

당신은'line'과'lineReference'를 혼합했습니다. 'line'은 새로운 라인이 할당 될 때 변경되는 유일한 변수입니다. 'lineReference'는 루프 본문 안에서 선언되고 각 루프 반복문에는 논리적으로 "새로운"lineReference' 변수가 있기 때문에 수명 동안 변경되지 않습니다. – Boann

+0

@Boann 당신 말이 맞아요. 나는 그것을 놓쳤다. 감사! 지금은 해결되었지만 당신의 대답은 여전히 ​​내 것보다 훨씬 낫습니다. +1 – Eran

관련 문제