본문 바로가기

JAVA/JAVA 8

자바 8 #스트림(Stream) 종류와 함수형 인터페이스

반응형

기본 타입 스트림

정수를 래퍼 객체로 감싸는 일이 명백히 비효율적인데도 불구하고 정수들을 Stream<Integer>로 모았다.

래퍼 객체의 비효율성은 다른 기본타입인 double, float, long, short, char, byte, boolean의 경우도 마찬가지다.

스트림 라이브러리는 기본 타입 값들을 직접 저장하는 데 특화된 타입인 IntStream, LongStream, DoubleStream을 포함한다.

short, char, byte, boolean 타입을 저장할려면 IntStream을 사용하고, float인 경우는 DoubleStream을 사용한다.

라이브러리 설계자들은 이들 나머지 5개 스트림 타입을 추가할 가치가 있다고 생각하지 않았다.


IntStream을 생성하려면 IntStream.of 와 Arrays.stream 메서드를 호출한다.

예)

IntStream stream = IntStream.of(1, 1, 2, 3, 5);

stream = Arrays.stream(values, form, to) // 같은 int[] 배열이다.


객체 스트림과 마찬가지로 정적 generate와 iterate 메서드를 사용할 수 있다.

또한 IntStream과 LongStream은 크기  증가 단위가 1인 정수 범위를 생성하는 정적 range와 rageClosed 메서드를 포함한다.

예)

IntStream zeroToNinetyNine = IntStream.rang(0, 100); // 상한값 제외

IntStream zeroToHundred = IntStream.rangeClosed(0, 100); //상한값 포함


CharSequence 인터페이스는 각각 문자의 유니코드와 UTF-16 인코딩의 코드 단위로 구성된 IntStream을 돌려주는 codePoints와 chars 메서드를 포함한다.(코드 단위가 무엇인지 모른다면 chars 메서드를 사용하지 않는게 좋다.)


String sentence = "\uD835\uDD46 is the set fo octonions.";

IntStream codes = sentence.codePoints(); //16진수 값 으로 구성된 스트림


객체 스트림은 mapToInt, mapToLong, mapToDouble 메서드를 이용해서 기본 타입 스트림으로 변환할 수 있다.

예) 문자열 스트림에서 요소의 길이를 정수로 처리하려는 경우


Stream<String> words = ...;

IntStream lengths = words.mapToInt(String::length);


기본 타입 스트림을 객체 스트림으로 변환하려면 boxed 메서드를 이용한다.

Stream<Integer> integers = Integer.rang(0, 1000).boxed();


일반적으로 기본 타입 스트림을 대상으로 동작하는 메서드는 객체 스트림 대상 메서드와 유사하다.

차이점


toArray 메서드는 기본타입 배열을 리턴한다.

옵션결과를 돌려주는 메서드 OptionalInt, OptionalLong , OptionalDouble을 리턴한다.

이들 클래스는 Optional 클래스와 유사하지만 get 메서드 대신 getAsInt, getAsLong, getAsDouble 메서드를 포함한다.

각각 합계, 평균, 최대값, 최소값을 리턴하는 sum, average, max, min  메서드가 있다. 객체스트림에는 이러한 메서드가 정의되어 있지 않다.

summaryStatistics 메서드는 스트림의 합계, 평균, 최대값, 최소값을 동시에 보고할 수 있는 IntSummaryStatistics, LongSummaryStatistics, DoubleStatistics 타입 객체를 돌려준다.


Random 클래스는 난수로 구성된 기본 타입 스트림을 리턴하는 ints, longs, doubles 메서드를 포함한다.


병렬 스트림

스트림은 벌크연산 bulk operation을 병렬화하기 쉽게 해준다.

처리과정은 자동으로 일어나지만 몇 가지 규칙을 따라야 한다.

먼저 병렬 스트림 parallel stream을 얻어야 한다.

Collections.parallelStream()을 제외하면 스트림 연산은 기본적으로 순차 스트림 sequential stream을 생성한다.

parallel 메서드는 순차 스트림을 병렬 스트림으로 변환한다.

예)

Stream<String> parallelWords = Stream.of(wordArray).parallel();


스트림이 병렬 모드에 있으면 최종 메서드가 실행할 때 모든 지연 처리 중간 스트림 연산이 병렬화된다.


스트림 연사들이 병렬로 실행할 때 목적은 해당 연산을 차례대로 실행했을 때와 같은 결과를 리턴하는 것이다.

