Java/Effective Java 3E

[이펙티브자바 3판] ITEM46. 스트림에는 부작용 없는 함수를 사용하라

잭피 2020. 12. 5. 11:17

이번장의 핵심은...

스트림 파이프 라인 프로그래밍 핵심은 부작용 없는 함수 객체입니다 

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”로 메일을 보내주시면, 바로 삭제하도록 하겠습니다.