자바8에서 함수형 인터페이스, 람다, 메더스 참조 개념과 함께 스트림API가 추가됨. 이기능들을 효과적으로 사용하는 방법을 알아봄
아이템 42. 익명 클래스보다는 람다를 사용하라
- 함수 객체를 만드는 주요수단으로 익명 클래스가 사용됨
- 전략 패턴처럼, 함수 객체를 사용하는 과거 객체 지향 디자인 패턴에서는 익명 클래스면 충분했음
- 자바 특성상 익명 클래스 방식은 코드가 너무 길기 때문에 자바는 함수형 프로그로그래밍에 적합하지 않았음
- 자바8에 와서 추상 메서드 하나짜리 인터페이스가 대우를 받게됨
- 함수형 인터페이스라 부르는 이 인터페이스의 인스턴스를 람다식을 사용해 생성 가능해짐
// 익명 클래스
Collection.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
// 람다식
Collection.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
// 비교자 생성 메서드
Collection.sort(words, comparingInt(String::length));
// List 인터페이스에 추가된 sort
words.sort(comparingInt(String::length));
- 타입을 명시해야하는 코드가 더 명확할 때만 제외하고는, 람다의 모든 매개변수 타입은 생략하자
- 람다는 이름이 없고 문서화도 할 수 없음
- 따라서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 사용하면 안됨
아이템 43. 람다보다는 메서드 참조를 사용하라
- 람다가 익명 클래스보다 나은점 중 가장 큰 특징은 간결함
- 그러나 자바에는 함수 객체를 람다보다 더 간결하게 만드는 방법이 존재, 이 방법이 메서드 참조(method reference)
// 람다
map.merge(key, 1, (count, incr) -> count + incr);
// 메서드 참조
map.merge(key, 1, Integer::sum);
- 메서드 참조쪽이 더 짧고 명확하면 메서드 참조, 그렇지 않으면 람다를 사용
아이템 44. 표준 함수형 인터페이스를 사용하라
- util.function 패키지에 다양한 용도의 표준 함수형 인터페이스가 존재
- 필요한 용도에 맞느게 존재한다면, 직접 구현하지 말고 표준 함수형 인터페이스 활용을 권장
- 직접 만든 함수형 인터페이스에는 항상 @FunctionInterface 애너테이션을 사용해야함
아이템 45. 스트림은 주의해서 사용하라
- 스트림 API는 다량의 데이터 처리 작업(순차적이든 병렬적이든)을 돕고자 자바8에 추가됨
- 이 API가 제공하는 추상 개념 중 핵심은 두가지
- 스트림은 데이터 원소의 유한 또는 무한 시퀀스를 의미
- 스트림 파이트라인은 이 원소들로 수행하는 연산의 단계를 표현하는 개념
- 스트림을 과용하면 프로그램이 읽거나 유지보수하기 어려워짐
- 기존 코드는 스트림을 사용하도록 리팩터링하되, 새 코드가 더 나아 보일 때만 반영
// 반복버전의 데카르트 곱
private static List<Card> newDeck() {
List<Card> result = new ArrayList<>();
for (Suit suit : Suit.values())
for (Rank rank : Rank.values())
result.add(new Card(suit, rank));
return result;
}
// 스트림버전의 데카르트 곱
private static List<Card> newDeck() {
return Stream.of(Suit.values())
.flatMap(suit ->
Stream.of(Rank.values())
.map(rank -> new Card(suit, rank)))
}
아이템 46. 스트림에서는 부작용 없는 함수를 사용하라
- 스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성하는 부분
- 이때 각 단계는 가능한 이전 단계의 결과를 받아 처리하는 순수함수이어야함
- 다음은 주어진 파일에 존재하는 단어를 카운팅하는 예제
// 스트림 패러다임을 이해하지 못한채 사용된 API
// 스트림, 람다, 메서드 참조를 사용했고 결과 또한 올바름
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word ->
freq.merge(word.toLowerCase(), 1L, Long::sum);
)
}
// 스트림을 제대로 활용한
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(groupingBy(String::toLowerCase, counting()));
}
아이템 47. 반환 타입으로는 스트림보다 컬렉션이 낫다
- 자바7 까지는 시퀀스, 일련의 원소반환을 위해 Collection, Set, List같은 컬렉션 인터페이스 또는 Interable이나 배열을 사용
- 기본은 컬렉션, for-each나 컬렉션을 사용할 수 없으면 Iterable, 성능이 민감한 상황이면 배열을 사용함
- 자바8에서 스트림이라는 개념이 존재하면서 이 선택이 아주 복잡한 일이 되어버림
- 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection이나 그 하위 타입을 쓰는 게 일반적으로 최선
- Collection 인터페이스는 Iterable의 하위타입
- stream 메서드 지원, 즉 반복과 스트림을 동시에 지원
- 원소개수가 적다면 ArrayList와 같은 표준 컬렉션이 적합
- 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안됨
아이템 48. 스트림 병렬화는 주의해서 적용하라
- 자바는 동시성 처리에 대해 항상 앞서감
- 자바8 부터 parallel 메서드만 한번 호출하면 파이프라인을 병렬 실행할 수 있는 스트림을 지원
- 자바로 동시성 프로그램을 작성하기가 점점 쉬워지고있지만 이를 올바르고 빠르게 작성하는 일은 여전히 어려움
- 데이터 소스가 Stream.iterate거나 중간 연산으로 limit를 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없음
- 스트림 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스이거나 배열, int 범위, long 범위 일때 병렬화의 효과가 가장 좋음
- 스트림을 잘못 병렬화하면 성능이 나빠질 뿐만 아니라 결과가 잘못되거나 예상 못한 동작이 발생할 수 있음