Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Item 42 & 43 & 44 익명 클래스보다는 람다를, 람다보다는 메서드 참조를 사용하라 / 표준 함수형 인터페이스 #22

Open
ChaCha3088 opened this issue Dec 20, 2023 · 0 comments
Assignees

Comments

@ChaCha3088
Copy link

ChaCha3088 commented Dec 20, 2023

section: 7장

  • Item 42 익명 클래스보다는 람다를 사용하라
  • Item 43 람다보다는 메서드 참조를 사용하라
  • Item 44 표준 함수형 인터페이스를 사용하라

🍵 서론

익명 클래스보다는 람다를, 람다보다는 메서드 참조를 사용하라 / 표준 함수형 인터페이스

🌒 본론

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

예전 Java에서는 함수 객체를 사용했다.

예전 Java에서 함수 타입을 표현할 때, 추상 메서드를 하나만 담은 인터페이스(드물게는 추상 클래스)를 사용했다.
이런 인터페이스의 인스턴스를 함수 객체(Function Object)라고 하여, 특정 함수나 동작을 나타내는데 썼다.
1997년 JDK 1.1이 등장하면서, 함수 객체를 만드는 주요 수단은 익명 클래스(아이템 24)가 되었다.

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

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

Java 8

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

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

컴파일러가 문맥을 살펴 타입을 추론해준다.
타입을 명시해야 코드가 더 명확할 때 제외하고, 람다의 모든 매개변수 타입은 생략하자.
람다 자리에 비교자 생성 메서드를 사용하면 이 코드를 더 간결하게 만들 수 있다.(아이템 14, 43)

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

더 나아가 Java 8 List 인터페이스에 추가된 sort 메서드를 사용하면 더욱 짧아진다.

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

상수별 클래스 몸체와 데이터를 사용한 열거 타입(34-6)

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);
	
	  // DoubleBinaryOperator : Double 타입 인수 2개를 받아 Double 타입 결과를 돌려준다
	  private final DoubleBinaryOperator op;    // 계산식의 함수 객체 (멤버)
	  private final String symbol;
	
	  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);
	  }
}

단점

이름 없음, 문서화 불가
→ 코드 자체로 동작이 명확히 설명되지 않거나, 코드가 길어지면, 람다를 쓰면 안된다.

인스턴스 필드나 메서드를 사용해야만 하는 상황이면 클래스 몸체를 사용해야 한다.

익명 클래스의 건재함

추상 클래스의 인스턴스를 만들 때 람다를 쓸 수 없으므로, 익명 클래스를 써야한다.
비슷하게 추상 메서드가 여러 개인 인터페이스의 인스턴스를 만들 때도 익명 클래스를 쓸 수 있다.

자신을 참조할 수 없다.
람다에서의 this는 바깥 인스턴스를 가리킨다.
반면 익명 클래스의 this는 인스턴스 자신을 가리킨다.
따라서 함수 객체가 자신을 참조해야 한다면, 반드시 익명 클래스를 써야한다.

람다 직렬화는 극히 삼가라

람다도 익명 클래스처럼 직렬화 형태가 구현별로 다를 수 있다.
따라서 람다를 직렬화하는 일은 극히 삼가야 한다.(익명 클래스의 인스턴스도 마찬가지다.)
직렬화를 해야만 한다면, private 정적 중첩 클래스(아이템 24)의 인스턴스를 사용하자.

43. 람다보다는 메서드 참조를 사용하라

람다가 익명 클래스보다 나은 점 중 가장 큰 특징은 간결함이다.
하지만 더 간결하게 만드는 방법은 메서드 참조(Method Reference)다.

map.merge(key, 1, (count, incr) -> count + incr);

map.merge(key, 1, Integer::sum);

람다로 할 수 없는 일이라면, 메서드 참조로도 할 수 없다.
이렇게 메서드 참조는 간결하게 해줄 뿐 아니라, 이름을 지어줄 수 있고, 친절한 설명을 문서로도 남길 수 있다.

람다가 메서드 참조보다 간결한 경우

메서드와 람다가 같은 클래스에 있을 때

service.execute(GoshThisClassNameIsHumongous::action);

service.execute(() -> action());

메서드 참조의 유형

