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

Jimin Lim·2023년 6월 29일
0

Effective Java

목록 보기
29/38
post-thumbnail
post-custom-banner

아이템 45

스트림은 주의해서 사용하라

스트림 API는 다량의 데이터 처리작업을 돕고자 나왔다. 이 API의 핵심 개념은 다음과 같다.

  1. 스트림: 데이터 원소의 유한 혹은 무한 시퀀스
  2. 스트림 파이프라인: 이 원소들로 수행하는 연산 단계를 표현하는 개념

✅ 스트림 특징

1️⃣ 구성

스트림 파이프라인은 소스 스트림, 중간 연산(없을 수도), 종단 연산으로 구성된다.

중간 연산
각 원소에 함수 적용을 하거나 특정 조건을 만족 못하는 원소를 걸러낼 수 있다. 한 스트림을 다른 스트림으로 변환하는데 타입은 변환 전/후 다를 수도, 같을 수도 있다.

종단 연산
중간 연산이 내놓은 스트림에 최후 연산을 수행한다. 원소를 정렬해 컬렉션에 담거나, 특정 원소 하나를 선택하거나 모든 원소를 출력할 수 있다.

2️⃣ 지연 평가

평가는 종단 연산이 호출될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다. 즉, 종단 연산이 없는 스트림 파이프라인은 아무 일도 하지 않는 명령어와 같다.

3️⃣ 플루언트 API

메서드 연쇄를 지원한다. 즉, 파이프라인 하나를 구성하는 모든 호출을 연결해 단 하나의 표현식으로 완성할 수 있다.

4️⃣ 순차실행

파이프라인은 기본적으로 순차실행이며, parallel 메서드를 사용해 병렬적으로 실행할 수 있다. 하지만 이는 효과를 볼 수 있는 상황이 많지 않다 (아이템 48)

5️⃣ 참고 노하우

아래는 사전 파일에서 단어를 읽어 사용자가 지정한 값보다 원소 수가 많은 아나그램(철자를 구성하는 알파벳이 같고 순서만 다른 단어) 그룹을 출력하는 코드다.

public class Anagrams {
    public static void main(String[] args) throws IOException {
        File dictionary = new File(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);

        Map<String, Set<String>> groups = new HashMap<>();
        try (Scanner s = new Scanner(dictionary)) {
            while (s.hasNext()) {
                String word = s.next();
                groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word); //존재하지 않는다면 새롭게 추가
            }
        }

        for (Set<String> group : groups.values())
            if (group.size() >= minGroupSize)
                System.out.println(group.size() + ": " + group);
    }

    private static String alphabetize(String s) {
        char[] a = s.toCharArray();
        Arrays.sort(a);
        return new String(a);
    }
}

위 코드를 간략하게 스트림으로 나타낸다면, 아래와 같다.

public class Anagrams {
    public static void main(String[] args) throws IOException {
        Path dictionary = Paths.get(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);

        try (Stream<String> words = Files.lines(dictionary)) {
            words.collect(groupingBy(word -> alphabetize(word))) //모든 단어를 맵으로 모은다.
                    .values().stream()
                    .filter(group -> group.size() >= minGroupSize)
                    .forEach(g -> System.out.println(g.size() + ": " + g)); //종단연산
        }
    }

    private static String alphabetize(String s) {
        char[] a = s.toCharArray();
        Arrays.sort(a);
        return new String(a);
    }
}

위에서 alphabetize 또한 스트림으로 나타낼 수 있겠지만, char용 스트림을 지원하지 않아 형변환이 필요하다. (따라서 삼가는 편이 낫다.)

✅ 반복 코드와 비교

함수 객체(람다나 메서드 참조)로 할 수 없는, 반복 코드로 나타낼 수 있는 예시는 다음과 같다.

  1. 코드 블록은 범위 안의 지역변수를 읽고 수정할 수 있지만, 람다에서는 지역변수를 수정하는 건 불가능하고 final만 읽을 수 있다.
  2. 코드 블록에서는 return 문을 사용해 메서드를 빠져나가거나 break나 continue 문으로 블록 바깥의 반복문을 종료하거나 반복을 건너뛸 수 있다. 또 예외를 던질 수도 있다.

스트림이 안성맞춤인 경우는 다음과 같다.

  1. 원소들의 시퀀스를 일관되게 변환
  2. 시퀀스를 필터링
  3. 시퀀스를 하나의 연산을 사용해 결합
  4. 시퀀스를 컬렉션에 모음
  5. 시퀀스에서 특정 조건을 만족하는 원소를 찾음

✅ 사용 예시

스트림 연산은 한 값을 다른 값으로 매핑한다면 원래 값을 잃는다. 따라서 스트림 파이프라인의 여러 단계를 통과할 때, 각 단계에서의 값들에 동시 접근하기는 어려운 경우 스트림을 처리하기 힘들다.

🔗 예시: 반복문이 더 나은 경우

static Stream<BigInteger> primes() {
    return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
public static void main(String[] args) {
    primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE)) // p를 2^p-1로 변환
            .filter(mersenne -> mersenne.isProbablePrime(50))
            .limit(20)
            .forEach(System.out::println);
}

p가 소수이면서 2^p - 1도 소수라면 메르센 소수다. 이때 p를 최종적으로 출력하는 코드를 작성하고자 한다면 이미 파이프라인을 통과했기에 초기 값을 꺼내오긴 힘들다.

🔗 예시: 판단하기 힘든 경우

Suit와 Rank의 조합을 나타내는 코드다.

//반복문
private static List<Card> newDeck() {
    List<Card> result = new ArrayList<>();
    for (Suit suit : Suit.values())
        for (Rank rank : Rank.values())
            result.add(new Card(suit, rank));
    return result;
}
//Stream
private static List<Card> newDeck() {
    return Stream.of(Suit.values())
            .flatMap(suit ->
                    Stream.of(Rank.values())
                            .map(rank -> new Card(suit, rank)))
            .collect(toList());
}
  • flatMap: 스트림의 원소 각각을 다른 스트림과 매핑한 후 다시 하나의 스트림으로 합치는 것

위는 정말 취향차이다.. 따라서 숙련도에, 본인이 더 편리하다고 생각하는 방향으로 작성하자 !

profile
💻 ☕️ 🏝 🍑 🍹 🏊‍♀️
post-custom-banner

0개의 댓글