본문 바로가기

책, 강의 정리/이펙티브자바

[아이템 45] 스트림은 주의해서 사용하라

스트림: 데이터 원소의 유한 또는 무한 시퀀스

스트림 파이프라인: 원소들을 수행하는 연산 단계를 표현하는 개념

대표적인 스트림의 원소들의 출처

ㄱ. 컬렉션

default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
}

ㄴ. 배열

public static <T> Stream<T> stream(T[] array) {
            return stream(array, 0, array.length);
}

ㄷ. 파일

public static Stream<Path> list(Path dir) throws IOException

ㄹ. 정규표현식 패턴매처 - Pattern.splitAsStream

public Stream<String> splitAsStream(final CharSequence input) {

ㅁ. 난수생성기 - Random.doubles()

public DoubleStream doubles() {
    return StreamSupport.doubleStream
            (new RandomDoublesSpliterator
                     (this, 0L, Long.MAX_VALUE, Double.MAX_VALUE, 0.0),
             false);
}

ㅁ. 기본값 - IntStream, DoubleStream, LongStream

스트림 파이프라인의 특징

스트림 파이프라인은 소스스트림 -> (중간연산) -> 종단연산 으로 이루어진다

중간연산을 합친 다음에 합쳐진 중간연산을 최종 연산으로 한번에 처리 -> Lazy

중간연산

스트림을 변환(transform). 결과스트림의 원소 타입은 시작 스트림의 원소 타입과 같을 수도, 다를 수도 있다.

ㄱ. sorted

Stream<Integer> sorted = operands.stream().sorted();

ㄴ. filter

Stream<Integer> integerStream = operands.stream().filter((value) -> value > 2);

ㄷ. map

Stream<Double> doubleStream = operands.stream().map(Double::new);

종단연산

마지막 중간 연산의 스트림에 최후의 연산. 1개 이상의 중간연산들은 계속합쳐진 후 종단연산 시 수행된다.

즉, 스트림 파이프라인은 지연평가(lazy evaluation)된다.

  • 종단연산이 없는 파이프라인은 어떤 연산도 수행되지 않는다.
  • 지연평가는 무한스트림을 다룰 수 있게 해주는 열쇠다.
    • 지연평가를 하지 않는다면 중간연산은 끝나지 않는다.

스트림은 주의해서 사용해야한다?

대부분의 연산은 스트림으로도 구현할 수 있다.

하지만 과도한 스트림은 읽기 어렵고 유지보수가 힘들다. 또, 성능상 좋지 않을 수도 있다.

다음의 코드는 과도한 스트림때문에 읽기 어렵다.

public void anagram(){
        List<String> dictionary = Arrays.asList("Lee MinHyeong", "Seo Jaeyeon", "CRUD", "Minjeong", "PCI", "ICP");

        dictionary.stream()
                .collect(groupingBy( // Map의 Key
                    word -> word.chars().sorted() // char 배열을 정렬해서
                    .collect(StringBuffer::new, (stringBuffer, value) -> stringBuffer.append((char)value), StringBuffer::append).toString()))
                .values().stream() // Map의 Value들
                .filter(group -> group.size() >= 2)
                .map(group -> group.size() +": " + group)
                .forEach(System.out::println);


    }

무조건 스트림만 쓰는 것이 아닌 절중 지점을 찾아야한다.

또, 스트림의 변수는 람다형이기 때문에 스트림 변수를 이해하기 쉽게 짓는 것도 중요하다.

자바는 char용 스트림을 지원하지 않는다

IntStream chars = "Hello".chars(); // IntStream이 반환된다.

스트림을 사용하지 못할 때 - 함수객체 사용 관점에서

  1. 지역변수를 읽고 수정할 필요가 있을 때
    • 람다의 변수는 사실상 final이다.
  2. 람다는 return, break, continue 문이 불가능하다

스트림이 적절할 때

  1. 원소들의 시퀀스를 일관성 있게 변환할 때
  2. 원소들의 시퀀스를 필터링할 때
  3. 원소들의 시퀀스를 연산 후 결합할 때
  4. 원소들의 시퀀스를 모을 때
  5. 원소들의 시퀀스 중 특정 조건을 만족하는 원소를 찾을 때

스트림으로 처리하기 어려울 때

  1. 원본 스트림을 계속 써야할 때
    • 스트림은 중간연산을 지나고 나면 원래의 스트림을 잃는다. 파이프라인의 순서를 바꿈으로써 해결할 수 있는지 고민해보자.