아이템 42. 익명 클래스보다는 람다를 사용하라. - ksw6169/effective-java GitHub Wiki
- 추상 메소드를 하나만 담은 인터페이스 혹은 추상 클래스의 인스턴스를 함수 객체라고 한다.
- 함수 객체는 자바에서 함수 타입을 표현할 때 사용되었으며 이를 만드는 주요 수단은 익명 클래스였다.
/**
* 문자열을 길이 순으로 정렬한다. 여기서는 정렬을 위한 비교 함수로 익명 클래스를 사용한다.
* 마치 전략 패턴처럼 Comparator는 정렬을 담당하는 추상 전략을 뜻하며,
* 문자열을 정렬하는 구체적인 전략은 익명 클래스로 구현하였다.
*/
Collections.sort(words, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
- 함수 객체를 사용하기 위해 익명 클래스를 이용하는 방식은 코드가 너무 길기 때문에 자바는 함수형 프로그래밍에 적합하지 않았다.
- 따라서 Java 8부터 함수형 프로그래밍을 지원하기 위해 람다식이 추가되었다.
- 람다식이란 추상 메소드 하나 짜리 인터페이스인 함수형 인터페이스의 인스턴스를 의미한다.
- 람다식은 함수나 익명 클래스와 개념은 비슷하지만 코드는 훨씬 간결하다.
- 다음 코드는 람다식을 사용한 예제로 람다식, 매개변수, 반환값의 타입이 제거된 것을 확인할 수 있다.
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
- 이것이 가능한 이유는 컴파일러가 문맥을 통해 타입을 추론해줬기 때문이다.
- 상황에 따라 컴파일러가 타입을 추론하지 못할 수 있는데 이 경우에는 타입을 명시해줘야 한다.
- 타입을 명시해야 코드가 더 명확할 때만 제외하고는 람다의 모든 매개변수 타입은 생략하자.
람다 자리에 비교자 생성 메소드를 사용하면 코드를 더 간결하게 만들 수 있다.
Collections.sort(words, Comparator.comparingInt(String::length));
더 나아가 Java 8부터 List 인터페이스에 추가된 sort 메소드를 이용하면 더욱 짧아진다.
words.sort(Comparator.comparingInt(String::length));
이번에는 Operation enum 클래스를 람다식을 사용해 개선해보자. 기존 코드는 다음과 같으며 상수별 클래스 몸체를 구현하는 방식을 사용하였다.
public enum Operation {
PLUS("+") {
@Override
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
@Override
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
@Override
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
@Override
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() { return symbol; }
public abstract double apply(double x, double y);
}
위 코드를 각 상수에서 사용하는 함수를 생성자에서 람다로 받아 별도의 필드에 저장하고 이를 사용하도록 개선해보자.
public enum Operation {
PLUS("+", (x, y) -> x + y),
MINUS("-", (x, y) -> x - y),
TIMES("*", (x, y) -> x * y),
DIVIDE("/", (x, y) -> x / y);
private final String symbol;
private final DoubleBinaryOperator op;
Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}
@Override
public String toString() {
return symbol;
}
}
이 코드에서 사용된 DoubleBinaryOperator
는 java.util.function
패키지가 제공하는 함수 인터페이스 중의 하나로, Double
타입 인수 2개를 받아 Double
타입 결과를 돌려준다.
- 람다는 이름이 없고 문서화도 못한다.
- 코드 자체로 동작이 명확히 설명되지 않은 경우 람다를 쓰지 말아야 한다.
- 코드 줄 수가 많아지면 가독성이 심하게 나빠지므로 람다를 쓰지 말아야 한다. (길어야 세 줄)
- 열거 타입 생성자 안의 람다는 열거 타입의 인스턴스 타입에 접근할 수 없다. (열거 타입 생성자에 넘겨지는 인수들의 타입은 컴파일 타임에 추론되는데 인스턴스는 런타임에 만들어지기 때문이다.)
익명 클래스를 사용하는 상황에서 람다로 대체할 수 없는 경우가 있다.
- 추상 클래스의 인스턴스를 만드는 경우
- 추상 메소드가 여러 개인 인터페이스의 인스턴스를 만드는 경우
- 함수 객체가 자신을 참조해야 하는 경우(람다는 자신을 참조할 수 없어 this를 사용하지 못한다. 만약 this를 사용한다면 람다 밖의 인스턴스를 참조하게 된다.)
위 내용을 역으로 말하면 익명 클래스는 함수형 인터페이스가 아닌 타입의 인스턴스를 만들 때만 사용해야 한다.
람다도 익명 클래스처럼 직렬화 형태가 구현별로(가령 가상머신별로) 다를 수 있으므로 람다를 직렬화하는 일은 극히 삼가야 한다. (익명 클래스의 인스턴스도 마찬가지다.) 만약 직렬화해야만 하는 함수 객체가 있다면 private 정적 중첩 클래스의 인스턴스를 사용하자.
- Effective Java 3/E