관리 메뉴

루시와 프로그래밍 이야기

ITEM42 익명 클래스보다는 람다를 사용하라 본문

스터디/이펙티브 자바

ITEM42 익명 클래스보다는 람다를 사용하라

Lucy_Ko 2022. 11. 18. 14:51

https://blog.insightbook.co.kr/2018/10/24/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-3%ED%8C%90effective-java-3-e/

3판에서 볼 수 있는 새로운 내용

Java7 이후의 새로운 내용을 다루는 것뿐 아니라 함수형 프로그래밍 요소도 자세히 알아봅니다. 람다와 스트림에 집중한 7장을 포함하여 새로운 아이템도 많이 추가되었습니다.

 

람다에도 맵핵 시전!!

자바 7, 8, 9 신기능 반영

– 함수형 인터페이스, 람다식, 메서드 참조, 스트림

– 인터페이스의 디폴트 메서드와 정적 메서드

– 제네릭 타입에서의 다이아몬드 연산자를 포함한 타입 추론

– @SafeVarargs 애너테이션

– try-with-resources 문

– Optional〈T〉 인터페이스, java.time, 컬렉션의 편의 팩터리 메서드 등의 새로운 라이브러리 기능


예전에는 자바에서 함수 타입을 표현할 때 추상 메서드를 하나만 담은 인터페이스(드물게는 추상 클래스)를 사용했다.

이런 인터페이스의 인스턴스를 함수객체라고 하여, 특정 함수나 동작을 나타내는 데 썼다.

1997 JDK 1.1의 등장으로 인해, 함수 객체를 만드는 주요 수단은 익명 클래스가 되었다.

 다음 코드를 예로 살펴보자. 문자열을 길이순으로 정렬하는데, 정렬을 위한 비교 함수로 익명 클래스를 사용한다.

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

쪼개서 보아야 쉽게 보인다!

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

);

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


익명클래스란? "이름이 없는" 클래스

https://mommoo.tistory.com/16

 

JAVA - 익명클래스(Anonymous class)란?

오늘은 자바의 익명클래스(Anonymous Class) 혹 무명클래스 에 관해 포스팅을 할 것이다. 익명클래스는 말 그대로 익명의 성질을 가진 클래스라는 뜻이다. 즉 이름이 없는 클래스라고 불리우는데, '

mommoo.tistory.com

 

public class Test{
    private int num = 1;
    public int getNum(){
        return this.num;
    }
    public void setNum(int num){
        this.num = num;
    }
}

	//Test 라는 클래스
    Test t1 = new Test();
    
    //익명클래스  @Override Test를 이름없는 클래스안에서 재정의함
    Test t1 = new Test(){
         public int num = 10;
         @Override
         public int getNum(){
             return this.num;
         }
     };

 


  자바 8에 와서 추상 메서드 하나짜리 인터페이스는 특별한 의미를 인정받아 특별한 대우를 받게 된다. 지금은 함수형 인터페이스라 부르는 이 인터페이스들의 인스턴스를 람다식을 사용해 만들 수 있게 된 것이다. 람다는 함수나 익명 클래스와 개념은 비슷하지만 코드는 훨씬 간결하다.

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

여기서 람다, 매개변수(s1, s2), 반환값의 타입은 각각 (Comparator<String), String, int지만 코드에서는 언급이 없다. 우리 대신 컴파일러가 문맥을 살펴 타입을 추론해준 것이다.

  상황에 따라 컴파일러가 타입을 결정하지 못할 수도 있는데, 그럴 때는 프로그래머가 직접 명시해야 한다. 타입을 명시해야 코드가 더 명확할 때만 제외하고는, 람다의 모든 매개변수 타입은 생략하자. 그런 다음 컴파일러가 '타입을 알 수 없다'는 오류를 낼 때만 해당 타입을 명시하면 된다. 반환값이나 람다식 전체를 형변환해야 할 때도 있겠지만, 아주 드물 것이다.

 

https://developer-cheol.tistory.com/61

 

Effective Java ( 이펙티브 자바 ) - 아이템 42 - 7장 - 람다와 스트림

익명 클래스보다는 람다를 사용하라 예전에는 자바에서 함수 타입을 표현할 때 추상 메서드를 하나만 담은 인터페이스(드물게는 추상 클래스)를 사용했다. 이런 인터페이스의 인스턴스를 함수

developer-cheol.tistory.com


https://jjingho.tistory.com/91

예전에는 함수 객체를 만드는 주요 수단으로 익명 클래스를 많이 사용했다.

함수 객체
추상 메서드를 하나만 담은 인터페이스의 인스턴스

