본문 바로가기

JAVA/JAVA 8

자바 8 #스트림(Stream)

반응형

스트림

스트림은 자바8에서 연산의 스케줄링은 구현체에 맡기며, 값들의 묶음을 처리하고 원하는 작업을 지정하는데 필요한 핵심 추상화이다.

예) 특정 메서드에서 리턴하는 값들의 평균을 계산하려고 한다면

각 요소를 대상으로 해당 메서드를 호출하여  이 값들의 평균을 얻고 싶다고 명시한다.

각 부분의 합계와 카운트를 계산하고 결과를 합치기 위해 다중 스레드를 사용해 연산을 병렬화하는 일을 스트림 라이브러리에 맡긴다.


스트림

컬렉션을 처리할 때 보통은 요소들을 순회하면서 각 요소를 대상으로 작업한다.

String contents = new String(Files.readAllBytes(Paths.get("c:\\alice.txt")), StandardCharsets.UTF_8); //파일을 문자열로 읽어온다.

List<String> words = Arrays.asList(contents.split("[\\P{L}]+")); //단어로 분리한다(비문자를 구분자로 사용한다)


int count = 0;

for(String w : words) {

if(w.length() >12) count++;

}

코드를 병렬화하기 어렵다는 점

자바 8의 벌크연산 bulk operation이 등장할 곳이다.


long count = words.stream().filter(w -> w.length()>12).count();


stream 메서드는 word 리스트의 스트림을 돌려준다. filter 메서드는 12글자보다 긴 단어만 담은 다른 스트림을 리턴한다.

count 메서드는 이 스트림을 결과로 리듀스(reduce)한다.


스트림은 데이터를 변환하고 추출할 수 있게 해주어 겉으로는 컬렉션과 유사하게 보인다.

하지만 차이점이 있다.

1. 스트림은 요소들을 보관하지 않는다. 요소들은 하부의 컬렉션에 보관되거나 필요할 때 생성된다.

2. 스트림 연산은 원본을 변경하지 않는다. 대신 결과를 담은 새로운 스트림을 반환한다.

3. 스트림 연산은 가능하면 지연 lazy처리된다. 지연처리란 결과가 필요하기 전에는 실행되지 않음을 의미한다.

예) 긴 단어를 모두 세는 대신 처음 5개 긴 단어를 요청하면 filter 메서드는 5번째 일치후 필터링을 중단한다.

결과적으로 심지어 무한 스트림도 만들수 있다.


많은 사람이 루프형태보다 스트림 표현식을 읽기 쉽다고 여길것이다.

게다가 스트림은 병렬화할 수 있다.


long count = words.parallelStream().filter(w -> w.length() > 12).count();


단순히 stream을  parallelStream으로 변경하는 것만으로 스트림 라이브러리가 필터링과 카운팅을 병렬로 수행하게 된다.


스트림은 '어떻게가 아니라 무엇을' 원칙을 따른다.

따라서 이 스트림 예제에서 무엇을 해야하는지 기술한다.(긴 단어를 골라내고 개수를 센다)

작업이 어떤 순서로 또는 어떤 스레드에서 일어나야 하는지는 명시하지 않는다.

이와 반대로, 루프에서는 계산을 정확히 어떻게 해야하는지 명시하기 때문에 최적화의 모든 기회를 포기하게 된다.


스트림을 이용해 작업할 때 연산들의 파이프라인을 세단계로 설정한다.

1. 스트림을 생성한다.

2. 초기 스트림을 다른 스트림으로 변환하는 중간연산 intermediate operation들을 하나 이상의 단계로 지정한다.

3. 결과를 산출하기 위해 최종연산 terminal operation을 적용한다. 이 연산은 지연연산들의 실행을 강제한다.

이후로는 해당 스트림은 더는 사용할 수 없다.


스트림 연산들은 요소를 대상으로 실행될 때 스트림에서 호출된 순서로 실행되지 않는다.

count가 호출되기 전에는 아무 것도 일어나지 않는다. count 메서드가 첫 번째 요소를 요청하면 filter 메서드가 길이 > 12인 요소를 찾을 때까지 요소들을 요청하기 시작한다.


스트림 생성

자바 8에서 Collection  인터페이스에 추가된 stream 메서드를 이용해 컬렉션을 스트림으로 바꿀 수 있음을 알아보았다.

만일 배열이 있다면 정적 Stream.of 메서드를 사용한다.


Stream<String> words = Stream.of(contents.split("[\\P{L}]+")); //split은 String[] 배열을 리턴한다.


of 메서드는 가변 인자 varargs 파라미터를 받기 때문에 인자 개수가 몇 개든 스트림을 생성할 수 있다.

Stream<String> song = Stream.of("gently", "down", "the", "stream");


배열의 일부에서 스트림을 생성하려면 Arrays.stream(array, form, to)를 사용한다.

요소가 없는 스트림을 생성하려면 Steam.empty 메서드를 사용한다.


Stream<String> silence = Stream.empty();


Stream 인터페이스는 무한 스트림을 만드는 두가지 정적 메서드를 포함한다.

generate메서드는 인자 없는 함수(기술적으로 Supplier<T> 인터페이스의 객체)를 받는다.

스트림 값이 필요할 때는 이 함수를 호출해서 값을 생산한다. 상수값들의 스트림을 얻을 수 있다.


Stream<String> echos = Stream.generate(()-> "Echo");


난수 random number의 스트림

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


0 1 2 3 ... 같은 무한 수열을 만들어낼려면 iterate 메서드를 사용한다.

iterate메서드는 '시드 seed'값과 함수(기술적으로는 UnaryOperator<T>)를 받고, 해당 함수를 이전 결과에 반복적으로 적용한다.


Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n-> n.add(BigInteger.ONE));

List<BigInteger> test = integers.limit(10).collect(Collectors.toList());

System.out.println(test);


수열의 첫 번째 요소는 시드 값인 BigInteger.ZERO다.

두번째 요소는 f(seed), 즉  (BigInteger 값으로) 1이다.

다음요소는 f(f(seed)), 즉 2등이 된다.


자바 8 릴리스는 스트림을 돌려주는 다수의 메서드를 추가했다.

예) Pattern 클래스는 이제 정규표현식을 이용해 CharSequence를 분리하는 splitAsStream 메서드를 포함한다.


Stream<String> words = Pattern.compile("[\\P{L}]+").splitAsStream(contents);

words.filter(w -> w.length()>12).forEach(System.out::println);


정적 Files.lines 메서드는 파일에 있는 모든 행의 Stream을 리턴한다.

Stream 인터페이스는 AutoCloseable을 슈퍼인터페이스로 둔다. 따라서 스트림에 close 메서드를 호출할 때 하부 파일 또한 닫힌다.

이를 확인하려면 try-with-resource 문을 사용하는 것이 가장 좋다.


try(Stream<String> lines = Files.lines(path)){

//lines를 이용해 원하는 작업을 수행

}

스트림 및 이와 연계된 파일은 try 블록이 정상적으로 종료할 때 또는 예외를 통해 닫힌다.

반응형