[Effective Java] 7장 람다와 스트림 (1부)




정리
아이템 42. 자바 8로 판올림되면서 작은 함수 객체를 구현하는 데 적합한 람다가 도입되었다. 익명 클래스는 (함수형 인터페이스가 아닌) 타입의 인스턴스를 만들 때만 사용한다. 람다는 작은 함수 객체를 아주 쉽게 표현할 수 있다.
아이템 43. 메서드 참조는 람다의 간단명료한 대안이 될 수 있다. 메서드 참조 족이 짧고 명확하다면 메서드 참조를 쓰고, 그러지 않을 때만 람다를 사용한다.
아이템 44. 자바도 잠다를 지원한다. API를 설계할 때 람다로 염두에 두어야 한다. 입력값과 반환값에 함수형 인터페이스 타입을 활용하라. 보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 가장 좋은 선택이다. 단 직접 새로운 함수형 인터페이스를 만들어 쓰는 것이 나을 수도 있다.


내용
아이템 42. 익명 클래스보다는 람다를 사용하라
● 자바 8 이전에는 전략 패턴처럼, 함수 객체를 사용하는 과거 객체 지향 디자인 패턴에는 익명 클래스면 충분했다. 하지만, 코드가 너무 길기 때문에 자바는 함수형 프로그래밍에 적합하지 않았다. 자바 8 이후에는 함수형 인터페이스의 인스턴스를 람다식(lambda)을 사용해 만든다.
람다는 함수형 인터페이스에서만 사용한다.
람다식을 함수 객체로 사용하는 예
Collection.sort(words, (s1, s2) -> Inter.compare(s1.length(), s2.length()));
● 여기서 람다, 매개변수(s1, s2), 반환값의 타입은 각각 (Comparator<String>), String, int지만 코드에 언급이 없다. 상황에 따라 컴파일러가 타입을 결정하지 못할 때가 있는데, 그럴 땐 직접 명시해야한다.
● 타입을 명시해야 코드가 더 명확할 때만 제외하고는, 람다의 모든 매개변수 타입은 생략한다. 컴파일러가 타입을 추론하는 데 필요한 타입 정보는 대부분 제네릭에서 얻는다. 개발자가 이 정보를 주지 않으면 컴파일러는 람다의 타입을 추론할 수 없다.
● 람다 자리에 비교자 생성 메서드를 사용하면 이 코드를 더 간결하게 만들 수 있다.
Collections.sort(words, comparingInt(String::length));
추가로 자바 8 List 인터페이스에 추가된 sort 메서드를 이용한다면
Words.sort(comp[aringInt(String::length))가 된다.
● 열거타입의 인스턴스 필드를 이용하는 방식으로 상수별로 다르게 동작하는 코드를 쉽게 구현할 수 있다. 각 열거타입 상수의 동작을 람다로 구현해 생성자에 넘기고 생성자는 이 람다를 인스턴스 필드로 저장한다.
기존 열거 방식
public enum BasicOperation implements Operation {
                  PULS("+") {
                                   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;
                 
                  BasicOperation(String symbol) {
                                   this.symbol = symbol;
                  }
                 
                  @override public String toString() { return symbol; }
}
람다로 변환시
public enum BasicOperation implements Operation {
                  PULS("+", (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; thist.op = op;
                  }
                 
                  @override public String toString() { return symbol;}
public double apply(double x, double y) {
return op.applyAsDouble(x, y);
}
}
● 람다는 이름이 없고 문서화도 못한다. 따라서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 말아야 한다. 람다는 한줄내지 2~3줄 안에 끝내는 것이 좋다. 람다가 길거나 가독성이 떨어진다면 람드를 쓰지 않는 쪽으로 리펙터링을 하는 것이 좋다.
● 람다는 자신을 참조할 수 없다. 람다에서 this 키워드는 바깥 인스턴스를 가리키지만 익명 클래스에서의 this는 익명 클래스의 인스턴스를 가리킨다. 그래서 함수 객체가 자신을 참조해야한다면 반드시 익명 클래스를 써야한다.
 람다도 익명 클래스처럼 직렬화 형태가 가상머신별로 다를 수가 있다. 람다를 직렬화해서 사용하는 것은 삼가해야한다.
아이템 43. 람다보다는 메서드 참조를 사용해라
● 람다가 익명 클래스보다 나은 점 중에서 가장 큰 특징은 간결함이다. 그런데 자바에는 함수 객체를 람다보다 더 간결하게 만드는 방법이 있는데, 메서드 참조(method reference).
● 메서드 참조에는 기능을 잘 드러내는 이름을 지어줄 수 있고, 친절한 설명을 문서로 남길 수 있다. 하지만 항상 메서드 참조가 좋은 것은 아니다.
● 메소드 참조 유형은 5가지이다.
1. 정적메소드를 가리키는 메서드 참조다.
2. 수신 객체를 특정하는 한정적 인스턴스 메서드 참조다. 함수 객체가 받는 인수와 참조되는 메서드가 받는 인수가 똑같다. 수신객체는 전달용 매개변수가 매개변수 목록의 첫번째로 추가되며, 그 뒤로 참조되는 메서드 선언에 정의된 매개변수들이 뒤따른다.
3. 수신 객체를 특정하지 않는 비한정적 인스턴스 메서드 참조다. 함수 객체를 적용하는 시점에 수신 객체를 알려준다. 주로 스트림 파이프라인에서의 매핑과 피러 함수에 쓰인다.
4. 클래스 생성자를 가리키는 메서드 참조가 있다. 생성자 참조는 팩터리 객체로 사용된다.
5. 배열 생성자를 가리키는 메서드 참조가 있다.
● 메소드 참조유형 5가지 예제
메서드 참조 유형
같은 기능하는 람다
정적
Integer::parseInt
str -> Integer.parseInt(str)
한정적 (인스턴스)
Instant.now():isAfter
Instant then = Instant.now();
t -> then.isAfter(t)
비한정적 (인스턴스)
String::toLowerCase
str -> str.toLowerCase()
클래스 생성자
TreeMap<K ,V>::new
() -> new TreeMap<K, V>
배열 생성자
Int[]::new
len -> new int[len]
아이템 44. 표준 함수형 인터페이스를 사용하라
● 람다가 생기고 나서 API를 작성하는 방법이 바뀌고 있다. 과거에는 템플릿 메서드 패턴으로 했다면 지금은 함수 객체를 받는 정적 팩터리나 생성자를 제공하는 것이다.
● 필요 용도에 맞는게 있다면 직접 구현하지 말고 표준 함수형 인터페이스를 사용하자. 또한 표준 함수형 인터페이스들은 유용한 디폴트 메서드를 많이 제공하므로 다른 코드와 상호운영성도 크게 좋아질 것이다.
● 기본 인터페이스들은 모두 참조 타입용이다.
1. Operator 인터페이스 : 인수가 1개인 UnaryOperator 2개인 Binary Operator로 나뉘며, 반환값과 인수의 타입이 같은 함수를 뜻한다.
2. Predicate 인터페이스 : 인수 하나를 받아 boolean을 반환하는 함수를 뜻한다.
3. Function 인터페이스 : 인수와 반환 타입이 다른 함수를 뜻한다.
4. Supplier 인터페이스 : 인수를 받지 않고 반환(혹은 제공)하는 함수를 뜻한다.
5. Consumer인터페이스 : 인수를 하나 받고 반환 값은 없는(특히 인수를 소비하는) 함수를 뜻한다
● 기본 함수형 인터페이스들을 정리한 표다.
인터페이스
함수 시그니처
UnaryOperator<T>
T apply(T t)
String::toLowerCase
BinaryOperator<T>
T apply(T t1, T t2)
BigInteger::add
Predicate<T>
boolean test(T t)
Collection::isEmpty
Function<T, R>
R apply(T t)
Arrays::asList
Supplier<T>
T get()
Instant::now
Consumer<T>
void accept(T t)
System.out::println
● 기본 인터페이스는 기본 타입인 int, long, double 용으로 각 3개씩 변형이 생겨난다. Function 인터페이스 같은 경우 접두사가 붙어서 인터페이스명이 정해진다.
● 표준함수형 인터페이스는 대부분 기본 타입만 지원한다. 여기에 박싱된 기본 타입을 넣어 사용하지 말자
● 구조적으로 똑같은 표준 함수형 인터페이가 있더라도 직접 작성해야할 때가 있다. 이중 하나라도 만족한다면 전용 함수형 인터페이스를 구현해야 하는 건 아닌지 고민해야한다.
1. 자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.
2. 반드시 따라야 하는 규약이 있다.
3. 유용한 디폴트 메서드를 제공한다.
@FunctionalInterface 애너테이션은 프로그래머의 의도를 명시하는 것으로 3가지 목적이 있다.
1. 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다.
2. 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 한다.
3. 그 결과 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.
직접 만든 함수형 인터페이스에는 항상 @ FunctionalInterface 애너테이션을 사용해야한다.
● 함수형 인터페이스를 API에서 사용할 때 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중정의해선 안된다. 클라이언트에게 모호함으로 인해 올바른 메서드를 알려주기 위해서 형변환해야 할 때가 많이 생기는 문제가 있기 때문이다.

참고
● 자바 8에서는 함수형 인터페이스, 람다, 메서드 참조라는 개념이 추가 되었다.
● 람다 표현식 작성 방법
자바에서는 화살표(->) 기호를 사용하여 람다 표현식을 작성할 수 있다.
문법 : (매개변수들) -> {함수 몸체}
예제
new Thread(new Runnable() {
    public void run() {
        System.out.println("전통적인 방식의 일회용 스레드 생성");
    }
}).start();

new Thread(()->{
    System.out.println("람다 표현식을 사용한 일회용 스레드 생성");
}).start();



함수형 언어가 많아지는 만큼 자바도 이런 흐름에 자바도 람다를 지원하기 시작했다. 최근 유명한 Spark도 Java 로 RDD를 구현하고 있는데, 이번 장 내용이 도움이 많이 될 것이다. 특히 아이템 43의 메서드 참조 관련 내용은 알아두면 이해하기 좋을 것 같다.

댓글