기록이 힘이다.

[이펙티브 자바] 아이템45 스트림은 주의해서 사용하라 본문

JAVA

[이펙티브 자바] 아이템45 스트림은 주의해서 사용하라

dev22 2023. 4. 7. 21:21
728x90

스트림 API는 다량의 데이터 처리 작업(순차적이든 병렬적이든)을 돕고자 자바8에 추가되었다.

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

스트림 파이프라인은 지연 평가된다.

기본적으로 스트림 파이프라인은 순차적으로 수행된다.

어떠한 계산이라도 해낼 수 있다. 하지만 잘못 사용하면 읽기 어렵고 유지보수도 힘들어 진다.

ex) 아나그램이란 철자를 구성하는 알파벳이 같고 순서만 다른 단어를 말한다.

package effectivejava.chapter7.item45.anagrams;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.stream.Stream;

import static java.util.stream.Collectors.groupingBy;

public class StreamAnagrams {
// 코드 45-1 사전 하나를 훑어 원소 수가 많은 아나그램 그룹들을 출력한다. (269-270쪽)
			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);
    }
// 코드 45-2 스트림을 과하게 사용했다. - 따라 하지 말 것! (270-271쪽)
    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 -> **word.chars().sorted()
                            .collect(StringBuilder::new,
                                    (sb, c) -> sb.append((char) c),
                                    StringBuilder::append).toString()**))
                    .values().stream()
                    .filter(group -> group.size() >= minGroupSize)
                    .map(group -> group.size() + ": " + group)
                    .forEach(System.out::println);
        }
    }
}
// 코드 45-3 스트림을 적절히 활용하면 깔끔하고 명료해진다. (271쪽)
public class HybridAnagrams {
    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);
    }
}

람다에서는 타입 이름을 자주 생략하므로 매개변수 이름을 잘 지어야 스트림 파이프라인의 가독성이 유지된다.

기존 코드는 스트림을 사용하도록 리팩터링하되, 새 코드가 더 나아 보일 때만 반영하자.

스트림 파이프라인은 일단 한 값을 다른 값에 매핑하고 나면 원래의 값을 잃는 구조이다.

package effectivejava.chapter7.item45;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import static java.util.stream.Collectors.*;

// 반복 방식과 스트림 방식으로 두 리스트의 데카르트 곱을 생성한다. (275-276쪽)
public class Card {
    public enum Suit { SPADE, HEART, DIAMOND, CLUB }
    public enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN,
                       EIGHT, NINE, TEN, JACK, QUEEN, KING }

    private final Suit suit;
    private final Rank rank;

    @Override public String toString() {
        return rank + " of " + suit + "S";
    }

    public Card(Suit suit, Rank rank) {
        this.suit = suit;
        this.rank = rank;

    }
    private static final List<Card> NEW_DECK = newDeck();

    // 코드 45-4 데카르트 곱 계산을 반복 방식으로 구현 (275쪽)
    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;
    }

//    // 코드 45-5 데카르트 곱 계산을 스트림 방식으로 구현 (276쪽)
      private static List<Card> newDeck() {
          return Stream.of(Suit.values())
                  .flatMap(suit ->
                          Stream.of(Rank.values())
                                  .map(rank -> new Card(suit, rank)))
                  .collect(toList());
      }

    public static void main(String[] args) {
        System.out.println(NEW_DECK);
    }
}

스트림 방식이 나아 보이고 동료들도 스트림 코드를 이해할 수 있고 선호한다면 스트림 방식을 사용하자.

 

스트림의 코드를 프로젝트에서 본 적이 없는 것 같다. 아무래도 나에게는 반복문이 더 익숙한 것 같다. 코딩테스트를 잠깐 준비할 때 스트림을 보기도 하였는데 자바8이라는 비교적 구버전에서 나온 것이라는 걸 이번에 알게 되었다.