이펙티브 자바 #item45 스트림은 주의해서 사용하라

임현규·2023년 5월 2일
0

이펙티브 자바

목록 보기
45/47
post-thumbnail

스트림 특징

스트림 파이프라인은 다량의 데이터 처리 작업(순차 또는 병렬)을 처리하기 위해 java 8에 추가된 API이다.

기본 타입 지원은 int, long, double을 지원한다

스트림 파이프라인은 소스 스트림에서 시작해 중간 연산 과정을 거쳐 종단 연산으로 끝난다. 이 떄 중간 연산에는 필터 또는 타입 변환 등이 있을 수 있다.

스트림 파이프 라인은 지연 평가(lazy evaluation)된다. 평가는 종단 연산이 호출될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다. 그렇기 때문에 아무리 중간 연산을 많이 연결해도 종단 연산을 하지않으면 stream 연산은 수행하지 않는다.

스트림 파이프라인 메서드 연쇄를 지원하는 fluent API 이다. 이 말은 메서드 연쇄를 통해 대량의 데이터를 처리하는데 가독성을 높일 수 있다.

스트림 효율적으로 사용하기

class Solution {

    public static void main(String[] args) {
        String line = "hello ehllo asb bas asdf fdsa qq qrert ahzxcv scv vcs csv";
        int minGroupSize = 1;
        Map<String, Set<String>> groups = new HashMap<>();
        for (String word : line.split(" ")) {
            groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);
        }
        for (Set<String> group : groups.values()) {
            if (group.size() > minGroupSize) {
                System.out.println(group);
            }
        }
    }

    private static String alphabetize(String word) {
        char[] chars = word.toCharArray();
        Arrays.sort(chars);
        return String.valueOf(chars);
    }
}

해당 코드는 애너그램을 추출하고 동일한 애너그램 갯수가 minGroupSize보다 크다면 해당 group을 출력하는 예제이다. 이를 stream으로 바꿔보자

과하게 스트림을 사용한 예시

class Solution {

    public static void main(String[] args) {
        String line = "hello ehllo asb bas asdf fdsa qq qrert ahzxcv scv vcs csv";
        int minGroupSize = 1;
        Stream.of(line.split(" ")).collect(
                Collectors.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);
    }
}

위의 코드는 과도하게 스트림을 사용한 예이다. 여러 로직들이 포함되어 있고 stream 내에 구현 로직이 포함되어 있다. stream의 메서드 체이닝을 효율적으로 사용하려면 한 라인당 하나의 로직을 처리하도록 체이닝 하는 것이 좋다.

헬퍼 메서드를 활용해 stream 사용하기

class Solution {

    public static void main(String[] args) {
        String line = "hello ehllo asb bas asdf fdsa qq qrert ahzxcv scv vcs csv";
        int minGroupSize = 1;
        Arrays.stream(line.split(" "))
            .collect(Collectors.groupingBy(Solution::alphabetize))
            .values().stream()
            .filter(group -> group.size() >= minGroupSize)
            .forEach(g -> System.out.println(g.size() + ": " + g));
    }

    private static String alphabetize(String word) {
        char[] chars = word.toCharArray();
        Arrays.sort(chars);
        return String.valueOf(chars);
    }
}

헬퍼 메서드를 활용해 코드를 분리했다. 위 코드를 보면 java를 모르는 사람도 쉽게 코드를 파악할 정도로 코드가 쉽다.

책에서는 예제가 간단하기 때문에 stream 최종연산에 stream으로 연결해서 처리했지만, stream은 하나의 목적으로만 처리하는 것이 읽기 더 편한 것 같다.

개인적으로 개선한 코드는 다음과 같다.

class Solution {

    public static void main(String[] args) {
        String line = "hello ehllo asb bas asdf fdsa qq qrert ahzxcv scv vcs csv";
        int minGroupSize = 1;
        Map<String, Set<String>> groups = Arrays.stream(line.split(" "))
                .collect(Collectors.groupingBy(Solution::alphabetize, Collectors.toSet()));
        
        groups.values().stream()
            .filter(group -> group.size() >= minGroupSize)
            .forEach(g -> System.out.println(g.size() + ": " + g));
    }

    private static String alphabetize(String word) {
        char[] chars = word.toCharArray();
        Arrays.sort(chars);
        return String.valueOf(chars);
    }
}

char 연산에 stream을 사용하지 말자

"Hello, java!".chars().forEach(System.out::print);

문자가 나올것이라 생각하지만 실제로는 이상한 숫자만 나온다. 그 이유는 chars()는 intStream으로 변환하기 때문이다. 그래서 char를 출력하려면 형변환을 해줘야 한다.

chars()라는 메서드가 charStream을 지원할 것 같은 착각을 들게 하고 오해하기 충분한 메서드이름이다. 되도록이면 사용하지 말자.

Stream에 적합한 로직

  • 원소들의 시퀀스를 일관되게 변환
  • 원소들의 시퀀스 필터링
  • 원소들의 시퀀스를 하나의 연산을 사용해 결합
  • 원소들의 시퀀스를 컬렉션에 모으기
  • 원소들의 시퀀스에서 특정 조건을 만족하는 원소 찾기
profile
엘 프사이 콩그루

0개의 댓글