Post

(Effective Java) 7장 람다와 스트림

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

  • 람다의 모든 매개변수 타입은 생략한다.
    • 타입을 명시해야 코드가 더 명확한 경우만 명시
    • 컴파일러가 “타입을 알 수 없다”는 오류를 낼 때만 명시
  • 단, 람다 코드로 명확히 동작을 알 수 없거나 코드 줄 수가 많아지면 람다를 쓰지 않는게 좋다.
    • 람다는 이름도 없고 문서화도 못하기 때문.
  • 함수 객체가 자기 자신을 참조해야 한다면 반드시 익명 클래스를 써야 한다.
    • 람다에서의 this는 바깥 인스턴스를 가리키는 반면,
    • 익명 클래스에서의 this는 인스턴스 자신을 가리킨다.

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

  • 가독성 측면에서 하는 얘기인데, 꼭 메서드 참조가 깔끔하리라는 법은 없으므로 상황에 맞게 쓰면 된다.

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

  • 웬만한 FunctionalInterface는 이미 표준 함수형 인터페이스 로 제공되고 있으므로, 직접 만들지 말고 이거 쓰는게 낫다.
    • java.util.function에 총 43개의 인터페이스가 담겨있다.
  • 기본 6개 인터페이스만 기억하면 나머지도 유추할 수 있다.
1
2
3
4
5
6
UnaryOperator<T>    T apply(T t)
BinaryOperator<T>   T apply(T t1, T t2)
Predicate<T>        boolean test(T t)
Function<T,R>       R apply(T t)
Supplier<T>         T get()
Consumer<T>         void accept(T t)
  • 기본 타입은 기본타입용 인터페이스를 제공하므로, 박싱된 기본 타입을 넣어서 쓰지 말고 그걸 써라.
  • 구조적으로 똑같은 표준 함수형 인터페이스가 있더라도, 직접 작성해야 하는 경우가 있다.
    • 자주 쓰이며, 이름 자체가 용도를 명확히 설명해 주는 경우
    • 이를 구현하면서 반드시 따라야 하는 규약이 있는 경우
    • 유용한 디폴트 메서드를 제공해야 하는 경우

아이템 45. 스트림은 주의해서 사용하라

  • 지연 평가(lazy evaluation)
    • 종단 연산에 쓰이지 않는 데이터 원소는 아예 중간 연산도 타지 않는다. (take(2)로 2개만 취할거면, 2개를 취한 다음에는 더 이상 연산 할 필요가 없으니까.)
    • 그냥 하나씩 꺼내서 filter,map 태우는 식으로 동작하기 때문에, for-if문 보다 느릴 것도, 빠를 것도 없다.
  • 스트림에 로직을 다 때려 넣다 보면 스트림이 너무 복잡해져 오히려 가독성이 떨어진다.
  • 스트림을 반환하는 메서드 이름은 원소의 정체를 알려주는 복수 명사로 쓰는 것을 강력히 추천한다.
1
2
3
static Stream<BigInteger> primes() {
    return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}

아이템 46. 스트림에서는 Side-Effect 없는 함수를 사용하라

아이템 47. 반환 타입으로는 스트림보다 컬렉션이 낫다

  • 왜냐면, 스트림은 Iterable을 extend하지 않아서 for-each 등에서 사용할 수 없기 때문.
    • Stream<E>Iterable<E>로 중개해주는 어댑터를 사용하면 for-each로 돌릴 수 있긴 하다
  • 반대로 Iterable도 마찬가지. Stream을 태울 수 없다.
    • 역시 Iterable<E>Stream<E>타입으로 변환해주는 어댑터를 사용하면 스트림을 태울 수 있다.
  • 근데 번거롭게 어댑터 쓸 필요 없이 그냥 Collection을 반환하면, 스트림도 반복도 지원된다!
    • 하지만 상황에 따라 그냥 스트림이나 Iterable을 반환하는게 더 나을 때도 있음. 컬렉션 구현이 애매한 경우나..

아이템 48. 스트림 병렬화는 주의해서 적용하라

  • 데이터 소스가 Stream.iterate인 경우 성능 개선을 기대하기 어렵다
    • parallel은 병렬로 실행하기 위해서 데이터 소스를 chunk 단위로 자르는데, iterate는 순차적으로 다음 요소를 반환받는 방식이라 chunk 단위로 자르기 어렵다
  • 중간 연산 limit 또는 findFirst같이 요소의 순서에 의존하는 연산을 쓰는 경우 성능 개선을 기대하기 어렵다
    • limit 대신 rangeClosed를 쓰자. LongStream.rangeClosed(1, n)
    • 특히 limit 같은 것 쓸 때, 소수 찾기 처럼 나중 갈 수록 연산이 오래걸리는 케이스는 더 주의해야 한다.
      • 10개의 소수 찾기를 쿼드코어 환경에서 돌린다고 가정하면
      • 10번째 소수를 찾는 마지막 연산에서 남는 CPU 코어가 11번째, 12번째, 13번째 소수를 찾는 연산을 시작한 다음 나중에 11,12,13번째 소수를 버린다. 너무 비효율적이고 심각하게 오래걸린다.
  • 박싱/언박싱 문제는 성능에 큰 영향을 미친다. 병렬 스트림에 잘 맞는 소스는 다음과 같다.
    • 기본형 스트림 (IntStream 등)
    • ArrayList, HashMap, HashSet, ConcurrentHashMap
    • SplittableRandom
      • 보통 ThreadLocalRandom을 쓰는데, 병렬 스트림에는 SplittableRandom을 쓴다.
This post is licensed under CC BY 4.0 by the author.