Open sean-k1 opened 2 years ago
익명 클래스 내부에서 this 키워드는 익명 클래스의 인스턴스를 가르킨다.
하지만 람다 내부에서 this 키워드는 람다를 감싼 바깥 클래스의 인스턴스를 가르킨다.
public class Outer {
public void thisAC(int a, int b) {
new Comparator<>() {
@Override
public int compare(Object o1, Object o2) {
System.out.println("익명 클래스의 this : " + this.getClass());
return 0;
}
}.compare(a, b);
}
public void thisLambda(int a, int b) {
IntBinaryOperator lambda = (int x, int y) -> {
System.out.println("람다의 this: " + this.getClass());
return Integer.compare(x, y);
};
lambda.applyAsInt(a, b);
}
public static void main(String[] args) {
var outerClass = new Outer();
outerClass.thisAC(1, 2);
outerClass.thisLambda(1, 2);
}
}
익명 클래스의 this : class com.kakaoent.item42.Outer$1
람다의 this: class com.kakaoent.item42.Outer
익명 클래스 내부에서는 새로운 Scope가 생성된다.
따라서 익명 클래스 외부에 선언된 변수와 동일한 이름을 가진 변수를 익명 클래스 내부에서 새롭게 선언한다면, 외부에서 선언한 변수는 덮어씌워진다(Shadowing).
int outer = 0;
new Comparator<>() {
@Override
public int compare(Object o1, Object o2) {
int outer = 1; //익명 클래스 내부에서 새로운 Scope 생성
System.out.println(outer) // 1
return 0;
}
};
하지만 람다 내부의 Scope는 람다를 감싸고 있는 Scope와 동일하다.
int outer = 0;
IntBinaryOperator lambda = (int x, int y) -> {
int outer = 1; // 바깥 Scope를 그대로 사용하기 때문에 변수명이 동일하다는 컴파일 에러 발생
return Integer.compare(x, y);
};
익명 클래스나 람다 내부에서 참조하는 지역 변수를 변경하려고 하면 컴파일 에러가 발생한다.
컴파일 에러를 보면, 람다식 내부에서 사용하는 변수는 반드시 final이거나 effective final(final로 선언하지 않았지만 변경되지 않는 변수)이어야 한다고 나온다.
간단한 예시를 통해 왜 람다식 내부에서 사용하는 변수는 final 혹은 effective final이어야 하는지 알아보자.
다음은 String 타입 메시지를 처리하는 AsyncMessageProcessor
클래스이다.
이 클래스는 메시지를 처리할 때 메시지와 람다식을 함께 전달받는다.
그리고 새로운 워커 쓰레드
를 생성해 전달받은 람다식을 실행시킨다.
public class AsyncMessageProcessor {
public void asyncProcess(String message, Consumer<String> consumer/**람다식**/) {
ExecutorService executorService = Executors.newSingleThreadExecutor();//새로운 쓰레드 생성
executorService.submit(() -> {
System.out.println("람다식이 실행되는 쓰레드 -> " + Thread.currentThread().getName());
consumer.accept(message);//람다식 실행
/**
실제로 실행되는 내용
System.out.println(message.toUpperCase());
**/
});
}
public static void main(String[] args) {
var messageProcessor = new AsyncMessageProcessor();
System.out.println("람다를 전달하는 쓰레드 -> " + Thread.currentThread().getName());
messageProcessor.asyncProcess("hello", s -> {
System.out.println(s.toUpperCase());
});
}
}
main 메서드에서 실행한 결과를 보면 람다식을 전달하는 쓰레드와, 람다식을 실행하는 쓰레드가 서로 다른 것을 확인할 수 있다.
람다를 전달하는 쓰레드 -> main
람다가 실행되는 쓰레드 -> pool-1-thread-1
HELLO
이제 이 코드를 살짝 바꿔보자.
메시지가 잘 처리되었는지 확인하기 위해 boolean 타입의 isProcessed
라는 지역 변수를 선언하고, 전달하는 람다식이 실행될 때 true로 변경되게끔 작성해 보자.
public class AsyncMessageProcessor {
public void asyncProcess(String message, Consumer<String> consumer) {
ExecutorService executorService = Executors.newSingleThreadExecutor();//새로운 쓰레드 생성
executorService.submit(() -> {
System.out.println("람다가 실행되는 쓰레드 -> " + Thread.currentThread().getName());
consumer.accept(message);//람다 실행
/**
실제로 실행될 내용
System.out.println(message.toUpperCase());
isProcessd = true;
**/
});
}
public static void main(String[] args) {
var messageProcessor = new AsyncMessageProcessor();
boolean isProcessed = false;
System.out.println("람다를 전달하는 쓰레드 -> " + Thread.currentThread().getName());
messageProcessor.asyncProcess("hello", s -> {
System.out.println(s.toUpperCase());
isProcessed = true; <--- 컴파일 에러가 나지 않는다면 무슨 문제가 발생할까?
});
}
}
만약 이 코드에서 컴파일 에러가 발생하지 않는다고 가정하면 어떤 문제가 발생할까?
지역 변수 isProcessed
는 main 쓰레드
의 스택
에 저장되어 있다.
하지만 람다식이 실행되는 쓰레드는 pool-1-thread-1
이다(이 쓰레드를 워커 쓰레드라고 하자).
스택은 쓰레드 별로 할당되기 때문에, main 쓰레드
의 스택에 저장된 지역 변수를 워커 쓰레드
에서는 참조할 수 없다.
이런 문제를 해결하기 위해 자바는 지역변수의 값을 복사해 람다식 내부에서 참조할 수 있게끔 전달한다.
이 시점에서 main 쓰레드
의 스택에 있는 isProcessed
가 변경되거나 워커 쓰레드
의 람다식 내부에서 isProcessed
의 값이 변경되는 경우, 복사된 변수값과 실제 변수값이 일치하지 않는 문제가 발생한다.
이런 문제를 피하기 위해 람다식 내부에서 참조하는 지역변수는 반드시 final이거나 effective final이어야 한다.
반면 인스턴스 변수
는 힙 영역
에 할당되기 때문에 다른 쓰레드에서 실행되는 람다의 내부에서도 인스턴스 변수는 자유롭게 변경할 수 있다.
하지만 이런 경우엔 반드시 동기화 작업을 해 주어야 한다.
서론
함수타입을 표현 할때 추상 메서드를 하나만 담은 인터페이스를 사용했다. 이런 인터페이스의 인스턴스를 함수객체라고 부른다.
JDK 1.1 등장하면서 함수 객체를 만드는 주요 수단은 익명 클래스가 되었다.
제목에도 나와있듯이 익명클래스 -> 람다를 사용하라 하는 이유를 알아보자
본문
익명클래스의 최대 단점은 코드가 너무 길어져 자바에는 함수형 프로그래밍에 적합하지 않는다.
자바 8 에서는 추상 메서드 하나 짜리 인터페이스는 특별한 의미를 인정받아 특별한 대우를 받게 되었다.
지금은 함수형 인터페이스라 부르는 이 이너테페이스들의 인스턴스를 람다식으로 사용해 만들 수 있게 된것이다.
결국 코드의 길이가 짧고 가독성을 높이기위해 가독성있는 함수객체를 만드는 것이다.
함수 객체를 람다식으로 넘기니 더 간결하게 표현되었다.
의문이 드는점은 타입을 명시를 안해줘도 되는것인가? 라는 고민이 들기시작한다.
Collections.sort 코드를 보면
List<T>
의 T타입을 통해 컴파일러가Comparator<? suepr T>
를 집어넣어 추론한다.따라서 List를 다음과 같이 선언하면 타입을 알지못해 오류가 발생한다.
따라서 강조한 내용은 타입을 명시해야 코드가 더 명확할 때만 제외하고, 람다의 모든 매개변수 타입은 생략하자 이다.
여기에서더 람다식 코드를 줄일 수 있다. 바로 비교자 생성 메서드를 사용하면 더 간결하게 나온다.
이것은 다음장에 더 자세히 나옵니다.
Item 34의 Operation 열거타입 예를 가져와 람다식으로 변경해보자.
해당코드를 람다식으로 리팩토링 해보자.
더이상 apply를 추상메서드로 두지말고 람다식으로 코드가 간결해졌다.
근데 과연 코드가 짧다고 가독성이 좋아지는 것이 아니다.
그럼 언제 람다식을 표현해야할까?
람다는 이름이 없고 문서화를 할 수 없다. 따라서 람다식을 쓸때는 코드 자체로 어떤 행위를 하는지 명확히 보여져야한다. 명확히 보이지 않는다면 기존의 클래스 몸체를 활용하자.
열거 타입 생성자에 넘겨지는 인수들의 타입도 컴파일타임에 추론된다. 따라서 열거 타입 생성자 안의 람다는 인스턴스 생성이 런타임에 발생하기 때문에 컴파일 시점에서는 열거타입의 인스턴스 멤버에 접근할 수 없다.
람다로 대체할수 없는 것
만약 두개의 메서드가 있다고하면 다음과같이 선언해야될것이다.
람다식은 함수명을 따로 적지 않기때문에 두개인 경우 어느 함수의 람다식 인지 당연히 판단하지 못한다.
java lambda this reference
로 구글링)결론