본문 바로가기

Reading Record/이펙티브자바

[아이템 42] 익명 클래스보다는 람다를 사용하라

[아이템 42] 익명 클래스보다는 람다를 사용하라

예전 함수 객체를 표현하는 방식은 익명클래스로 충분했다.

이번 장에서 익명클래스는 무엇인지, 람다로 대체하면 어떤 이점이 있는지 알아보자

익명 클래스의 인스턴스를 함수 객체로 사용하는 것은 낡은 기법이다.

Collection.sort(words, new Comparator<String>() {
  public int compare(String s1, String s2){
    return Integer.compare(s1.length(), s2.length());
  }
});

위 코드는 Comparator 함수 객체를 익명클래스로 구현하여 sort function의 파라미터로 넘겨주는 코드이다.

예전 디자인 패턴에서는 위와 같은 코드로도 충분히 함수객체를 표현하는것이 가능했다.

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

자바 8에서는 위와 같이 하나짜리 인터페이스특별대우를 받게 된다.

바로 람다 표현이 가능하다는 것이다.

Collection.sort(words, (s1,s2) -> Integer.compare(s1.length(), s2.length()));

위와 같이 표현하면 익명클래스에서 사용했던 자질구레한 코드들이 사라지고 간결하게 표현할 수 있다.

이 부분에서 신기한 것은 타입에 대한 언급이 전혀 없다는 것이다.

String인자를 받아 int를 리턴하는 함수형 인터페이스에 대한 타입 추론은 컴파일러가 대신해주기 때문에 언급이 불필요 한것이다.
컴파일러가 타입을 추론할때 필요한 정보들은 대부분 제네릭을 통해 얻는다.

위 람다 예제를 다시보면 sort 함수는 이렇게 선언되어 있다.

만약 words가 매개변수화 타입으로 넣지 않았다면, 위 람다 표현은 컴파일 오류가 발생할 것이다.

열거타입에서의 람다

public enum Operation {
    PLUS("+") {
        public double apply(double x, double y) {
            return x + y;
        }
    };

    private final String symbol;

    Operation(String symbol) {
        this.symbol = symbol;
    }

    public abstract double apply(double x, double y);
}

위 코드는 열거타입에 추상 메서드를 선언해 각 enum 상수별 추상 메서드를 구현하는 방식으로 표현했다.

public enum OperationLambda {
    PLUS("+", (x, y) -> x + y);

    private final String symbol;
    private final DoubleBinaryOperator op;

    OperationLambda(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }

    public double apply(double x, double y){
        return op.applyAsDouble(x,y);
    }
}

두번째 코드는 java.util패키지에서 제공하고 있는 function 인터페이스enum의 멤버변수로 두어 구현한 방식이다.

몸통 클래스를 구현한 것 보다 짧고 보기 편한 코드가 되었다.

하지만 꼭 상수별 클래스 몸체를 사용할 필요가 없는 것은 아니다.

아래 2가지의 상황에서 사용하는 것이 좋다.

1.람다는 이름이 없다. 고로 문서화하기 어렵다. 코드 자체로 표현이 안될때는 람다를 쓰지 말자.

2.열거 타입 생성자에 넘겨지는 인수들의 타입도 컴파일타임에 추론된다. 따라서 열거 타입 생성자 안의 람다는 열거타입의 인스턴스 멤버에 접근할 수 없다.(인스턴스 생성은 런타임에 된다.)

2번은 예제로 살펴보자.

첫번째 코드는 몸톰 클래스를 구현해 인스턴스 변수에 접근할 수 있는 경우고, 두번째 람다 구현은 인스턴스 변수에 접근 할 수 없다.

람다가 대체 할 수 없는것

  1. 추상클래스의 인스턴스를 만들때 람다사용은 불가능하다.
  2. 추상메서드가 여러개인 인터페이스의 인스턴스도 람다로 표현 불가능하다.
  3. 람다의 this는 바깥 인스턴스를 가리킨다. (https://stackoverflow.com/questions/24202236/lambda-this-reference-in-java)

유의사항

직렬화가 필요한 경우에는 람다와 익명클래스를 삼가해야한다. vm마다 직렬화 형태가 다르기 때문이다.

직렬화가 궁금하다면 https://javarevisited.blogspot.com/2011/04/top-10-java-serialization-interview.html

직렬화 해야만 하는 함수객체는 정적 중첩클래스의 인스턴스로 구현하자.

public class FruitCollection {
    private final FruitComparator comparator;
        ....

    public FruitCollection(Comparator<Fruit> comparator) {
        this.comparator = comparator;
    }

    static class FruitComparator implements Comparator<Fruit>, Serializable {
        private static final long serialVersionUID = 1;

        @Override
        public int compare(Fruit o1, Fruit o2) {...}
    }
}

과일들을 정렬하여 저장하는 컬렉션이 있다면, 아래와 같이 중첩 정적 클래스로 Comparator를 직렬화 하여 선언하여 구현한다.