이때 연산들은 무상태stateless고, 임의의 순서로 실행될 수 있다는 점이 중요하다.


수행할 때 없는 일의 예

문자열 스트림에서 모든 짧은 단어의 수

int [] shortWords = new int[12];

words.parallel().forEach(s -> { if(s.length() <12) shortWords[s.length()]++; });

//오류 - 경쟁조건!

System.out.println(Arrays.toString(shortWords));


나쁜 코드

forEach에 전달된 함수는 다수의 스레드에서 동시에 실행되어 공유 배열을 업데이트 한다.

이 상황은 전형적인 경쟁조건race condition이다. 

이 프로그램을 여러번 실행하면 매번, 생성할때마다 다른 개수를 얻고 각각도 잘못된 결과일것이다.


병렬 스트림 연산에 전달하는 함수가 스레드에 안전함을 보장하는 일은 개발자의 책임이다.

카운터로 AtomicInteger 객체의 배열을 사용할 수 있다.

다른 방법으로 단순히 스트림 라이브러리의 기능을 사용하고 문자열을 길이에 따라 그룹으로 묶을 수 있다.

기본적으로 순서 유지 컬렉션(배열과 리스트), 범위range, 발생기generator, 반복자 또는 Stream.sorted를 호출해서 얻는 스트림은 순서를 유지한다.

순서 유지 스트림의 결과들은 원본 요소들의 순서로 쌓이고, 전체적으로 예측 가능하게 동작한다.

따라서 같은 연산을 두번 실행도 완전히 같은 결과를 얻는다.


순서 때문에 병렬화를 이용할 수 없는 것은 아니다.

예) stream.map(fun)을 계산할 때 스트림은 n개 세그먼트로 분할되어 각각이 동시에 처리될 수 있다.

그런 다음 순서대로 재조립된다.

몇몇 연산은 순서에 대한 요구사항을 버리면더 효과적으로 병렬화 될 수 있다.

Stream.unoredered 메서드를 호출함으로써 순서에는 관심이 없음을 나타낼 수 있다.

이로부터 이점을 얻을 수 있는 한가지 연산은 Stream.distinct다. 순서유지 스트림에서 distinct는 같은 요소 중 첫 번째를 보존한다.

하지만 이 동작은 병렬화를 방해한다.(세그먼트를 처리중인 스레드는 이전 세그먼트가 처리되기 전에는 어느 요소를 버려야 하는지 알수 없다.) 유일한 요소라면 어느것이든 보존해도 괜찮다면(중복을 추적하기 위해 공유 집합을 사용해) 모든 세그먼트를 동시에 처리할 수 있다.


순서를 포기하면 limit 메서드를 빠르게 할 수 있다.

스트림에서 단지 n개요소를 원할 뿐 어느 것을 얻든지는 상관하지 않는다면 다음과 같이 사용한다.

Stream<T> sample  = stream.parallel().unordered().limit(n);


맵을 병합하는 일은 비용이 많이 든다.

이 때문에 Collectors.groupingByConcurrent 메서드는 공유되는 병행맵을 사용한다.

분명히 병렬화의 이점을 얻기 위해 맵 값들의 순서는 스트림 순서와 달라질 것이다.

이 컬렉터는 심지어 순서 유지 스트림에서도 순서를 유지하지 않는 '성질'이 있다.

따라서 스트림이 순서를 유지하지 않게 만들 필요 없이도 효율적으로 사용할 수 있다.

그럼에도 여전히 스트림을 병렬로 만들어야 한다.


Map<String, List<String>> result = cities.parallel().collect(

Collectors.groupingByConcurrent(City::getState));

//값들이 스트림 순서로 모이지 않는다.


스트림 연산을 수행하는 동안에는 해당 스트림을 뒷받침하는 컬렉션을 절대 수정하면 안된다.

(스레드에 안전한 수정인 경우에도). 스트림은 자체적으로 데이터를 모으지 않음을 명심하기 바란다.

(데이터는 항상 별도의 컬렉션에 존재한다.) 만일 해당 컬렉션을 수정하면 스트림 연산들의 결과는 정의되지 않는다.

JDK 문서에서는 이 요구사항을 방해금지noninterferernce라고 언급하고 있다.

이 사항은 순차 스트림과 병렬 스트림 모두 적용된다.


엄밀히 말하면 중간 스트림 연산이 진연 처리되기 때문에 최종 연산이 실행하는 시점 이전까지는 컬렉션을 변경할 수 있다.

예) 올바른 예

