본문 바로가기
Language/Effective java

람다와 스트림

by y.j 2022. 7. 26.
728x90

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

스트림 API는 다량의 데이터 처리 작업을 돕고자 추가되었다.

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

 

스트림 파이프라인

  • 스트림 파이프라인은 소스 스트림에서 시작해 종단 연산으로 끝나며, 그 사이에 하나 이상의 중간 연산이 있을 수 있다. 중간 연산은 스트림을 어떠한 방식으로 변환한다. 
  • 지연 평가 된다. 평가는 종단 연산이 호출될 때 이뤄지며 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다. 종단 연산이 없는 스트림 파이프라인은 아무일도 하지 않는 명령어인 no-op과 같으므로 종단 연산을 빼먹는 일이 절대 없도록 하자.

스트림은 가독성이 어려워 유지보수가 어려워질 수 있다.

아래코드는 스트림에 익숙하지 않은 프로그래머라면 이해하기 어렵고, 그렇지 않더라도 이해하기 어렵다.

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 word) {
        char[] a = word.toCharArray();
        Arrays.sort(a);
        return new String(a);
    }
}

 

char값들을 처리할 떄는 스트림을 삼가자.

아래 코드는 숫자를 반환하게 된다. chars()가 반환하는 값이 int이기 때문이다.

"Hello World".chars().forEach(System.out::println);

 

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

 

스트림의 함수객체로 할 수 없는 코드블록의 장점

  • 람다에서는 final이거나 사실상 final 변수만 읽을 수 있고, 지역변수를 수정하는 건 불가능하다.
  • 코드 블록에서는 return문을 사용해 메서드에서 빠져나가거나 beak나 continue문으로 블록 바깥의 반복문을 종료하거나 반복을 한번 건너 뛸 수 있다. 람다로는 불가능하다.

 

스트림 시퀀스에 안성맞춤인 일

  • 원소들의 시퀀스를 일관되게 변환한다.
  • 원소들의 시퀀스를 필터링한다.
  • 원소들의 시퀀스를 하나의 연산을 사용해 결합한다.
  • 원소들의 시퀀스를 컬렉션에 모은다. ( 공통된 속성 )
  • 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.

 

Stream.iterator사용법

iterator는 2개의 매개변수를 받는다. 첫 번째는 스트림의 첫 번째 원소이고, 두 번째는 다음 원소를 생성해주는 함수이다. 

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

 

flatMap사용법

flatMap은 원소 각각을 하나의 스트림으로 매핑한 다음 그 스트림들을 다시 하나의 스트림으로 합친다.

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

 

스트림에서는 부작용 없는 함수를 사용하라

스트림의 패러다임의 핵심은 계산을 일련의 변환으로 재구성하는 부분이다.

  • 각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수 함수(오직 입력만이 결과에 영향을 주는 함수)여야 한다.
  • 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다.

 

잘못된 예시

얼핏보면 좋은 코드 같지만, 좋지 않은 코드이다. forEach 연산은 스트림 결과 계산를 보고할 때만 사용하고, 계산하는 데는 사용하지 말자.

Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
    words.forEach(word -> {
        freq.merge(word.toLowerCase(), 1L, Long::sum);
    });
}

 

수정된 코드

Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
    freq = words
            .collect(groupingBy(String::toLowerCase, counting()));
}

 

Collector

Collector란 스트림의 원소들을 객체 하나에 취합하는 축소전략을 캡슐화한 객체이다.

 

toMap(keyMapper, valueMapper)

아래코드는 Object의 toString으로 리턴되어있는 객체를 key로 삼고 value르 그대로 반환하는 것이다.

private static final Map<String, Operation> stringToEnum = 
        Stream.of(values()).collect(
                toMap(Object::toString, e -> e)
        );

 

toMap(keyMapper, valueMapper, BinaryOperator<T> operation)

toMap 3번째 매개변수로써 함수를 받을 수 있는 버전도 존재한다. 

Map<Artist, Album> topHits = albums.collect(
        toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));

 

groupingBy

입력으로 분류 함수를 받고 출력으로는 원소들을 카테고리별로 모아 놓은 맵을 담은 수집기를 반환한다.

words.collect(groupingBy(word -> alpabetize(word)));

 

groupingBy로 맵을 생성하는 방법

분류함수와 함께 다운 스트림 수집기도 명시해야 한다. 

다운스트림 : toSet(), toCollection(collectionFactory), counting()

Map<String, Long> freq = words
        .collect(groupingBy(String::toLowerCase, counting()));

 

수집과는 관련이 없는 메서드

minBy : 스트림 중에서 가장 작은 값

maxBy : 스트림 중에서 가장 큰 값

 

joining메서드

CharSqeunce인스턴스 스트림에서 원소들을 잇는 역할을 하고 구분문자를 매개변수로 받는다.

 

 

 

 

728x90

'Language > Effective java' 카테고리의 다른 글

메서드  (0) 2022.08.01
람다와 스트림  (0) 2022.07.29
람다와 스트림  (0) 2022.07.24
열거 타입과 애너테이션  (0) 2022.07.23
열거 타입과 애너테이션  (0) 2022.07.20

댓글