[Moden Java] 스트림으로 데이터 수집

Collector 인터페이스

Collector 인터페이스를 상속해서 커스텀 컬렉션을 만들 수 있다. 다음은 Collector 인터페이스이다.

1
2
3
4
5
6
7
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
Function<A, R> finisher();
BinaryOperator<A> combiner();
Set<Characteristics> characteristics();
}
  • supplier : 결과를 위한 컨테이너 생성
  • accumulator : 결과 컨데이너에 요소 추가
  • finisher : 최종 변환값을 결과 컨테이너로 적용
  • combiner : 병렬로 처리될 때 서로 다른 두 결과 컨테이너 병합
  • characteristics : 컬렉터의 연산에 대한 힌트 제공
    • UNORDERED : 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않음
    • CONCURRENT : 다중 스레드에서 accumulator를 동시에 호출 가능
    • IDENTITY_FINISH : 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용 가능

가령 다음과 같은 클래스가 있다고 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Student {
private String name;
private Set<Subject> subjects;

public Student(String name, Set<Subject> subjects) {
this.name = name;
this.subjects = subjects;
}

public String getName() {
return name;
}

public Set<Subject> getSubjects() {
return subjects;
}
}

public enum Subject {
MATH,
ENGLISH,
MUSIC,
SCIENCE
}

학생들의 목록을 가지고 수업별로 수강하는 학생 이름 리스트를 만들고자 한다. 즉 List<Student>Map<Subject, List<String>>으로 만들고자 한다.
커스텀 컬렉션을 만드는 Collector를 사용하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class StudentSubjectCollector implements Collector<Student, Map<Subject, List<String>>, Map<Subject, List<String>>> {
@Override
public Supplier<Map<Subject, List<String>>> supplier() {
return () -> new HashMap<>() {{
put(Subject.MATH, new ArrayList<>());
put(Subject.ENGLISH, new ArrayList<>());
put(Subject.SCIENCE, new ArrayList<>());
put(Subject.MUSIC, new ArrayList<>());
}};
}

@Override
public BiConsumer<Map<Subject, List<String>>, Student> accumulator() {
return (Map<Subject, List<String>> result, Student student) -> {
for(Subject subject : student.getSubjects()) {
result.get(subject).add(student.getName());
}
};
}

@Override
public BinaryOperator<Map<Subject, List<String>>> combiner() {
return (Map<Subject, List<String>> map1, Map<Subject, List<String>> map2) -> {
for(Subject subject : map2.keySet()) {
map1.get(subject).addAll(map2.get(subject));
}

return map1;
};
}

@Override
public Function<Map<Subject, List<String>>, Map<Subject, List<String>>> finisher() {
return Function.identity();
}

@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
}
}
1
2
3
4
5
6
7
8
9
10
11
List<Student> students = Arrays.asList(
new Student("Kim", new HashSet<>(Arrays.asList(Subject.ENGLISH, Subject.MATH))),
new Student("Park", new HashSet<>(Arrays.asList(Subject.ENGLISH, Subject.MUSIC))),
new Student("Lee", new HashSet<>(Arrays.asList(Subject.SCIENCE, Subject.MATH))),
new Student("Choi", new HashSet<>(Arrays.asList(Subject.MUSIC, Subject.MATH))),
new Student("Song", new HashSet<>(Arrays.asList(Subject.SCIENCE, Subject.MUSIC))),
new Student("Kang", new HashSet<>(Arrays.asList(Subject.ENGLISH, Subject.MATH, Subject.MUSIC, Subject.SCIENCE)))
);

Map<Subject, List<String>> result = students.stream().collect(new StudentSubjectCollector());
Map<Subject, List<String>> parallelResult = students.parallelStream().collect(new StudentSubjectCollector());

다음은 이와 같은 동작을 하는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
List<Student> students = Arrays.asList(
new Student("Kim", new HashSet<>(Arrays.asList(Subject.ENGLISH, Subject.MATH))),
new Student("Park", new HashSet<>(Arrays.asList(Subject.ENGLISH, Subject.MUSIC))),
new Student("Lee", new HashSet<>(Arrays.asList(Subject.SCIENCE, Subject.MATH))),
new Student("Choi", new HashSet<>(Arrays.asList(Subject.MUSIC, Subject.MATH))),
new Student("Song", new HashSet<>(Arrays.asList(Subject.SCIENCE, Subject.MUSIC))),
new Student("Kang", new HashSet<>(Arrays.asList(Subject.ENGLISH, Subject.MATH, Subject.MUSIC, Subject.SCIENCE)))
);

Map<Subject, List<String>> res = students.stream()
.map(student -> {
Map<Subject, String> studentSubjects = new HashMap<>();
for (Subject subject : student.getSubjects()) {
studentSubjects.put(subject, student.getName());
}

return studentSubjects;
})
.flatMap(map -> map.entrySet().stream())
.collect(groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
Author: Song Hayoung
Link: https://songhayoung.github.io/2021/04/17/Languages/Modern%20Java/chapter6/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.