[Java8 In Action] #5 스트림 활용 |
Java8 In Action 내용을 보며 정리한 내용입니다.
정리자는 기존에 Java8 을 한차례 rough 하게 공부한 적이 있고, Kotlin 역시 공부한 적이 있습니다.
위의 prerequisite 가 있는 상태에서 추가적인 내용만 정리한 내용이므로, 제대로 공부를 하고 싶다면 책을 구매해서 보길 권장합니다!
5.1. 필터링과 슬라이싱
5.1.1. 프리디케이트로 필터링
-
filter 메서드는 프레디케이트(불린을 반환하는 함수)를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.
5.1.2. 고유 요소 필터링
-
고유 요소로 이루어진 스트림을 반환하는 distinct 라는 메서드가 있다.
고유 여부는 스트림에서 만든 객체의 hashCode, equals 로 결정한다.
5.1.3. 스트림 축소
-
주어진 사이즈 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다.
5.1.4. 요소 건너뛰기
-
처음 n 개의 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원.
n개 이하의 요소를 포함하는 스트림에 skip(n) 을 호출하면 빈 스트림이 반환된다.
5.2. 매핑
5.2.1. 스트림의 각 요소에 함수 적용하기
-
인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다. ( 변환, transforming )
5.2.2. 스트림 평면화
map 과 Arrays.stream 활용
flatMap 사용
-
flatMap 은 각 배열의 스트림이 아니라 스트림의 컨텐츠로 매핑한다.
스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.
-
두 개의 숫자 리스트가 있을 때 모든 숫자 쌍의 리스트를 반환하는 코드는 다음과 같이 짜면 된다.
List<Integer> numbers1 = Arrays.asList(1,2,3); List<Integer> numbers2 = Arrays.asList(3,4); List<int[]> pairs = number1.stream() .flatMap( i -> number2.stream() .map( j -> new int[]{ i, j } ) ) .collect(toList());
5.3. 검색과 매칭
5.3.1. 프레디케이트가 적어도 한 요소와 일치하는지 확인
-
anyMatch 를 이용하면 된다.
이는 최종 연산으로 boolean 을 return 한다.
5.3.2. 프레디케이트가 모든 요소와 일치하는지 검사
-
allMatch
-
nonMatch 는 allMatch 의 반대연산이다.
일치하는 요소가 없는지 확인하다.
-
anyMatch, allMatch, noneMatch 세 가지 메서드는 스트림 쇼트 서킷 기법, 즉 자바의 &&, || 와 같은 연산을 활용한다.
-
때로 전체 스트림을 처리하지 않았더라도 결과를 반환할 수 있다.
이러한 상황을 쇼트서킷이라고 부른다.
allMatch, noneMatch, findFirst, findAny 등의 연산이 대표적이다.
limit 도 쇼트 서킷 연산이다.
5.3.3. 요소 검색
-
findAny 는 현재 스트림에서 임의의 요소를 반환한다.
값이 없을 수 있기 때문에 Optional 을 반환한다.
Optional 이란?
-
Optional<T> 클래스는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스다.
isPresent() 는 Optional 이 값을 포함하면 true, 그렇지 않으면 false 를 반환한다.
ifPresent(Consumer<T> block) 은 값이 있으면 주어진 블록을 실행한다.
T get() 은 값이 존재하면 값을, 없으면 NoSuchElementException 을 일으킨다.
T orElse(T other)는 값이 있으면 값, 없으면 기본값을 반환한다.
5.3.4. 첫 번째 요소 찾기
-
findFirst.
이 역시 Optional 을 반환한다.
-
병렬에서는 findAny 는 쉬우나 findFirst 는 찾기 어렵다.
그래서 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny 를 사용한다.
5.4. 리듀싱
-
모든 스트림 요소를 처리해서 값으로 도출하는 것을 “리듀싱 연산” 이라고 한다.
함수형 프로그래밍 언어 용어로는 이 과정이 마치 종이를 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드(fold)라고 부른다.
5.4.1. 요소의 합
-
int sum = 0; for (int x : numbers){ sum += x; }
reduce 를 사용하면 아래와 같이 코드를 짤 수 있다.
int sum = numbers.stream().reduce(0, (a, b) -> a + b); // 초깃값은 0 이다. // 두 요소를 조합해서 새로운 값을 만든다. 결과값은 계속 a 로 전달된다.
method reference 를 사용해 더 간단히 만들 수 있다.
int sum = numbers.stream().reduce(0, Integer::sum)
초깃값 없음
초깃값을 받지 않도록 오버로드된 reduce 도 있으며, 이 reduce 는 Optional 객체를 반환한다.
Optional<Integer> sum = numbers.stream().reduce((a,b) -> (a+b));
return 값이 Optional 인 이유는 스트림에 아무 요소가 없는 경우 합계를 반환할 수 없기 때문이다.
5.4.2. 최댓값과 최솟값
-
Optional<Integer> max = numbers.stream().reduce(Integer::max); Optional<Integer> min = numbers.stream().reduce(Integer::min);
-
int count = menu.stream() .map(d -> 1) .reduce(0, (a,b) -> a+ b);
위와 같이 map 과 reduce 를 연결하는 기법을 맵 리듀스(map-reduce)패턴이라 하며, 쉽게 병렬화하는 특징 때문에 구글이 웹 검색에 적용하면서 유명해졌다.
-
단계적 반복으로 합계를 구하는 것과 reduce 를 이용해서 합계를 구하는 것은 차이가 있다.
reduce 를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce 를 실행할 수 있게 된다.
반복적인 합계에서는 sum 변수를 공유해야 하므로 쉽게 병렬화가 어렵다.
-
컬렉션으로 스트림을 만드는 stream 메서드를 parallelStream 로 바꾸는 것만으로도 별다른 노력 없이 병렬성을 얻을 수 있다.
map, filter 등은 람다나 메서드 레퍼런스가 내부적인 가변 상태를 갖지 않는다는 가정 하에 상태를 갖지 않는 연산(stateless operation)이다.
reduce, sum, max 같은 연산은 결과를 누적할 내부 상태가 필요하다. 스트림에서 처리하는 요소 수와 관계없이 내부 상태의 크기는 한정(bounded)되어 있다.
반면 sorted 나 distinct 같은 연산은 filter 나 map 처럼 스트림을 입력으로 받아 다른 스트림을 출력하는 것처럼 보일 수 있으나, 이들은 과거 이력을 알고 있어야 한다. 예를 들어 어떤 요소를 출력 스트림으로 추가하려면 모든 요소가 버퍼에 추가되어 있어야 한다. 연산을 수행하는 데 필요한 저장소의 크기는 정해져있지 않다. 따라서 데이터 스트림의 크기가 크거나 무한이라면 문제가 생길 수 있다. 따라서 이러한 연산은 내부 상태를 갖는 연산(stateful operation)으로 간주할 수 있다.
5.5. 실전 연습
5.5.1. 거래자와 트랜잭션
5.5.2. 실전 연습 정답
5.6. 숫자형 스트림
-
primitive type 을 사용하는 많은 경우 박싱비용이 숨어있다.
이를 방지하기 위해 기본형 특화 스트림(primitive stream specialization) 을 제공한다.
5.6.1. 기본형 특화 스트림
-
int 요소에 특화된 IntStream, double 요소에 특화된 DoubleStream, long 요소에 특화된 LongStream 을 제공한다.
숫자 스트림으로 매핑
-
스트림을 특화 스트림으로 변환할 때는 mapToInt, mapToDouble, mapToLong 세 가지 메서드를 가장 많이 이용한다.
이들 메서드는 map 과 정확히 같은 기능을 수행하지만, Stream<T> 대신 특화된 스트림을 반환한다.
객체 스트림으로 복원하기
-
IntStream intStream = menu.stream().mapToInt(Dish::getCalories); Stream<Integer> stream = intStream.boxed();
기본값: OptionalInt
-
Optional 의 기본형 특화형인 OptionalInt, OptionalDouble, OptionalLong 등이 있다.
5.6.2. 숫자 범위
-
IntStream 과 LongStream 에서는 range 와 rangeClosed 라는 두 가지 정적 메서드를 제공한다.
두 메서드 모두 첫 번째 인수로 시작값을, 두 번째 인수로 종료값을 갖는다.
range 메서드는 종료값이 결과에 포함되지 않는 반면, rangeClosed 는 시작값과 종료값이 결과에 포함된다는 점이 다르다.
5.6.3. 숫자 스트림 활용 : 피타고라스 수
피타고라스 수
세 수 표현하기
좋은 필터링 조합
집합 생성
b값 생성
-
mapToObj 을 통해 primitive to Obj map 을 할 수 있다.
a값 생성
코드 실행
개선할 점?
5.7. 스트림 만들기
5.7.1. 값으로 스트림 만들기
-
임의의 수를 인수로 받는 정적 메서드 Stream.of 를 이용해서 스트림을 만들 수 있다.
Stream<String> stream = Stream.of(“Java 8 ”, “Lambdas ”, “In ”, “Action”);
-
empty 메서드를 이용해 스트림을 비울 수 있다.
Stream<String> emptyStream = Stream.empty();
5.7.2. 배열로 스트림 만들기
-
Arrays.stream 을 이용해서 스트림을 만들 수 있다.
int[] numbers = {2, 3, 5, 7, 11, 13}; int sum = Arrays.stream(numbers).sum();
5.7.3. 파일로 스트림 만들기
-
파일을 처리하는 등의 I/O 연산에 사용하는 자바의 NIO API(Nonblocking I/O)도 스트림 API 를 활용할 수 있도록 업데이트되었다.
java.nio.file.Files 의 많은 정적 메서드가 스트림을 반환한다.
Files.lines 는 주어진 파일의 행 스트림을 문자열로 반환한다.
long uniqueWords = 0; try{Stream<String> lines = Files.lines(Paths.get(“data.txt”), Charset.defaultCharset())){ uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(“ “))) .distinct() .count(); }catch(IOException e){ }
5.7.4. 함수로 무한 스트림 만들기
-
Stream.iterate 와 Stream.generate 는 무한 스트림(infinite stream) 을 만들 수 있다.
iterate 와 generate 에서 만든 스트림은 요청할 때마다 주어진 함수를 이용해서 값을 만든다.
보통 무한한 값을 출력하지 않도록 limit(n) 함수를 함께 연결해서 사용한다.
iterate
-
Stream.iterate(0, n -> n+2 ) .limit(10) .forEach(System.out::println);
0은 초깃값이고, 람다를 이용해 계속 값을 생성해낸다.
첫번째 요소는 0이다.
-
무한 스트림(inifinite stream)은 언바운드 스트림(unbounded stream)이라고 표현한다.
-
일반적으로 연속된 일련의 값을 만들 때는 iterate 를 사용한다.
generate
-
iterate 와 달리 generate 는 생산된 각 값을 연속적으로 계산하지 않는다.
generate 는 Supplier<T> 를 인수로 받아 새로운 값을 생산한다.
Stream.generate(Math::random) .limit(5) .forEach(System.out::println);
-
스트림을 병렬로 처리하면서 올바른 결과를 얻으려면 불변 상태 기법을 고수해야 한다.
5.8. 요약
-
스트림 API 를 이용하면 복잡한 데이터 처리 질의를 표현할 수 있다.
-
filter, distinct, skip, limit 메서드로 스트림을 필터링하거나 자를 수 있다.
-
map, flatMap 메서드로 스트림의 요소를 추출하거나 변환할 수 있다.
-
findFirst, findAny 메서드로 스트림의 요소를 검색할 수 있다.
allMatch, nonMatch, anyMatch 메서드를 이용해서 주어진 프레디케이트와 일치하는 요소를 스트림에서 검색할 수 있다.
-
이들 메서드는 쇼트서킷(short-circuit), 즉 결과를 찾는 즉시 반환하며, 전체 스트림을 처리하지는 않는다.
-
reduce 메서드로 스트림의 모든 요소를 반복 조합하며 값을 도출할 수 있다.
예를 들어 reduce 로 스트림의 최댓값이나 모든 요소의 합계를 계산할 수 있다.
-
filter, map 등은 상태를 저장하지 않는 상태 없는 연산(stateless operation)이다.
reduce 같은 연산은 값을 계산하는 데 필요한 상태를 저장한다.
sorted, distinct 등의 메서드는 새로운 스트림을 반환하기에 앞서 스트림의 모든 요소를 버퍼에 저장해야 한다.
이런 메서드를 상태 있는 연산(stateful operation) 이라고 부른다.
-
IntStream, DoubleStream, LongStream 은 기본형 특화 스트림이다.
이들 연산은 각각의 기본형에 맞게 특화되어 있다.
-
컬렉션뿐 아니라 값, 배열, 파일, iterate 와 generate 같은 메서드로도 스트림을 만들 수 있다.
-
크기가 정해지지 않은 스트림을 무한 스트림이라고 한다.
'프로그래밍 놀이터 > 안드로이드, Java' 카테고리의 다른 글
[Java8 In Action] #7 병렬 데이터 처리와 성능 (0) | 2018.12.27 |
---|---|
[Java8 In Action] #6 스트림으로 데이터 수집 (0) | 2018.12.26 |
[Java8 In Action] #4 스트림 소개 (0) | 2018.12.24 |
[Java8 In Action] #3 람다 표현식 (0) | 2018.12.23 |
[Java8 In Action] #2 동적 파라미터화 코드 전달하기 (0) | 2018.12.22 |
댓글