아이템 42. 익명 클래스보다는 람다를 사용하라. - ksw6169/effective-java GitHub Wiki

함수 객체(function object)

  • 추상 메소드를 하나만 담은 인터페이스 혹은 추상 클래스의 인스턴스를 함수 객체라고 한다.
  • 함수 객체는 자바에서 함수 타입을 표현할 때 사용되었으며 이를 만드는 주요 수단은 익명 클래스였다.
/**
 * 문자열을 길이 순으로 정렬한다. 여기서는 정렬을 위한 비교 함수로 익명 클래스를 사용한다.
 * 마치 전략 패턴처럼 Comparator는 정렬을 담당하는 추상 전략을 뜻하며, 
 * 문자열을 정렬하는 구체적인 전략은 익명 클래스로 구현하였다.
 */
Collections.sort(words, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});
  • 함수 객체를 사용하기 위해 익명 클래스를 이용하는 방식은 코드가 너무 길기 때문에 자바는 함수형 프로그래밍에 적합하지 않았다.
  • 따라서 Java 8부터 함수형 프로그래밍을 지원하기 위해 람다식이 추가되었다.

람다식(lambda expression)의 등장

  • 람다식이란 추상 메소드 하나 짜리 인터페이스인 함수형 인터페이스의 인스턴스를 의미한다.
  • 람다식은 함수나 익명 클래스와 개념은 비슷하지만 코드는 훨씬 간결하다.
  • 다음 코드는 람다식을 사용한 예제로 람다식, 매개변수, 반환값의 타입이 제거된 것을 확인할 수 있다.
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 클래스를 람다식을 사용해 개선해보자.

이번에는 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;
    }
}

이 코드에서 사용된 DoubleBinaryOperatorjava.util.function 패키지가 제공하는 함수 인터페이스 중의 하나로, Double 타입 인수 2개를 받아 Double 타입 결과를 돌려준다.


람다식의 단점

  • 람다는 이름이 없고 문서화도 못한다.
  • 코드 자체로 동작이 명확히 설명되지 않은 경우 람다를 쓰지 말아야 한다.
  • 코드 줄 수가 많아지면 가독성이 심하게 나빠지므로 람다를 쓰지 말아야 한다. (길어야 세 줄)
  • 열거 타입 생성자 안의 람다는 열거 타입의 인스턴스 타입에 접근할 수 없다. (열거 타입 생성자에 넘겨지는 인수들의 타입은 컴파일 타임에 추론되는데 인스턴스는 런타임에 만들어지기 때문이다.)

람다 대신 익명 클래스를 사용해야 되는 상황

익명 클래스를 사용하는 상황에서 람다로 대체할 수 없는 경우가 있다.

  • 추상 클래스의 인스턴스를 만드는 경우
  • 추상 메소드가 여러 개인 인터페이스의 인스턴스를 만드는 경우
  • 함수 객체가 자신을 참조해야 하는 경우(람다는 자신을 참조할 수 없어 this를 사용하지 못한다. 만약 this를 사용한다면 람다 밖의 인스턴스를 참조하게 된다.)

위 내용을 역으로 말하면 익명 클래스는 함수형 인터페이스가 아닌 타입의 인스턴스를 만들 때만 사용해야 한다.


주의 사항

람다도 익명 클래스처럼 직렬화 형태가 구현별로(가령 가상머신별로) 다를 수 있으므로 람다를 직렬화하는 일은 극히 삼가야 한다. (익명 클래스의 인스턴스도 마찬가지다.) 만약 직렬화해야만 하는 함수 객체가 있다면 private 정적 중첩 클래스의 인스턴스를 사용하자.


참고 자료

  • Effective Java 3/E
⚠️ **GitHub.com Fallback** ⚠️