Language/Java

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

JOYERIM 2023. 7. 2. 23:09

 

 

 

 

 

 

핵심 내용

 

 

자바 8부터 도입된 람다 함수는 기존의 익명 클래스보다 훨씬 간결하다.

익명 클래스의 경우, 인스턴스가 필요할 경우 사용하자.

직렬화해야 할 경우에는 private 정적 중첩 클래스의 인스턴스를 사용하자. (익명 클래스와 람다 함수 모두)

 

 

 

 

 

낡은 방법
: 익명 클래스 방식

 

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

 

익명 클래스 방식은 코드가 너무 길기 때문에 함수형 프로그래밍에 적합하지 않다.

 

 

 

 

 

자바 8 이후 방법
: 람다식 방식

 

 

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

 

람다식 방식은 훨씬 간결하다. 위 코드를 살펴보면, 매개변수(s1, s2), 반환값의 타입은 (Comparator<String>), String, int지만 지정하지 않아도 정상동작한다. 컴파일러가 타입을 추론한 것으로, 상황에 따라 프로그래머가 직접 명시해야 한다.

 

타입 추론 규칙은 매우 복잡하므로 명확하지 않을 경우에만 타입을 지정할 경우나, 컴파일러가 오류를 낼 경우에만 명시하면 된다.

 

컴파일러가 타입을 추론할 때 필요한 타입 정보 대부분을 제네릭에서 얻는다.

즉, 아래 규칙을 더욱 명심해서 사용하라.

- 제네릭의 raw 타입을 쓰지 마라(아이템 26)
- 제네릭을 사용하라(아이템 29)
- 제네릭 메소드를 쓰라(아이템 30)

 

Collections.sort(words, comparingInt(String::length));

 

위 코드처럼 비교자 생성 메소드를 사용하면 더 간결해진다.

 

 

words.sort(comparingInt(String::length));

 

자바 8 때 List 인터페이스에 추가된 sort 메소드를 사용하면 더 간결해진다.

 

 

 

 

예시 1
: 열거 타입

 

public enum Operation {
    PLUS("+") {
        public double apply(double x, double y) { return x + y; }
    },
    MINUS("-") {
        public double apply(double x, double y) { return x - y; }
    },
    TIMES("*") {
        public double apply(double x, double y) { return x * y; }
    },
    DIVIDE("/") {
        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; }
 
    public double apply(double x, double y) {
        return op.applyAsDouble(x, y);
    }
 
    public static void main(String[] args) {
        double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        for (Operation op : Operation.values())
            System.out.printf("%f %s %f = %f%n",
                    x, op, y, op.apply(x, y));
    }
}

 

람다로 구현하여 훨씬 간결하게 작성할 수 있다.

 

 

람다식 방식 관련 주의해야 할 점

 

람다는 이름이 없고 문서화도 못 한다.
따라서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 말아야 한다. 

 

열거 타입 생성자가 실행되는 시점에는 열거 타입 인스턴스들은 아직 완전히 생성되지 않는다. 따라서 생성자 또는 초기화 블록에서는 인스턴스 메소드나 필드를 사용할 수 없다. 그러나 상수별 클래스 몸체는 열거 타입 상수가 이미 생성된 후에 인스턴스 메소드가 실행되므로 인스턴스 필드나 메소드를 사용할 수 있다. (예제 1 Operation 참고)

 

추상 클래스의 인스턴스를 만들 때 람다를 쓸 수 없으니, 익명 클래스를 사용해야 한다.
또한, 추상 메소드가 여러 개인 인터페이스의 인스턴스를 만들 때도 익명 클래스를 사용해야 한다.

 

람다는 자신을 참조할 수 없다.
람다에서의 this 키워드는 바깥 인스턴스를 가리킨다. (<-> 익명 클래스에서의 this는 익명 클래스의 인스턴스 자신을 가리킨다.)
즉, 자신을 참조해야 한다면 반드시 익명 클래스를 사용해야 한다.

 

람다와 익명클래스의 인스턴스는 직렬화하지 말아야 한다. (구현별로 달라질 수 있으므로 !!)
직렬화해야만 하는 함수 객체(ex. Comparator)가 있다면 private 정적 중첩 클래스(아이템 24)의 인스턴스를 사용하자.

 

import java.io.Serializable;
import java.util.Comparator;

public class Person {
    private String name;

    // ... 기타 필드와 메서드 ...

    public String getName() {
        return name;
    }

    // 'name' 기준으로 비교하는 Comparator, 직렬화 가능
    public static final Comparator<Person> NAME_ORDER =
            new NameOrderComparator();

    private static class NameOrderComparator 
            implements Comparator<Person>, Serializable {
        @Override
        public int compare(Person p1, Person p2) {
            return p1.name.compareTo(p2.name);
        }
    }
}