java

자바 8 - 스트림 1

Feb 09, 2020

스트림을 생성하고 변환하는 방법

목차

1. 반복에서 스트림 연산으로 전환하기
2. 스트림 생성
3. filter, map, flatMap 메서드
4. 서브스트림 추출과 스트림 결합하기
5. 기타 스트림-변환


1. 반복에서 스트림 연산으로 전환하기

스트림은 컬렉션보다 개념적으로 높은 수준의 데이터 뷰 역할을 하며, 이를 이용하면 좀 더 직관적으로 계산을 명시할 수 있다. 스트림을 이용할 때는 일을 수행하는 방법이 아니라 하고 싶은 일을 명시한다. 그리고 연산을 스케줄링하는 일은 구현체에 맡긴다. 에를 들어 프로퍼티의 평균을 구한다고 해보자. 데이터의 소스와 프로퍼티를 지정하면 스트림 라이브러리에서 계산을 최적화 할 수 있다. 이를테면 멀티 스레드로 합계의 개수를 구하고 결과로 결합하는 식으로 한다.

다음은 긴 단어 리스트에서 길이가 12 보다 큰 단어의 갯수를 구하는 코드다.

String contents = new String(Files.readAllBytes(Paths.get("alice.txt")),
                StandardCharsets.UTF_8);
List<String> words = Arrays.asList(contents.split("\\PL+"));

// for문을 사용해 길이가 12이상인 단어 수 구하기
int cnt = 0;
for(String w : words) {
        if(w.length() > 12) cnt++;
}

// 스트림을 사용
long count = words.stream()
        .filter(w -> w.length() > 12)
        .count();

// parallelStream을 사용하면 스트림 라이브러리에서 필터링과 카운팅을 병렬로 수행한다.
long newCnt = words.parallelStream()
        .filter(w -> w.length() > 12)
        .count();

스트림을 이용하면 필터링과 카운팅을 증명하기 위해 루프를 살펴볼 필요가 없다. 메서드 이름을 보면 코드가 무엇을 의도하는지 바로 알 수 있다. 게다가 루프에서는 연산 순서를 자세히 작성해야 하지만, 스트림은 결과만 맞으면 원하는 방식으로 연산을 스케줄링할 수 있다.

스트림은 ‘어떻게가 아니라 무엇을’이라는 원칙을 따른다. 방금 전 스트림에서는 수행해야 하는 일을 기술했다. 일을 수행할 순서나 스레드는 명시하지 않았다. for문으로 작성하면 계산을 정확히 어떻게 수행해야 하는지 명시하므로 최적화 기회를 좋치고 만다.

컬렉션과의 차이점

  1. 스트림은 요소를 저장하지 않는다. 요소는 스트림을 지원하는 컬렉션에 저장되거나 필요할 때 생성된다.
  2. 스트림 연산은 원본을 변경하지 않는다. 예를 들어 filter 메서드는 새로운 스트림에서 요소를 지우는 게 아니라 해당 요소가 없는 새로운 스트림을 돌려준다.
  3. 스트림 연산은 가능하면 지연시켜둔다. 즉, 연산 결과가 필요하지 전까지는 실행되지 않는다. 예들 들어 긴 단어를 모두 요청하는 것이 아니라 처음 다섯 개만 요청했다면. filter 메서드는 다섯 번째 일치 단어를 찾은 후 필터링을 중단한다. 결과로 무한 스트림까지도 얻을 수 있다.

stream 메서드는 word 리스트에 대응하는 스트림을 돌려준다. filter 스트림에서는 또 다른 스트림을 반환하는데 12글자 보다 큰 단어만 포함된다. count 메서드는 해당 스트림을 결과로 리듀스 한다.

스트림은 보통 다음의 세 단계로 파이프라인을 준비한다.

  1. 스트림을 생성한다.
  2. 초기 스트림을 다른 스트림으로 변환하는 중간 연산을 지정한다. 여러 단계가 될 수도 있다.
  3. 종료 연산을 적용해서 결과를 산출한다. 종료 연산은 앞에서 지정한 지연 연산이 실행되게 한다. 그 이후로는 더 이상 해당 스트림을 사용할 수 없다.