List<String> wordList = ...;

Stream<Stream> words = wordList.stream();

wordList.add("END");

long n = words.distinct().count();


예) 잘못된 예

Stream<String> words = wordList.stream();

words.forEach(s -> if(s.length() < 12) wordList.remove(s));

// 오류 - 방해



함수형 인터페이스


Stream.filter 메서드는 함수 인자를 받는다.


Stream<String> longWords = words.filter(s -> s.length() >= 12);


Stream 클래스의 filter메서드

Stream<T> filter(Predicate<? super T> predicate)


Predicate는 boolean값을 리턴하는 넌디폴트 nondefault 메서드 한개를 포함하는 인터페이스이다.


public interface Predicate {

boolean test(T argument);

}


실전에서는 보통 람다 표현식이나 메서드 레퍼런스를 전달하기 때문에 메서드의 이름은 실제로 문제가 되지 않는다.

여기서 중요한 부분은 boolean이라는 리턴 타입이다.

Stream.filter 문서를 읽을 때 Predicate가 boolean을 리턴하는 함수라는 점만 기억하면된다.


Stream.filter의 선언부를 자세히 보면 와일드카드 타입 Predicate<?  super T>를 주목하게 될 것이다.

흔히 함수 파라미터로 이와 같은 타입을 사용한다.

예를들어, Employee는 Person의 서브클래스고, Stream<Employee>가 있다고 하자.

이 경우 Predicate<Employee>, Predicate<Person> 또는 Predicate<Object>로 스트림을 필터링할 수 있다.

(여기에서 T는 Employee)

이와 같은 유연성은 메서드 레퍼런스를 전달할 때 특히 중요하다.

예를들어, Stream<Employee>를 필터링하는데 Person::isAlive를 사용하려 한다고 하자.

이 작언은 순전히 filter메서드의 파리미터에 있는 와이드카드 덕분에 동작한다.


Stream과 Collectors에 속한 메서드들의 파라미터로 나타나는 함수형 인터페이스

함수형 인터페이스 파라미터 타입 리턴 타입 설명

Supplier<T> 없음 T T 타입 값을 공급한다.

Consumer<T> T void T 타입 값을 소비한다.

BiConsumer<T, U> T, U void T와 U 타입 값을 소비한다.

Predicate<T> T boolean boolean 값을 리턴하는 함수다.

ToIntFunction<T> T int T타입을 인자로 받고 각각 int, long, double 값을 리턴하는 함수

ToLongFunction<T> T long

ToDoubleFunction<T> T double

IntFunction<R> int R 각각 int, long, double을 인자로 받고 R 타입을 리턴하는 함수

LongFunction<R> long R

DoubleFuncton<R> double R

Function<T, R> T R T타입을 인자로 받고 R 타입을 리턴하는 함수다.

BiFunction<T, U, R> T,U R T와 U타입을 인자로 받고 R타입을 리턴하는 함수다.

UnaryOperator<T> T T T타입에 적용되는 단항 연산자다.

BinaryOperator<T> T T T타입에 적용되는 이항 연산자다.


핵심내용

반복자iterator는 특정 순회전략을 내포하므로 효율적인 동시 실행을 방해한다.

컬렉션, 배열, 발생기generate, 반복자로부터 스트림을 생성할 수 있다.

요소를 선택하는데 filter를 사용하고, 요소를 변환하는데 map을 사용한다.

스트림을 변환하는 다른 연산으로는 limit, distinct, sorted가 있다.

스트림에서 결과를 얻을려면 count, max, min, findFirst, findAny 같은 리덕션reduction 연산자를 사용한다. 이들 중 몇몇은 Optional 값을 리턴한다.

Optional 타입은 null 값을 다루는 안전한 대안을 목적으로 만들어졌다.  Optional 타입을 안전하게 사용하려면 ifPresent와 orElse 메서드를 이용한다.

스트림 결과를 컬렉션, 배열, 문자열, 맵으로 모을 수 있다.

Collectors 클래스의 groupingBy, paritioningBy 메서드는 스트림의 내용을 그룹을 분할하고, 각 그룹의 결과를 얻을 수 있게 해준다.

기본 타입인 int, long, double 용으로 특화된 스트림이 있다.

병렬 스트림을 이용할 때는 부가작용 side effect를 반드시 피해야 하고, 순서 제약을 포기하는 방안도 고려한다.

스트림 라이브러리를 사용하려면 몇가지 함수형 인터페이스와 친숙해져야 한다.

반응형