// 익명 클래스를 함수 객체로 사용 - 낡은 기법이다.
Collections.sort(words, new Comparator<String>() {
  public int compare(String s1, String s2){
    return Integer.compare(s1.length(), s2.length());
  }
});

 

하지만 이 방식은 낡은 기법이고 코드가 너무 길어서 함수형 프로그래밍에 적합하지 않다.

Java8부터는 추상 메서드가 하나인 인터페이스는 특별한 대우를 받게 되었다. 지금은 함수형 인터페이스로 부르는 이 인터페이스를 람다 표현식으로 만들 수 있게 된 것이다.

// 람다식을 함수 객체로 사용 - 익명 클래스를 대체했다.
Collections.sort(words,
        (s1,s2) -> Integer.compare(s1.length(), s2.length()));

 

람다는 함수나 익명 클래스와 개념은 비슷하지만 코드는 훨씬 간결하고 의도를 명확히 드러낸다는 장점이 있다. 또한, 컴파일러가 함수형 인터페이스의 타입을 추론해주기 때문에 타입을 생략할 수 있다. 따라서 타입을 명시해야 코드가 더 명확할 때를 제외하고 타입을 생략하는 것이 좋다.

다만 주의해야 할 점은 컴파일러가 제네릭을 통해 타입 정보를 얻는다는 것이다. 즉, 제네릭을 사용하지 않으면 컴파일러는 타입 정보를 추론할 수 없게 된다.

 

열거 타입과 람다 표현식

// 추상 메서드 구현
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;
    }

    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;

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

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

DoubleBinaryOperator ?
Java에서 제공하는 함수형 인터페이스이다. double 타입 인수 2개를 받아 double을 리턴한다.

열거 타입에서 람다 사용 시 주의사항

1. 람다는 이름이 없기 때문에 문서화를 할 수 없다.

람다는 간결한 코드와 의도를 명확히 드러낼 때 강점을 가진다. 따라서 코드 자체로 동작이 명확히 설명되지 않거나 람다로 표현한 코드가 세 줄 이상이라면 람다를 사용하지 말자.

 

2. 열거 타입 생성자 안의 람다는 열거 타입의 인스턴스 멤버에 접근할 수 없다.

열거 타입 생성자에 넘겨지는 인수들의 타입도 컴파일 타임에 추론된다. 하지만 인스턴스는 런타임에 만들어지기 때문에 인스턴스 멤버에 접근할 수 없게 된다. 이 경우에는 상수별 클래스 몸체를 사용해야 한다.

 

2번 추가 설명.

public enum Operation {
    PLUS("+", (x, y) -> {
        System.out.println(symbol);
        return x + y;
    }),
    MINUS("-", (x, y) -> x - y),
    TIMES("*", (x, y) -> x * y),
    DIVIDE("/", (x, y) -> x / y);

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

    public String getSymbol() {
        return symbol;
    }

    protected final String symbol;
    private final DoubleBinaryOperator op;

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

 

람다를 생성자의 인수로 넘길 때 인스턴스 멤버인 symbol을 참조하면 컴파일 오류가 발생한다.

 

이는 람다가 열거 타입의 멤버가 아니기 때문에 발생한다. 즉, 문맥(context)이 다르기 때문에 열거 타입의 인스턴스 멤버에 접근할 수 없다. 따라서 람다가 생성자의 인자로 전달되는 시점에 인스턴스 멤버인 symbol을 참조할 수 없어 컴파일 오류가 발생한다.

 

람다로 대체 불가능한 부분

람다의 시대가 열리면서 익명 클래스는 설 자리가 크게 좁아진 게 사실이다. 하지만 람다로 대체할 수 없는 부분이 존재한다.

1. 람다는 함수형 인터페이스에서만 쓰인다.

함수형 인터페이스란 추상 메서드가 한 개인 인터페이스를 말한다. 즉, 추상 메서드가 하나 이상이라면 람다를 사용할 수 없다.

 

2. 추상 클래스의 인스턴스를 만들 때는 익명 클래스를 사용해야 한다.

추상 클래스에서는 람다를 사용할 수 없기 때문에 익명 클래스를 사용해야 한다.(추상 메서드가 여러 개인 인터페이스도 마찬가지)

 

3. 람다의 this는 바깥 인스턴스를 가리킨다.

람다의 스코프(scope)는 익명 클래스와 다르다. 람다는 바깥 인스턴스와 스코프가 같다. 하지만 익명 클래스는 더 좁은 범위의 스코프를 가진다. 따라서 익명 클래스의 this는 익명 클래스의 인스턴스 자신을 가리키고, 람다의 this는 바깥 인스턴스를 가리킨다. 만약 함수 객체가 자신을 참조해야 한다면 반드시 익명 클래스를 사용해야 한다.

 

Comments