2. 스트림 생성

Collection 인터페이스의 stream 메서드를 이용하면 어떤 컬렉션이든 스트림으로 변환할 수 있다.

배열일 경우는 정적 메서드 Stream.of를 사용한다.

// split은 String[] 배열을 반환한다.
Stream<String> words = Stream.of(contents.split(""));

// of 메서드는 가변 인자 파라미터를 받는다.
Stream<String> song = Stream.of("gently", "down", "the", "stream");

// 배열의 일부에서 스트림을 만드려면 Arrays.stream(array, from, to) 를 사용한다.
Stream<String> range = Arrays.stream(contents.split("\\PL"), 0, 3);

// 요소가 없는 스트림을 만드려면 정적메서드 Stream.empty를 사용한다.
Stream<String> silence = Stream.empty();

// 스트림 인터페이스에는 무한 스트림을 만드는 정적 메서드도 2개 있다. (gernerate, iterate)
// 예를 들어 상수 값의 스트림은 다음과 같이 얻을 수 있다.
Stream<String> echos = Stream.generate(() -> "Echo");

// 난수의 스트림
Stream<Double> randoms = Stream.generate(Math::random);

// 무한 수열을 만드려면 iterate 메서드를 사용하면 된다.
// iterate 메서드는 '시드' 값과 함수를 받아서 해당 함수를 이전 결과에 반복하여 적용한다.
// 수열의 첫 번째 요소는 시드 값이 BigInteger.ZERO, 두 번째 요소는 f(seed), 즉 1(BigInteger)이다. 그 다음 요소는 f(f(seed)), 즉 2가 되는 것이다.
Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));

자바의 여러 API에서 스트림을 돌려준다. 에를 들어 Pattern 클래스에는 정규 표현식으로 CharSequence를 분리하는 SplitAsStream 메서드가 있다. Files.lines 메서드는 Stream을 반환한다.



3. filter, map, flatMap 메서드

스트림 변환은 또 다른 스트림에 들어 있는 요소에서 파생된 요소의 스트림을 만들어낸다.

filter : 특정 조건과 일치하는 요소를 새로운 스트림을 돌려 준다.

List<String> words = Arrays.asList("Apple", "Banana", "Pear");
Stream<String> someWords = words.stream()
        .filter(w -> w.length() > 4);

map : 스트림에 들어 있는 값들을 특정 방식으로 변환 한다.

Stream<String> lowercaseWords = words.stream().map(String::toLowerCase);

Stream<String> firstLetters = words.stream().map(s -> s.substring(0, 1)).map(String::toLowerCase);

값의 스트림을 반환하는 메서드

public Stream<String> letters(String s) {
        List<String> result = new ArrayList<>();
        for (int i = 0; i < s.length(); i++)
                result.add(s.substring(i, i + 1));
        return result.stream();
}

letters 메서드를 문자열 스트림에 매핑하기

// 스트림의 스트림을 얻는다. [... [..], [..], [..]...]
Stream<Stream<String>> result = Stream.of(song).map(w -> letters(w));
// 이 스트림을 문자열 스트림 [ .., .., .., ..] 으로 펼치려면 map 대신 
// flatMap, 메서드를 사용해야 한다.
Stream<String> flatResult = Stream.of(song).flatMap(w -> letters(w));



4. 서브스트림 추출과 스트림 결합하기

스트림.limit(n)

스트림.skip(n)

스트림.concat(스트림)

Stream<Double> randoms = Stream.generate(Math::random).limit(100);

Stream<String> words = Stream.of("apple", "Banana", "Arsnal").skip(1);

Stream<String> combined = Stream.concat(letters("HellO"), letters("hELLo"));



5. 기타 스트림 변환

distinct

sorted

peek

Stream<String> uniqueWords = Stream.of("apple", "apple", "samsung", "google", "google").distinct();


Stream<String> longestFirst = words.stream().sorted(Comparator.comparing(String::length).reversed());

Object[] powers = Stream.iterate(1.0, p -> p * 2)
        .peek(e -> System.out.println("Fetching " + e))
        .limit(20).toArray();