다섯 가지
가장 흔한 유형은 위와 같이 정적 메서드를 가리키는 메서드 참조

인스턴스 메서드를 참조하는 유형

두 가지

수신 객체(Receiving Object)를 특정하는 한정적 인스턴스 메서드 참조

근본적으로 정적 참조와 비슷

수신 객체를 특정하지 않는 비한정적 인스턴스 메서드 참조

함수 객체를 적용하는 시점에 수신 객체를 알려준다.
주로 스트림 파이프라인에서의 매핑과 필터 함수에 쓰인다.(아이템 45)

클래스 생성자를 가리키는 메서드 참조

팩터리 객체로 사용된다.

배열 생성자를 가리키는 메서드 참조

Screenshot 2023-12-19 at 21 14 31

정리

메서드 참조가 짧고 명확하다면 메서드 참조
그렇지 않으면 람다

44. 표준 함수형 인터페이스를 사용하라

Java가 람다를 지원하면서 API를 작성하는 모범 사례도 크게 바뀌었다.
예로, 상위 클래스의 기본 메서드를 재정의해 원하는 동작을 구현하는 템플릿 메서드 패턴[Gamma95]의 매력이 크게 줄었다.
이를 대체하는 현대적인 해법은 같은 효과의 함수 객체를 받는 정적 팩터리나 생성자를 제공하는 것이다.
조금 더 일반화 하면, 함수 객체를 매개변수로 받는 생성자와 메서드를 더 많이 만들어야 한다. 이 때, 함수형 매개변수 타입을 올바르게 선택해야한다.

함수형 인터페이스?

1개의 추상 메소드를 갖는 인터페이스
여러 개의 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스

