• 스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성하는 부분이다.
  • 이때 각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수 함수여야 한다.
  • 순수 함수란 오직 입력만이 결과에 영향을 주는 함수를 말한다.
  • 다른 가변 상태를 참조하지 않고 함수 스스로도 다른 상태를 변경하지 않아야 하는데
  • 이렇게 하려면 스트림 연산에 건네는 함수 객체는 모든 부작용이 없어야 한다.

 

//잘못 사용한 스트림
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()){
    words.forEach(word -> {
       freq.merge(word.toLowerCase(), 1L, Long::sum); 
    });
}

 

  • 스트림, 람다, 메소드 참조를 사용했지만 스트림코드라고 부를 수 없다.
  • 스트림 API의 이점을 사릴지 못하여 같은 기능의 반복적 코드보다 길고, 읽기 어렵고, 유지보수에도 좋지않다.
  • 이 코드의 모든 작업이 종단연산인 forEach에서 일어나면서 외부 타입 변경시에 문제가 생길 수 있다.

 

//바르게 고친 스트림
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()){
    freq = words
        .collect(groupingBy(String::toLowerCase, counting()));
}

forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고 계산하는 데는 사용하지 말자

 

 

Collector(수집기)

  • collector는 스트림의 원소들을 객체 하나에 취합한다는 듯이다.
  • 수집기가 생성하는 객체는 일반적으로 컬렉션이다.
  • 수집기는 총 3가지이다.
    • toList()
    • toSet()
    • toCollection

 

//빈도표에서 가장 흔한 단어 10개를 뽑아내는 스트림 파이프라인

List<String> topTen = freq.keySet().stream()
    .sorted(comparing(freq::get).reversed())
    .limit(10)
    .collect(toList());

 

  • ket 추출 함수로 쓰인 freq::get은 입력받은 단어를 빈도표에서 찾아 그 빈도를 반환한다.
  • reverse는 정렬이다.(내림차순)

 

//가장 간단한 맵 수집기 toMap(keyMapper, valueMapper)
//toMap 수집기를 사용하여 문자열을 열거 타입 상수에 매핑

private static final Map<String, Operation> stringToEnum =
    Stream.of(values()).collect()
        toMap(Object:;toString, e->e));

 

  • 열거 타입 상수의 문자열 표현을 열거 타입 자체에 매핑하는 fromString을 구현한 것
  • 스트림의 각 원소가 고유한 키에 매핑되어 있을 때 적합하다.

 

 

 

요소를 그룹핑해서 수집

  • 스트림은 컬렉션의 요소들을 그룹핑해서 Map 객체를 생성하는 기능도 제공한다.
  • collect()를 호출할 때 Collectors의 groupingBy()또는 groupingByConcurrent()가 리턴하는 Collector를 매개값으로 대입하면 된다.
    • groupingBy()는 스레드에 안전하지않은 Map을 생성한다.
    • groupingByConcurrent()는 스레드에 안전한 ConcurrentMap을 생성한다.

 

 

다음 코드는 학생들을 성별로 그룹핑하고 나서, 같은 그룹에 속하는 학생 List를 생성한 후, 성별을 키로, 학생 List를 값으로 갖는 Map을 생성한다.

//1. 전체 학생 List에서 Stream을 얻는다.
Stream<Student> totalStream = totlaList.stream();

//2. Student를 Student.Sex로 매핑하는 Function을 얻는다.
Function<Student, Student.Sex> classifier = Student :: getSetx;

//3. Student.Sex가 키가 되고 그룹핑된 List<Student>가 값인 Map을 생성하는 Collector를 얻는다.
Collector<Strudent, ?, Map<Student.Sex, List<Student>>> collector=
    Collectors.groupingBy(classifier);

//4. Stream의 collect() 메소드로 Student를 Student.Sex별로 그룹핑해서 Mpa을 얻는다.
Map<Student.Sex, List<Student>> mapBySex = totlaStream.collect(collector);
//위의 코드를 간단히 표현 한 식
Map<Student.Sex, List<Student>> mapBySex = totlaList.stream()
    .collect(Collectors.GroupingBy(Student :: getsex));

 

 

 

정리

  • 스트림 파이프라인 프로그래밍의 핵심은 부작용 없는 함수 객체에 있다.
  • 스트림 뿐만 아니라 스트림 관련 객체에 건네지는 모든 함수 객체가 부작용이 없어야한다.
  • forEach는 출력용으로만 사용하라
  • 스트림을 올바로 사용하려면 수집기를 잘 알아둬야 한다.
  • 가장 중요한 수집기 패곹리는 toList, toSet, tomap, groupingBy, joining이다.

+ Recent posts