• 스트림 API는 다량의 데이터 처리작업을 돕고자 추가되었다.
  • 스트림은 데이터 원소의 유한 혹은 무한 시퀀스를 뜻한다.
  • 스트림 파이프라인은 이 원소들로 수행하는 연산 단계를 표현하는 개념이다.
  • 스트림의 원소들은 대표적으로 컬렉션, 배열 파일, 정규표현식 패턴 매처, 난수 생성기, 혹은 다름스트림에서 온다.
  • 스트림 안의 데이터 원소들은 객체 참조나 기본 타입 값이다.
  • 기본 타입 값으로는 int, long, double 이렇게 세가지를 지원한다.
    • Stream
    • IntStream
    • LongStream
    • DoubleStream

 

 

스트림 파이프 라인

  • 스트림 파이프라인은 지연 평가(lazy evaluation) 된다.
  • 평가는 종단 연산이 호출될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다.
  • 기본적으로 스트림 파이프 라인은 순차적으로 진행된다.
  • 스트림을 제대로 사용하면 프로그램이 짧고 깔끔해지지만 잘못 사용하면 읽기 어렵고 유지보수도 힘들어진다.

 

사용 예시

  • 사전 파일에서 단어를 읽어 사용자가 지정한 문턱값보다 원소 수가 많은 아나그램 그룹 출력
 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);
        }
    }
  • Map의 key는 철자를 알파벳순으로 정렬한 것, value는 단어 값
    • ex) staple의 키는 aelpst , petals의 키도 aelpst --> 아나그램끼리는 같은 키 공유
  • computeIfAbsent 메소드는 맵 안에 키가 있는지 찾은 다음 있으면 키에 매핑된 값을 반환
  • 키가 없으면 건네진 함수 객체를 키에 적용하여 값을 계산하고 그 키와 값을 매핑하고 계산된 값 반환

 

다음의 예시는 위의 코드를 스트림을 과하게 사용해서 만든 예시다

 

 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 -> 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);
            }
        }
    }

 

다음은 스트림을 깔끔하게 사용한 경우다.

 

 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);
        }
    }

 

주의 사항

  • 기존 코드는 스트림을 사용하도록 리팩토링하되 새 코드가 더 나아보일 때만 반영하자.
  • 스트림이 안성맞춤인 작업
    • 원소들의 시퀀스를 일관되게 변환한다.
    • 원소들의 시퀀스를 필터링한다.
    • 원소들의 시퀀스를 하나의 연산을 사용해 결합한다.
    • 원소들의 시퀀스를 컬렉션에 모은다
    • 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.

 

 

스트림과 반복 중 어느 쪽을 써야 할지 바로 알기 어려운 작업도 많다.

  • 다음은 카드 덱을 초기화 하는 작업이다.
  • 카드는 숫자(rank)와 무늬(suit)를 묶은 불변 값 클래스이고 숫자와 무늬는 모두 열거타입이다.
  • 이 작업은 두 집함의 원소들로 만들 수 있는 가능한 모든 조합을 계산한다.(데카르트 곱)

 

 

먼저 for-each로 구현한 코드다

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;
}

 

 

다음은 스트림이다.

private static List<Card> newDeck() {
    return Stream.of(Suit.values())
    .flatMap(suit -> Stream.of(Rank.values())
                      .map(rank -> new Card(suit, rank)))
    .collect(toList());
}

 

 

어느 방식이 더 편한가는 프로그래머의 개인차이다.

+ Recent posts