[Java] 스트림과 병렬 처리 ⑥

kiteB·2022년 3월 7일
0

Java2

목록 보기
21/36
post-thumbnail

[ 수집 (collect()) ]

스트림은 요소들을 필터링 또는 매핑한 후 요소들을 수집하는 최종 처리 메소드인 collect()를 제공하고 있다.

  • collect() 메소드를 이용하면 필요한 요소만 컬렉션으로 담을 수 있고, 요소들을 그룹핑한 후 집계(리덕션)할 수 있다.

1. 필터링한 요소 수집

Stream의 collect(Collector<T, A, R> collect) 메소드는
필터링 또는 매핑된 요소들을 새로운 컬렉션에 수집하고, 이 컬렉션을 리턴한다.

매개값인 Collector(수집기)는 어떤 요소를 어떤 컬렉션에 수집할 것인지를 결정한다.
Collectors의 타입 파라미터 T요소, A누적기, R요소가 저장될 컬렉션을 의미한다.
즉, Collector<T, A, R>요소를 A누적기가 R에 저장한다는 의미이다.


✅ Collector 클래스의 정적 메소드

Collector의 구현 객체는 다음과 같이 Collectors 클래스의 다양한 정적 메소드를 이용해서 얻을 수 있다.

  • 리턴값인 Collector의 A(누적기)가 ?로 되어 있는 것은 Collector가 R(컬렉션)에 T(요소)를 저장하는 방법을 알고 있어 A(누적기)가 필요 없음을 의미한다.
  • Map은 스레드에 안전하지 않고, ConcurrentMap은 스레드에 안전하다.
    그러므로 멀티 스레드 환경에서 사용하려면 ConcurrentMap을 얻는 것이 좋다.

✅ 예제 | 필터링해서 새로운 컬렉션 생성

전체 학생 리스트 중에서 남학생들만 필터링해서 별도의 List로 생성하고 싶을 때, 다음의 코드를 이용하면 된다.

1. Stream<Student> totalStream = totalList.stream();
2. Stream<Student> maleStream = totalStream.filter(s -> s.getSex() == Student.Sex.MALE);
3. Collector<Student, ?, List<Student>> collector = Collectors.toList();
4. List<Student> maleList = maleStream.collect(collector); 
  1. 전체 학생 List에서 Stream을 얻는다.
  2. 남학생만 필터링해서 Stream을 얻는다.
  3. List에 Student를 수집하는 Collector를 얻는다.
  4. Stream에서 collect() 메소드로 Student를 수집해서 새로운 List를 얻는다.

위의 코드에서 변수를 생략하면 다음과 같이 간단하게 작성할 수 있다.

Set<Student> femaleSet = totalList.stream()
        .filter(s -> s.getSex() == Student.Sex.FEMALE)
        .collect(Collectors.toCollection(HashSet::new));

전체 코드

  • Student
public class Student {
    public enum Sex {MALE, FEMALE}
    public enum City {Sedoul, Incheon}

    private String name;
    private int score;
    private Sex sex;
    private City city;

    public Student(String name, int score, Sex sex) {
        this.name = name;
        this.score = score;
        this.sex = sex;
    }

    public Student(String name, int score, Sex sex, City city) {
        this.name = name;
        this.score = score;
        this.sex = sex;
        this.city = city;
    }

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }

    public Sex getSex() {
        return sex;
    }

    public City getCity() {
        return city;
    }
}
  • ToListExample
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class ToListExample {
    public static void main(String[] args) {
        List<Student> totalList = Arrays.asList(
                new Student("김철수", 10, Student.Sex.MALE),
                new Student("이영희", 6, Student.Sex.FEMALE),
                new Student("박철수", 10, Student.Sex.MALE),
                new Student("최영희", 6, Student.Sex.FEMALE)
        );

        //남학생들만 묶어서 Lit 생성
        List<Student> maleList = totalList.stream()
                .filter(s -> s.getSex() == Student.Sex.MALE)
                .collect(Collectors.toList());
        maleList.stream()
                .forEach(s -> System.out.println(s.getName()));
        System.out.println();

        //여학생들만 묶어서 HashSet 생성
        Set<Student> femaleSet = totalList.stream()
                .filter(s -> s.getSex() == Student.Sex.FEMALE)
                .collect(Collectors.toCollection(HashSet::new));
        femaleSet.stream()
                .forEach(s -> System.out.println(s.getName()));

    }
}
  • 실행 결과
김철수
박철수

이영희
최영희

2. 사용자 정의 컨테이너에 수집하기

사용자 정의 컨테이너 객체에 수집하는 방법에 대해 알아보자.

스트림은 요소들을 필터링, 또는 매핑해서 사용자 정의 컨테이너 객체에 수집할 수 있도록 다음과 같이 collect() 메소드를 추가적으로 제공한다.

  • Supplier요소들이 수집될 컨테이너 객체(R)를 생성하는 역할을 한다.
    • 순차 처리(싱글 스레드) 스트림에서는 단 한 번 Supplier가 실행되고 하나의 컨테이너 객체를 생성한다.
    • 병렬 처리(멀티 스레드) 스트림에서는 여러 번 Supplier가 실행되고 스레드별로 여러 개의 컨테이너 객체를 생성한다.
      하지만 최종적으로 하나의 컨테이너 객체로 결합된다.
  • XXXConsumer컨테이너 객체(R)에 요소(T)를 수집하는 역할을 한다.
    • 스트림에서 요소를 컨테이너에 수집할 때마다 XXXConsumer가 실행된다.
  • BiConsumer컨테이너 객체(R)를 결합하는 역할을 한다.
    • 순차 처리 스트림에서는 호출되지 않고, 병렬 처리 스트림에서만 호출되어
      스레드별로 객체를 결합해서 최종 컨테이너 객체를 완성한다.
  • 리턴 타입 R요소들이 최종 수집된 객체이다.
    • 순차 처리 스트림에서는 리턴 객체가 첫 번째 Supplier가 생성한 객체지만,
    • 병렬 처리 스트림에서는 최종 결합된 컨테이너 객체가 된다.

✅ 순차 처리를 이용해서 사용자 정의 객체에 요소를 수집

학생들 중에서 남학생만 수집하는 MaleStudent 컨테이너가 다음과 같이 정의되어 있다고 가정해보자.

  • MaleStudent (남학생이 저장되는 컨테이너)
import java.util.ArrayList;
import java.util.List;

public class MaleStudent {

    private List<Student> list; //요소를 저장할 컬렉션

    public MaleStudent() {
        list = new ArrayList<Student>();
        System.out.println("[" + Thread.currentThread().getName() + "] MaleStudent()");
    }

    public void accumulate(Student student) {   //요소를 수집하는 메소드
        list.add(student);
        System.out.println("[" + Thread.currentThread().getName() + "] accumulate");
    }

    public void combine(MaleStudent other) {    //두 MaleStudent를 결합하는 메소드 (병렬 처리 시에만 호출)
        list.addAll(other.getList());
        System.out.println("[" + Thread.currentThread().getName() + "] combine()");
    }

    public List<Student> getList() {    //요소가 저장된 컬렉션을 리턴
        return list;
    }
}
  • list 필드는 남학생들이 수집될 필드이다.

스트림에서 읽은 남학생을 MaleStudent에 수집하는 코드는 다음과 같다.

1. Stream<Student> totalStream = totalList.stream();
2. Stream<Student> maleStream = totalStream.filter(s -> s.getSex() == Student.Sex.MALE);
3. Supplier<MaleStudent> supplier = () -> new MaleStudent();
4. BiConsumer<MaleStudent, Student> accumulator = (ms, s) -> ms.accumulate(s);
5. BiConsumer<MaleStudent, MaleStudent> combiner = (ms1, ms2) -> ms1.combine(ms2);
6. MaleStudent maleStudent = maleStream.collect(supplier, accumulator, combiner);
  1. 전체 학생 List에서 Stream을 얻는다.
  2. 남학생만 필터링해서 Stream을 얻는다.
  3. MaleStudent를 공급하는 Supplier를 얻는다.
  4. MaleStudent와 Student를 매개값으로 받아서 MaleStudent의 accumulate() 메소드로
    Student를 수집하는 BiConsumer 얻는다.
  5. 두 개의 MaleStudent를 매개값으로 받아 combine() 메소드로 결합하는 BiConsumer를 얻는다.
  6. supplier가 제공하는 MaleStudent에 accumulator가 Student를 수집해서 최종 처리된 MaleStudent를 얻는다.

참고 | 싱글 스레드에서는 combiner가 사용되지 않는다.


위의 코드에서 변수를 생략하면 다음과 같이 간단하게 작성할 수 있다.

MaleStudent maleStudent = totalSList.stream()
        .filter(s -> s.getSex() == Student.Sex.MALE)
        .collect(
                () -> new MaleStudent(),
                (r, t) -> r.accumulate(t),
                (r1, r2) -> r1.combine(r2)
        );

람다식을 메소드 참조로 변경하면 다음과 같이 더욱 간단하게 작성할 수 있다.

MaleStudent maleStudent = totalList.stream()
        .filter(s -> s.getSex() == Student.Sex.MALE)
        .collect(MaleStudent::new, MaleStudent::accumulate, MaleStudent::combine);

✅ 예제 | 순차 스트림 이용해서 MaleStudent에서 남학생 수집

  • MaleStudentExample
import java.util.Arrays;
import java.util.List;

public class MaleStudentExample {
    public static void main(String[] args) {
        List<Student> totalList = Arrays.asList(
                new Student("김철수", 10, Student.Sex.MALE),
                new Student("이영희", 6, Student.Sex.FEMALE),
                new Student("박철수", 10, Student.Sex.MALE),
                new Student("최영희", 6, Student.Sex.FEMALE)
        );

        MaleStudent maleStudent = totalList.stream()
                .filter(s -> s.getSex() == Student.Sex.MALE)
                .collect(MaleStudent::new, MaleStudent::accumulate, MaleStudent::combine);

        maleStudent.getList().stream()
                .forEach(s -> System.out.println(s.getName()));
    }
}
  • 실행 결과
[main] MaleStudent()
[main] accumulate
[main] accumulate
김철수
박철수
  • 실행 결과를 보면 순차 처리를 담당한 스레드는 main 스레드임을 알 수 있다.
  • MaleStudent() 생성자가 딱 한 번 호출되었기 때문에 한 개의 MaleStudent 객체가 생성되었으며, accumulate()는 두 번 호출되었기 때문에 요소들이 2번 수집되었다.
    그래서 collect()가 리턴한 최종 MaleStudent에는 남학생 두 명이 저장되어 있는 것을 볼 수 있다.

[ 참고자료 ]

이것이 자바다 책
http://tcpschool.com/java/java_stream_terminal

profile
🚧 https://coji.tistory.com/ 🏠

0개의 댓글