[Java 8 함수형 인터페이스 (Functional Interface)](https://bcp0109.tistory.com/313)

Java 표준 라이브러리 java.util.function 패키지를 보면 다양한 용도의 표준 함수형 인터페이스가 담겨있다.

💡 필요한 용도에 맞는게 있다면, 직접 구현하지 말고, 표준 함수형 인터페이스를 활용하라.

그러면 API가 다루는 개념의 수가 줄어들어 익히기 더 쉬워진다.
또한 표준 함수형 인터페이스들은 유용한 디폴트 메서드를 많이 제공하므로, 다른 코드와의 상호운용성도 크게 좋아질 것이다.

예로 Predicate 인터페이스는 Predicate들을 조합하는 메서드를 제공한다.
기본 인터페이스 6개만 기억하면 나머지를 충분히 유추해 낼 수 있다.
이 기본 인터페이스들은 모두 참조 타입용이다.

Operator 인터페이스는 인수가 1개인 UnaryOperator와 2개인 BinaryOperator로 나뉘며, 반환값과 인수의 타입이 같은 함수를 뜻한다.
Predicate 인터페이스는 인수 하나를 받아 boolean을 반환하는 함수를 뜻하며
Function 인터페이스는 인수와 반환 타입이 다른 함수를 뜻한다.
Supplier 인터페이스는 인수를 받지 않고 값을 반환하는 함수
Consumer 인터페이스는 인수를 하나 받고 반환값은 없는 함수를 뜻한다.

Screenshot 2023-12-19 at 21 25 37 Screenshot 2023-12-19 at 21 25 48

표준 함수형 인터페이스 대부분은 기본 타입만 지원한다.
그렇다고 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말자.(아이템 61)

예시

[BOJ_11723_집합](https://www.acmicpc.net/problem/11723)

/*
비어있는 공집합 S가 주어졌을 때, 아래 연산을 수행하는 프로그램을 작성하시오.

add x: S에 x를 추가한다. (1 ≤ x ≤ 20) S에 x가 이미 있는 경우에는 연산을 무시한다.
remove x: S에서 x를 제거한다. (1 ≤ x ≤ 20) S에 x가 없는 경우에는 연산을 무시한다.
check x: S에 x가 있으면 1을, 없으면 0을 출력한다. (1 ≤ x ≤ 20)
toggle x: S에 x가 있으면 x를 제거하고, 없으면 x를 추가한다. (1 ≤ x ≤ 20)
all: S를 {1, 2, ..., 20} 으로 바꾼다.
empty: S를 공집합으로 바꾼다.
*/

public class Main {
    private static StringBuffer sb = new StringBuffer();
    private static boolean[] arr = new boolean[21];
    private static StringTokenizer st;

    enum Calculator {
        // S에 x를 추가한다. (1 ≤ x ≤ 20) S에 x가 이미 있는 경우에는 연산을 무시한다.
        ADD("add", (number) -> {
            if (arr[number]) {
                return 0;
            }

            arr[number] = true;
            return 1;
        }),
        // S에서 x를 제거한다. (1 ≤ x ≤ 20) S에 x가 없는 경우에는 연산을 무시한다.
        REMOVE("remove", (number) -> {
            if (!arr[number]) {
                return 0;
            }

            arr[number] = false;
            return 1;
        }),

        // S에 x가 있으면 1을, 없으면 0을 출력한다. (1 ≤ x ≤ 20)
        CHECK("check", (number) -> {
            if (arr[number]) {
                return 1;
            } else {
                return 0;
            }
        }),

        // S에 x가 있으면 x를 제거하고, 없으면 x를 추가한다. (1 ≤ x ≤ 20)
        TOGGLE("toggle", (number) -> {
            arr[number] = !arr[number];
            return 1;
        }),

        // S를 {1, 2, ..., 20} 으로 바꾼다.
        ALL("all", (number) -> {
            Arrays.fill(arr, true);
            return 1;
        }),

        // S를 공집합으로 바꾼다.
        EMPTY("empty", (number) -> {
            Arrays.fill(arr, false);
            return 1;
        });

        private final String symbol;
        private final Function<Integer, Integer> expression;

        Calculator(final String symbol, final Function<Integer, Integer> expression) {
            this.symbol = symbol;
            this.expression = expression;
        }

        public int calculate(final int number) {
            return expression.apply(number);
        }
    }

    public static void main(String[] args) throws IOException {
        System.setIn(new FileInputStream("input.txt"));
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        for (int n = 0; n < N; n++) {
            st = new StringTokenizer(br.readLine());

            String command = st.nextToken();

            int number = 0;

            if (command.equals("all") || command.equals("empty")) {
                number = 0;
            } else {
                number = Integer.parseInt(st.nextToken());
            }

            int result = Calculator.valueOf(command.toUpperCase()).calculate(number);

            if (command.equals("check")) {
                sb.append(result).append("\n");
            }
        }

        System.out.println(sb);
    }
}

그럼에도 코드를 직접 작성해야 할 때

  1. 자주 쓰이며, 이름이 용도를 아주 훌륭히 설명할 때
  2. 구현하는 쪽에서 반드시 지켜야 할 규약을 담고 있을 때
  3. 유용한 디폴트 메서드들을 제공 할 때

전용 함수형 인터페이스를 작성하기로 했다면…

자신이 작성하는 것이 인터페이스임을 명심해야 한다.
아주 주의해서 설계해야 한다는 뜻(아이템 21)

코드 44-1의 EldestEntryRemovalFunction 인터페이스에 @FunctionalInterface 어노테이션이 달려있음에 주목하자.
이 어노테이션을 사용하는 이유는 @OverRide를 사용하는 이유와 비슷하다.
프로그래머의 의도를 명시하는 것으로, 크게 세 가지 목적이 있다.

  1. 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다.
  2. 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다.
  3. 그 결과 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.

💡 직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 어노테이션을 사용하라.

함수형 인터페이스를 API에서 사용할 때의 주의점

  1. 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중 정의해서는 안된다.
    1. 클라이언트에게 불필요한 모호함만 준다.
  2. 서로 다른 함수형 인터페이스를 같은 위치의 인수로 사용하는 다중 정의를 피하는 것이다.

정리

입력값과 반환값에 함수형 인터페이스 타입을 활용하라.
보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 가장 좋은 선택이다.
흔치는 않지만, 직접 새로운 함수형 인터페이스를 만들어 쓰는 편이 나을 수도 있다.

🍃 결론

  1. 익명 클래스보다는 람다를, 람다보다는 메서드 참조를 사용하라
  2. 입력값과 반환값에 표준 함수형 인터페이스를 활용하라

reference

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants