function outter(){
var title = 'coding everybody';
return function(){
alert(title);
}
}
inner = outter();
inner();
위의 예시에서 inner()는 outter()의 지역변수 title에 접근할 수 있는데, return 문으로 outter()의 실행이 종료된 이후에도 inner()는 outter()의 변수에 접근할 수 있다. 클로저(Closure)란 이와같이 내부함수가 외부함수의 지역변수에 접근 할 수 있고, 외부함수는 외부함수의 지역변수를 사용하는 내부함수가 소멸될 때까지 소멸되지 않는 특성을 의미한다.
public class ClosureTest {
private Integer b = 2;
private Stream<Integer> calculate(Stream<Integer> stream, Integer a) {
return stream.map(t -> t * a + b);
}
public static void main(String... args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> result = new ClosureTest()
.calculate(list.stream(), 3)
.collect(Collectors.toList());
System.out.println(result); // [5, 8, 11, 14, 17]
}
}
자바에서의 클로저는 람다 클로저로 람다 표현식이 범위를 둘러싼 변수(enclosing scope)를 참조할 때 생성된다. 위의 예제에서는 calculate 메서드에서 map 메서드를 참조하고 있으며, map의 인자로 들어간 람다는 외부 변수인 a와 b를 참조하고 있다.
이 때 a와 b는 자바 컴파일러에 의해 final로 취급되기 때문에 calculate 메서드 안에서 이 a 또는 b의 값을 변경하려고 할 경우 컴파일 에러가 난다.
※ 람다 안에서 사용되는 enclosing scope의 지역 변수는 final 또는 effectively final이어야 한다.
※ 자바 8에서는 effectively final이라는 개념을 도입해 해당 변수가 변경되지 않았다고 컴파일러가 판단하면, 해당 변수를 final로 해석한다.
int n = 0;
final int k = n; // With Java 8 there is no need to explicit final
Runnable r = () -> { // Using lambda
int i = k;
// do something
};
n++; // Now will not generate an error
r.run(); // Will run with i = 0 because k was 0 when the lambda was created
그러므로 람다 안에서 변화하는 변수를 사용해야 한다면 위와 같이 final로 복사본을 선언한 뒤, 그 복사본을 이용해야 한다.
Java에서 람다 클로저를 사용할 때 기억해야 하는 점이 있는데 Java의 람다는 true closure를 지원하지 않는다는 것이다. 자바의 람다는 인스턴스화 된 환경에서의 변화를 확인할 수 있는 방식으로 생성할 수 없으며, 변화를 확인하고 싶다면 class를 이용해야 한다.
// 컴파일 에러
public IntUnaryOperator createAccumulator() {
int value = 0;
IntUnaryOperator accumulate = (x) -> { value += x; return value; };
return accumulate;
}
위의 코드는 컴파일 할 수 없기 때문에 class를 생성하여 아래의 코드로 고칠 수 있는데, 이 코드는 functional하고 stateless해야 한다는 IntUnaryOperator 인터페이스의 디자인 계약을 파기한다는 문제가 있다.
public class AccumulatorGenerator {
private int value = 0;
public IntUnaryOperator createAccumulator() {
IntUnaryOperator accumulate = (x) -> { value += x; return value; };
return accumulate;
}
}
위의 코드를 인텔리제이에서 실행해보았다.
(IntUnaryOperator 인터페이스는 처음 사용해 봐서 잘 사용한 건지 모르겠다.)
public static void main(String[] args) {
AccumulatorGenerator ag = new AccumulatorGenerator();
IntUnaryOperator iuo = ag.createAccumulator();
int result = iuo.applyAsInt(3);
System.out.println("result: " + result);
System.out.println("value: " + ag.value);
int result2 = iuo.applyAsInt(5);
System.out.println("result2: " + result2);
System.out.println("value: " + ag.value);
}
// result: 3
// value: 3
// result2: 8
// value: 8
final로 취급되어야 할 지역 변수의 값이 변해버렸다. 만약 이 Closure가 functional object를 받는 내장 함수에게 전달되면 충돌을 일으킬 가능성이 높아진다. 그러므로 Java에서 변경 가능한 상태를 캡슐화하고 싶다면 아래와 같은 일반 클래스와 메서드로 구현하는 것이 좋다.
public class Accumulator {
private int value = 0;
public int accumulate(int x) {
value += x;
return value;
}
}
(a) -> a.isFactor(); // Lambda
() -> b.isFactor(); // Closure
Lambda 중 외부 변수를 참조하는 람다를 Closure라고 부를 수 있을 것 같다. 위의 람다로 표현된 코드에서 a라는 매개변수를 참조하고 있는 람다식은 람다, 외부 변수인 b를 참조하고 있는 람다식을 (완벽하지는 않지만) Closure라고 부를 수 있다.
Source
우와.. 클로저 정리 잘해놓으셨네요👍 저는 클로저 학습하다가 이해가 잘 안가서 내일 토론 이후에 다시 학습해보기로 했어요ㅜㅜ