스트림 연산에 건네는 함수 객체는 모두 side effect(부작용)이 없어야 한다!!
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 = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()){
freq = words.collect(groupingBy(String::toLowerCase, counting()))
}
java.util.stream.Collectors 클래스의 메서드를 사용하는데, 스트림의 원소들을 축소해서 객체 하나에 모아주는 역할을 한다. toList() : 스트림 원소를 list 에 담는다.toSet() : 스트림 원소를 set 에 담는다.toCollection(collectionFactory) : 프로그래머가 지정한 타입에 담는다.List<String> topTen = freq.keySet().stream()
.sorted(comparing(freq::get).reversed())
.limit(10)
.collect(toList());
위의 toList()는 Collectors의 메서드로 정적 임포트하여 사용하고 있다. 이렇듯 정적 임포트를 사용하면 코드 가독성이 좋아진다. collect(Collectors.toList()) → collect(toList())
toMap(keyMapper, valueMapper)Key에 매핑되는 keyMapper(함수)와 Value에 매핑되는 valueMapper(함수)를 인수로 받는다.toMap()은 스트림 원소가 다수의 같은 키를 사용하는 경우 IllegalStateException을 던진다. 주의하자.public class Artist {
private String name;
private String nickname;
public Artist(String name, String nickname) {
this.name = name;
this.nickname = nickname;
}
public String getName() {
return name;
}
public String getNickname() {
return nickname;
}
@Override
public String toString() {
return name;
}
}
public class Album {
private Artist artist;
private int sales;
public Album(Artist artist, int sales) {
this.artist = artist;
this.sales = sales;
}
public Artist artist() {
return artist;
}
public int sales() {
return sales;
}
@Override
public String toString() {
return Integer.toString(sales);
}
}
public class StreamToMapMain {
public static void main(String[] args) {
Artist a = new Artist("a", "11");
Artist b = new Artist("a", "22");
Artist c = new Artist("c", "22");
List<Artist> artists = List.of(a,b,c);
artists.stream()
.collect(toMap(Artist::getName, Artist::getNickname));
}
}

public class StreamToMapMain {
public static void main(String[] args) {
Artist a = new Artist("a", "11");
Artist b = new Artist("b", "22");
Artist c = new Artist("c", "22");
List<Album> albums = List.of(
new Album(a, 21),
new Album(a, 11),
new Album(b, 12),
new Album(c, 28)
);
Map<Artist, Album> topHits = albums.stream().collect(
toMap(Album::artist,
album -> album,
maxBy(comparing(Album::sales))
)
);
System.out.println(topHits);
}
}

위코드는 비교자로는 BinaryOperator에서 정적 임포트한 maxBy라는 정적 팩토리 메서드를 사용했습니다. maxBy는 Comparator<T>를 입력받아 BinaryOperator<T>를 반환합니다.
인수가 3개인 toMap은 충돌이 나면 마지막 값을 취하는(last-write-wins) 수집기를 만들 때도 유용합니다.
toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
public class StreamToMapMain {
public static void main(String[] args) {
Artist a = new Artist("a", "11");
Artist b = new Artist("b", "22");
Artist c = new Artist("c", "22");
List<Album> albums = List.of(
new Album(a, 21),
new Album(a, 11),
new Album(b, 12),
new Album(c, 28)
);
Map<Artist, Album> topHits = albums.stream().collect(
toMap(Album::artist,
album -> album,
(aa, bb)-> bb
)
);
System.out.println(topHits);

toMap()은 마지막 인수로 맵 팩토리를 받습니다. 이 인수로는 EnumMap이나 TreeMap처럼 원하는 특정 맵 구현체를 직접 지정할 수 있습니다. TreeMap<Artist, Album> topHitsTree = albums.stream().collect(
toMap(Album::artist,
album -> album,
maxBy(comparing(Album::sales)),
TreeMap::new
)
);
class BlogPost {
String title;
String author;
BlogPostType type;
int likes;
}
enum BlogPostType {
NEWS,
REVIEW,
GUIDE
}
List<BlogPost> posts = Arrays.asList( ... );
Map<BlogPostType, List<BlogPost>> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType));
public class StreamToMapMain {
public static void main(String[] args) {
List<BlogPost> posts = List.of(
new BlogPost("제목1","저자1", NEWS, 1),
new BlogPost("제목2","저자2", REVIEW, 1),
new BlogPost("제목3","저자3", GUIDE, 1),
new BlogPost("제목4" ,"저자4", GUIDE, 1)
);
// list 로 매핑
Map<BlogPostType, List<BlogPost>> collect1 = posts.stream()
.collect(groupingBy(BlogPost::getType));
// set 으로 매핑
Map<BlogPostType, Set<BlogPost>> collect2 =posts.stream()
.collect(groupingBy(BlogPost::getType, toSet()));
// Collection 으로 custom 매핑
Map<BlogPostType, LinkedHashSet<BlogPost>> collect3 = posts.stream()
.collect(groupingBy(BlogPost::getType, toCollection(LinkedHashSet::new)));
// 원소의 개수로 매핑
Map<BlogPostType, Long> collect4 = posts.stream()
.collect(groupingBy(BlogPost::getType, counting()));
}
}
TreeMap<BlogPostType, Long> collect5 = posts.stream()
.collect(groupingBy(BlogPost::getType, TreeMap::new, counting()));
joining() 메서드는 (문자열 등의) CharSequence 인스턴스의 스트림에만 적용할 수 있습니다.
인수가 없는 joining은 단순히 원소들을 연결(concatenate)하는 수집기를 반환합니다.
인수가 하나짜리 joining은 CharSequence 타입의 구분문자(delimiter)를 매개변수로 받습니다. 연결 부위에 이 구분문자를 삽입하는데, 구분문자로 쉼표(,)를 입력하면 CSV 형태의 문자열을 만들어줍니다(단, 스트림에 쉼표를 이미 포함한 원소가 있다면 구분문자와 구별되지 않으니 유의해야 합니다).
인수가 3개짜리 joining은 구분문자에 더해 접두문자(prefix)와 접미문자(suffix)도 받습니다.
public class Test {
public static void main(String[] args) {
List<String> stringList = List.of("Hello", "World", "!!");
// 인수가 없는 경우
String collect1 = stringList.stream().collect(joining());
System.out.println(collect1);
// 인수가 1개인 경우
String collect2 = stringList.stream().collect(joining(","));
System.out.println(collect2);
// 인수가 3개인 경우
String collect3 = stringList.stream().collect(joining(",","[","]"));
System.out.println(collect3);
}
}

스트림 파이프라인 프로그래밍의 핵심은 부작용(side effect)없는 함수 객체이다!!
스트림을 올바르게 사용하려면 수집기를 잘 알아두자!!(toList, toSet, toMap, groupingBy, joining)