[이펙티브자바 3판] ITEM46. 스트림에는 부작용 없는 함수를 사용하라
이번장의 핵심은...
스트림 파이프 라인 프로그래밍 핵심은 부작용 없는 함수 객체입니다
forEach는 스트림이 수행한 계산 결과를 보고할 때만 이용하자 (계산 자체에는 이용하지 말자)
가장 중요한 수집기 팩터리는 [ toList, toSet, toMap, groupingBy, joining ]
스트림 패러다임
스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성하는 것입니다
이때 변환은 입력만이 결과에 영향을 주는 순수 함수여야 합니다
잘못된 스트림 연산의 예시를 볼까요?
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()){
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
위 코드는 스트림 코드를 가장한 반복코드블럭입니다
forEach는 연산결과를 보여주는 역할만 하는 종단 연산인데, 외부변수 freq를 수정하고 있습니다
또한, forEach 연산은 종단연산 중 기능이 가장 적고 덜 스트림 적입니다 (병렬화 할 수 없음)
forEach : forEach 연산은 스트림 계산 결과 보고할때만 사용하고, 계산할 때는 쓰지 말자
가끔 스트림 계산 결과를 기존 컬렉션에 추가하는 등의 다른 용도로는 사용가능합니다
그럼 위의 코드를 수정해볼까요?
// 수정
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()){
freq = words.collect(groupingBy(String::toLowerCase, counting()))
}
이제 스트림 API를 제대로 사용하는 코드라고 할 수 있습니다
수집기(collector)
collect 메소드는 스트림 종료 작업입니다 (Collector 타입의 인자를 받아서 처리함)
java.util.stream.Collectors 클래스에 있는 메서드들을 사용합니다 (39개)
스트림의 원소들을 객체 하나에 취합하는 행동을 합니다(축소(reduction) 전략)
몇 가지 종류를 살펴보죠
toList() - 스트림 원소를 List에 담습니다
toSet() - 스트림 원소를 Set에 담습니다
toCollection(collectionFactory) - 프로그래머가 지정한 컬렉션 타입에 담습니다
예제) 빈도표(freq)에서 가장 흔한 단어 10개를 뽑아내는 스트림 파이프라인
List<String> topTen = freq.keySet().stream()
.sorted(comparing(freq::get).reversed()) // 빈도수 반환해서 reversed 정렬
.limit(10)
.collect(toList()); // 결과로 리스트 반환
toMap()
스트림 원소를 Key-Value 형태로 재생산합니다
toMap(keyMapper, valueMapper)
스트림 원소를 키에 매핑하는 함수(keyMapper)와 값에 매핑하는 함수(valueMapper)를 인수로 받습니다
class Book {
private String name;
private int releaseYear;
private String isbn;
}
public Map<String, String> listToMap(List<Book> books) {
return books.stream().collect(Collectors.toMap(Book::getIsbn, Book::getName));
}
toMap(keyMapper, valueMapper, mergeMapper)
같은 키를 공유하는 값들이 있을 경우 입력받은 병합(merge)함수를 사용하여 기존값과 합칩니다
Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
-> albums 스트림을 맵으로 바꾸는데, 이 맵은 각 음악가와 그 음악가의 베스트 앨범을 묶어줍니다
toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
-> Key가 충돌하면 newVal로 취하는 수집기입니다
groupingBy
분류함수(classifier)를 입력받아서 출력으로 원소들을 분류기가 분류한 카테고리별로 모아놓은 맵을 반환
sql의 group by와 유사한 기능입니다
List<Product> productList = Arrays.asList(new Product(23, "potatoes"),
new Product(14, "orange"),
new Product(13, "lemon"),
new Product(23, "bread"),
new Product(13, "sugar"));
Map<Integer, List<Product>> collectorMapOfLists = productList.stream()
.collect(Collectors.groupingBy(Product::getAmount));
//실행 결과
//{23=[Product{amount=23, name='potatoes'},
// Product{amount=23, name='bread'}],
// 13=[Product{amount=13, name='lemon'},
// Product{amount=13, name='sugar'}],
// 14=[Product{amount=14, name='orange'}]}
Amount 값을 키로하여 같은 값이면 Product 인스턴스를 묶어서 리스트로 보여줍니다
인자로 다운스트림 수집기(down stream collector)를 같이 전달해주면 원소 리스트를 다른 걸로 바꿀 수 있습니다
words.collect(groupingBy(String::toLowerCase, counting())
각 카테고리에 속하는 원소의 개수를 매핑한 맵을 얻을 수 있습니다
partitioningBy()
분류함수 자리에 Predicate를 받으며 Map의 Key 타입이 Boolean입니다 (groupingBy와 비슷)
Stream<String> stream = Stream.of("HTML", "CSS", "JAVA", "PHP");
Map<Boolean, List<String>> patition = stream.collect(
Collectors.partitioningBy(s -> (s.length() % 2) == 0));
List<String> oddLengthList = patition.get(false);
System.out.println(oddLengthList);
List<String> evenLengthList = patition.get(true);
System.out.println(evenLengthList);
// 실행결과
// [CSS, PHP]
// [HTML, JAVA]
조건에 만족하는 것만 그룹핑 해줍니다
minBy(),maxBy()
수로 받은 비교자를 이용해 스트림에서 값이 가장 작은, 혹은 큰 원소를 찾아 반환
joining
문자열(CharSequence)에만 사용, 원소들을 연결합니다
joining()
단순 concat()입니다
joining(delimiter)
delimiter를 넣어서 연결합니다 ex) joining(",") : 양파,배추,...
joining(delimiter, prefix, suffix)
List<String> a = new ArrayList<>();
a.add("Jack");
a.add("Coding");
a.stream().collect(Collectors.joining(",","<",">"));
// <Jack,Coding>
이 글은 “이펙티브 자바 3판” 책 내용을 정리한 글입니다.
만약 저작권 관련 문제가 있다면 “shk3029@kakao.com”로 메일을 보내주시면, 바로 삭제하도록 하겠습니다.