4. 스트림 소개
Collection은 자바에서 가장 많이 사용하는 기능으로 모든 자바 어플리케이션은 컬렉션을 만들고 처리하는 과정을 포함한다. 컬렉션은 대부분의 프로그래밍의 필수 요소로 데이터를 그룹하고 처리할 수 있지만 완벽한 컬렉션 연산에는 아직 먹었다
SELECT name FROM dishes WHERE calories <400
이라는 SQL 질의에서 보듯이 요리의 속성을 어떻게 필터링할 것인가 구현할 필요가 없다- 예를 들어 자바처럼 반복자, 누적자등을 용할 필요가 없다
- SQL은 기대하는 것을 직접 표현할 수 있고 어떻게 구현할지 명시할 필요없이 구현이 자동으로 제공된다. 컬렉션은?
- 대용량의 컬렉션은 멀티코어를 이용한 병렬처리가 필요하지만 병렬처리 코드는 구현이 복잡하고 어려우며 디버깅도 어렵다
- 정답은? 스트림API
4.1 스트림이란?
스트림
- 자바API에 새로 추가된 기능
- 선언형(데이터를 처리하는 임시구현 코드 대신 질의로 표현가능)으로 컬렉션 처리가능
- 일단은 ‘데이터 컬렉션 반복을 멋지게 처리하는 기능’으로 생각하자
- 스레드 코드 구현없이 데이터를 투명하게 병렬 처리 가능하다
예제로 비교 : 저칼로리의 요리명을 반환하고 칼로리를 기준으로 요리를 정렬한다
자바7로 구현한 코드 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//누적자로 요소 필터링
List<Dish> lowCalroricDishes = new ArrayList<>();
for(Dish d : menu) {
if(d.getCalories() < 400) {
lowCalroricDishes.add(d);
}
}
//익명클래스로 요리 정렬
Collections.sort(lowCaloricDishes, new Comparator<Dish>) {
public int comare(Dish d1, Dish d2) {
return Integer.compare(d1.getCalories(), d2.getCalories());
}
}
//정렬된 리스트를 처리하면서 요리 이름 선택
List <String> lowCaloricDishesName = new ArrayList<>();
for(Dish d : lowCaloricDishes) {
lowCaloricDishesName.add(d.getName());
}lowCaloricDishes: 가비지 변수, 컨테이너 역할만 하는 중간 변수
자바8에서 이러한 세부구현은 라이브러리 내에서 모두 처리한다
자바 8로 구현 1
2
3
4
5
6
7
8import static java.util.Comparator.comapring;
import static java.util.stream.Collectors.toList;
List<String> lowCaloricDishesName =
menu.stream()
.filter(d -> d.getCalories() < 400) //400칼로리 이하 요리 선택
.sorted(comparing(Dish::getCalories))// 칼로리로 요리 정렬
.map(Dish::getName) //요리명 추출
.collect(toList()); //모든 요리명을 리스트에 저장stream()을 ParallelStream()으로 바꾸면 이 코드를 멀티코어 아키텍처에서 병렬로 실행할 수 있다
병렬처리 실행 예제 1
2
3
4
5
6List<String> lowCaloricDishesName =
menu.parallelStream() //병렬처리
.filter(d -> d.getCalories() < 400)
.sorted(comparing(Dish::getCalories))
.map(Dish::getName)
.collect(toList());스트림이라는 새로운 기능을 썼을때 이득
- 선언형으로 코드를 구현 가능
- 루프,if등의 제어 블럭을 사용해서 어떻게 동작을 구현할지 지정할 필요 없이
‘저칼로리 요리만 선택’같은 동작의 수행을 지정할 수 있다 - 지금까지 공부한 선언형 코드와 동작 파라미터화를 활용하면 요구사항 쉽게 대응
- 기존 코드 복붙 x, 람다를 이용해서 다른 필터링 코드도 쉽게 구현 가능
- 루프,if등의 제어 블럭을 사용해서 어떻게 동작을 구현할지 지정할 필요 없이
- filter,sorted,map,collect같은 여러 빌딩 블록 연산을 연결해서 복잡한 데이터 처리 파이프라인을 만들 수 있다
- 여러 연산을 파이프라인 연결해도 여전히 가독성과 명확성이 유지된다
- filter -> sorted -> map -> collect로 각 결과가 연결된다
- 이런 빌딩 블록 연산은 고수준 빌딩 블록(high-level build block) 으로 이루어져 있으므로 특정 스레딩 모델에 제한 되지 않고 자유롭게 사용이 가능
- 내부적으로 단일 스레드 모델에도 사용할 수 있지만 멀티코어 아키텍처를 최대한 투명하게 활용할 수 있도록 구현되어 있다
- 스트림 API덕분에 데이터 처리 과정을 병렬화 하면서 스레드와 락을 걱정할 필요가 없다
- 선언형으로 코드를 구현 가능
자바8의 스트림 API 특징 요약
- 선언형 : 더 간결하고 가독성이 좋아짐
- 어셈블러블(조립가능) : 유연성이 좋아짐
- 병렬화 : 성능이 좋아짐
예제에서 사용할 내용
Dish.java 요리Dish는 다음과 같은 불변형 클래스 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package me.rkaehdaos.streamdemo;
import lombok.AllArgsConstructor;
import lombok.Getter;
public class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private final Type type;
public String toString() {
return name;
}
public enum Type { MEAT, FISH, OTHER}
}
1 | List<Dish> menu = Arrays.asList( |
4.2 스트림 시작하기
- 스트림 : 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소
- 연속된 요소
- 스트림은 컬렉션처럼 특정 요소의 연속된 값 집합의 인터페이스 제공
- 컬렉션은 자료구조이므로 시간과 공간의 복잡성과 관련된 요소 저장 및 접근 연산이 주를 이룬다(예: ArrayList,LinkedList중 어떤 것을 사용할 것인가?)
- 스트림은 filter,sorted,map처럼 표현 계산식이 주를 이룬다
- 즉 컬렉션의 주제는 데이터고 스트림의 주제는 계산이다
- 소스
- 스트림은 컬렉션, 배열, I/O 자원등의 데이터 제공 소스로부터 데이터를 소비(consume)한다
- 정렬된 컬렉션으로 스트림을 생성하면 정렬이 그대로 유지된다
- 리스트로 스트림을 만들면 스트림의 요소는 리스트 요소와 같은 순서를 유지한다
- 데이터 처리 연산
- 스트림은 함수형 프로그래밍 언어의 일반적인 연산과 DB와 비슷한 연산을 지원한다
- filter, map, reduce, find, match, sort등으로 데이터 조작 가능
- 스트림 연산은 순차적 혹은 병렬로 실행할 수 있다
- 파이프라이닝
- 대부분의 스틀김 연산은 스트림 연산끼리 파이프 라인을 구성할 수 있도록 스트림 자신을 반환한다
- 연산 파이프 라인은 데이터 소스에 적용하는 DB 질의와 비슷하다
- 내부 반복
- 컬렉션은 반복자를 이용해서 명시적으로 반복하며
스트림은 내부 반복을 지원한다위 설명을 반영한 예제 1
2
3
4
5
6
7
8
9import static java.util.streamCollectors.toList;
List<String> threeHighCaloricDishNames =
menu.stream() //menu(Dish List)에서 스트림을 얻는다
.filter(d -> d.getCalories() > 300) //파이프 라인 1 : 고칼로리 필터링
.map(Dish::getName) //요리명 추출
.limit(3) //선착순 3개 선
.collect(toList()); //결과를 다른 리스트로 저장
System.out.println(threeHighCaloricDishNames);// 결과: [pork, beef, chicken]
- 컬렉션은 반복자를 이용해서 명시적으로 반복하며
- 연속된 요소
- 예제 설명
- menu에 stream메서드를 호출해서 요리 리스트로부터 스트림을 얻는다
- 데이터 소스 는 요리 리스트(메뉴)이며 데이터 소스는 연속된 요소 를 스트림에 제공한다
- collect를 제외환 모든 연산은 서로 파이프라인 을 형성할 수 있게 스트림 반환
- 파이프라인은 소스에 적용하는 질의 같은 존재
- 마지막으로 collect로 파이프라인을 처리 결과를 반환한다
- collect는 스트림이 아니라 List를 반환하는 것에 유의한다
- 마지막에 collect를 호출할 때까지 menu에서는 아무것도 선택되지 않고 아무 출력 결과도 없다. 즉 collect 호출 전까지 메서드 호출이 저장되는 효과가 있다
- 작업
- filter: 람다를 인수로 받아 스트림에서 특정 요소를 제외한다
- map: 람다를 이용해서 한요소를 다른 요소로 변환 혹은 정보를 추출한다
람다 d->d.getname()대신 메서드 레퍼런스 Dish::getName을 사용하였다 - limit: 정해진 개수 이상 스트림에 저장되지 못하게 스트림 크기를 축소(truncate)한다
- collect: 스트림을 다른 형식으로 변환한다
- collect는 나중에 더 공부
- 현재는 다양한 변환 방법을 인수로 받아서 스트림에 누적된 요소를 특정 결과로 반환 시키는 기능을 수행하는 정도로 이해
- toList()는 스트림을 리스트로 변환하라고 지시하는 인수다
- 이득
- 좀 더 선언형으로 데이터 처리
- 스트림 라이브러리에서 필터링(filter), 추출(map), 축소(limit)를 제공하기에 직접 이 기능을 구현할 필요가 없다
- 결과적으로 스트림API는 파이프라인을 더 최적화 할 수 있는 유연성을 제공한다
4.3 스트림과 컬렉션
- 컬렉션,스트림은 모두 연속된 요소 형식의 값을 저장하는 자료ㅕ구조의 인터페이스 제공
- 연속된(sequenced): 순서에 상관없이 어떤 값에 접속이 아닌 순차적으로 값에 접근한다는 것을 의미한다
- 컬렉션 스트림의 가장 큰차이 : 데이터를 언제 계산하느냐
- DVD나 mkv의 큰 영화를 컬렉션이라고 한다면, 비디오 스트리밍은 스트림으로 생
- 컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조
- 컬렉션 추가 삭제를 위해선 모든 값이 메모리에 있고 추가,삭제할 요소도 미리 계산되어 있어야한다
- 스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 자료구조다
-> 스트림에 요소를 추가하거나 스트림에서 요소를 제거할 수 없다
-> 사용자가 요청하는 값만 스트림에서 추출한다는 것이 핵심
-> 결과적으로 스트림은 생산자와 소비자 관계를 형성한다
-> lazy collection - 사용자가 데이터를 요청할 때만 값을 계산 - 컬렉션은 적극적 생성
- 생산자 중심 : 팔기 전에 이미 창고를 가득 채움
- 예) 소수 출력 -> 무한 루프로 계속 계산및 추가 반복 -> 영원히 결과 X
4.3.1 단 한번의 탐색
- 반복자와 마찬가지로 스트림도 한 번만 탐색 가능
- 탐색된 스트림 요소는 소비되며 재탐색하려면 소스에서 새로운 스트림을 만들어야한다
- 컬렉션처럼 반복 사용이 가능한 데이터 소스여야 가능한 일이며, 만약 데이터 소스가 I/O채널이라면 소스를 반복 사용할 수 없기 때문에 새로운 스트림을 만들 수 없다
스트림은 단 한번만 소비 될 수 있다 1
2
3
4List <String> title = Arrays.asList("Java8", "In", "Action");
Stream<String> s = title.stream();
s.forEach(system.out::println); //title의 각 단어를 출력
s.forEach(system.out::println); // java.lang.IllegalStateException!!!
4.3.2 외부 반복과 내부 반복
- 스트림, 컬렉션의 또다른 차이점
- 컬렉션 인터페이스: for-each등으로 사용자가 직접 요소를 반복해야한다 - 외부 반복
- 스트림 라이브러리: (반복을 알아서 처리하고 결과 스트림을 어딘가 저장하는)내부 반복
1 | //컬렉션: for-each 루프를 이용하는 외부반복 |
외부반복 상황 : 컬렉션은 명시적으로 컬렉션의 항목을 하나씩 가져와서 처리한다
A: 장난감을 정리하라. 어떤 장난감이 있는가?
B: 공이 있다
A: 공을 상자에 담아라. 또 어떤 장난감이 있는가?
B: 인형이 있다
A: 인형을 상자에 담아라. 또 어떤 장난감이 있는가?
B: 책이 있다
A: 책을 상자에 담아라. 또 어떤 장난감이 있는가?
B: 아무것도 없다
A: 굿잡!내부반복 상황 : 바닥에 있는 모든 장난감을 상자에 담아라
- 이득
- 한 손에 인형, 한손에 공을 들고 동시에 2개씩 처리가 가능 -> 병렬처리
- 모든 장난감을 상자 근처로 이동 시킨후 상자에 한꺼번에 넣을 수 있다 -> 최적화
- 이득
병렬성
- for-each등 외부 반복일때는 병렬성을 스스로 관리
- 병렬성을 포기하거나
- synchronized로 시작하는 힘들고 긴 전쟁을 시작해야 한다
- 스트림을 사용하면 병렬성 구현을 자동으로 선택한다
- for-each등 외부 반복일때는 병렬성을 스스로 관리
결론
- 스트림은 내부 반복을 사용하므로 반복을 개발자가 신경 쓰지 않아도 된다
- 이점을 얻기 위해선 filter,map같은 반복을 숨겨주는 연산 리스트가 미리 정의 필요
- 반복을 숨겨주는 대부분 연산은 람다 표현식을 인수로 받으므로 동작 파라미터화를 활용할 수 있다
- 자바에서는 개발자가 복잡한 데이터 처리 질의를 표현하도록 다양한 추가 연산 제공
4.4 스트림 연산
- filter,map,limit처럼 서로 파이프라인으로 연결을 할 수 있는 스트림 연산을 중간 연산(intermedicate operation)이라고 한다
- collect처럼 스트림을 단는 연산을 최종 연산(terminal operation)이라고 한다
4.4.1 중간 연산(intermedicate operation)
- 중간 연산은 다른 스트림을 반환 하므로 여러 중간 연산을 연결하여 질의 생성 가능
- 중간 연산은 단말 연산을 스트림 파이프라인에 실행하기 전까지 아무 연산도 수행하지 않는다 - lazy
- 중간 연산을 합친 다음에 합쳐진 중간 연산을 최종 연산으로 한번에 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26실제로 이런식으로 출력 코드 찍으면 안된다. 어디까지나 학습용
public void test1() {
List<String> names =
menu.stream()
.filter( d -> {
System.out.println("filtering: "+d.getName());
return d.getCalories() > 300;
})
.map( d -> {
System.out.println("mapping: "+d.getName());
return d.getName();
})
.limit(3)
.collect(Collectors.toList());
System.out.println(names);
}
//결과
/*
filtering: pork
mapping: pork
filtering: beef
mapping: beef
filtering: chicken
mapping: chicken
[pork, beef, chicken]
*/ - 최적화 효과
- 300 칼로리가 넘는 여러개의 요리중 처음 3개만 선택 ->limit, 쇼트서킷
- 덕분에 출력도 3개만 된 것을 알 수 있다
- filter, map이 한과정으로 병합 -> 루프 퓨전(loop fusion)
4.4.2 최종 연산
- 최종 연산은 스트림 파이프 라인에서 결과를 돌출한다
- 보통 최종 연산에 의해 스트림 이외의 결과가 반환된다
최종연산 예 1
2
3//forEach
//소스의 각 요리에 람다를 적용하고 void를 반환하는 최종 연산
menu.stream().forEach(System.out::println);
4.4.3 스트림 이용
스트림 이용과정 요약
- 질의를 수행할 (컬렉션 같은) 데이터 소스
- 스트림 파이프 라인을 구성할 중간 연산 연결
- 스트림 파이프라인을 실행하고 결과를 만들 최종 연산
스트림 파이프라인과 빌더 패턴
- 스트림 파이프라인의 개념은 빌더 패턴과 비슷하다
- 빌더패턴에서는 호출을 연결해서 설정을 만든다 - 스트림에서 중간연산 연결과 같다
- 준비된 설정 build 메서드를 호출 - 스트림에서 최종 연산
4.5 요약
- 스트림은 소스에서 추출된 연속 요소로, 데이터 처리 연산을 지원한다
- 내부 반복을 지원하며, 내부 반복은 filter, map, sorted등의 연산으로 반복 추상화
- filter,map처럼 스트림을 반환하면서 다른 연산과 연결 가능한 연산을 중간 연산이라고 한다
- 중간연산으로 파이프라인을 구성할 수 있지만 중간 연산으로는 어떤 결과도 생성X
- forEach, count, collect처럼 스트림 파이프라인을 처리하여 스트림이 아닌 결과를 반환하는 연산을 최종 연산이라고 한다
- 스트림의 요소는 요청할 때만 계산된다
5. 스트림 활용
- 4장에서 스트림을 사용해서 외부 반복을 내부 반복으로 바꾸는 방법을 살펴봤다
다시 한번 복습하는 내부 반복과 외부 반복 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//외부 반복 : 데이터 컬렉션 반복을 명시적으로 관리
List<Dish> vegetarianDishes = new ArrayList<>();
for(Dish d : menu){
if (d.isVegetarian()){
vegetarianDishes.add(d);
}
}
//내부 반복
//filter와 collect연산을 지원하는 스트림 API를 사용해서
//데이터 컬렉션 반복을 내부적으로 처리한다
//다음처럼 filter메서드에 필터링 연산을 인수로 넘겨주면 된다
import static java.util.stream.Collectors.toList;
List<Dish> vegetarianDishes =
menu.stream()
.filter(Dish::isVegetarian)
.collect(toList()); - 데이터를 어떻게 처리할지 스트림 API가 관리 -> 편하게 데이터 관련 작업 가능
- 스트림 API 내부적으로 다양한 최적화 가능, 병렬 실행 여부도 결정 가능
- 이러한 일은 순차적 반복을 단일 스레드로 구현 했기에 외부 반복으로는 불가능
- 5장에서는 스트림 API가 지원하는 다양한 연산을 살펴본다
5.1 필터링과 슬라이싱
5.1.1 predicate로 필터링
- 스트림 인터페이스는 이제 겨우 익숙해지고 있는 filter메서드를 지원한다
- filter 메서드는 predicate(boolean을 리턴)함수를 인수로 받아서 predicate와
일치하는 모든 요소를 포함하는 스트림을 반환한다 - 위에서 보았던 내부 반복 코드처럼 모든 채식 요리를 필터링해서 채식 메뉴를 만들 수
있다
5.1.2 고유 요소 필터링
- 스트림은 고유요소로 이루어지는 스트림을 반환하는
distinct
메서드를 지원한다 - 고유여부는 스트림에서 만든 객체의 hashCode, equals로 결정된다
리스트의 모든 짝수를 선택하고 중복을 필터링하는 예제 1
2
3
4
5List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i%2==0)
.distinct()
.forEach(System.out::println);
5.1.3 스트림 축소
- 스트림은 주어진 사이즈 이하의 크기를 갖는 새로운 스트림을 반환하는
limit(n)메서드를 지원한다 - 정렬된 스트림외에도 정렬되지 않는 스트림(소스가 Set)에도 사용할 수 있다
- 소스가 정렬되지 않았다면 limit의 결과도 정렬되지 않은 상태로 반환된다
5.1.4 요소 건너뛰기
- 스트림은 처음 n개 요소를 제외하는 스트림을 반환하는 skip(n)메서드를 지원한다
- n개 이하의 요소를 포함하는 스트림에 skip(n)을 하면 빈 스트림이 반환된다
- limit(n)과 skip(n)은 서로 상호 보완적인 연산을 수행한다
300칼로리 이상의 처음 두 요리를 건더뛴후 나머지 고칼로리 요리를 반환 1
2
3
4List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(toList());
5.2 매핑
- 특정 객체에서 특정 데이터를 선택하는 작업
- 데이터 처리 과정에서 자주 수행되는 연산
- ex) SQL 테이블에서 특정한 열 선택
- 스트림 API의 map, flatMap 메서드가 특정 데이터를 선택하는 기능을 제공한다
5.2.1 스트림의 각 요소에 함수 적용
- 스트림은 함수를 인수로 받는 map 메서드를 지원한다
- 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑
- 이 과정은 기존 값을 ‘고친다(modify)’라는 개념보다는 ‘새로운 버전을 만든다’에 가까운 개념이므로 ‘변환(transforming)’에 가까운 ‘매핑(mapping)’이라는 단어를 사용한다
Dish::getName을 map메서드에 인자로 전달해서 스트림의 요리명을 '추출' 1
2
3List<String> dishNames = menu.stream()
.map(Dish::getName)
.collect(toList()); - getName이 문자열을 반환하므로 map메서드의 출력 스트림은 Stream
형식 - 단어 리스트가 주어졌을때 각 단어가 포함하는 글자 수의 리스트를 반환한다면?
String::length를 map에 전달하여 문제를 해결 1
2
3
4List<String> words = Arrays.asList("Java8", "Lambdas", "In", "Action");
List<Integer> wordLengths = words.stream()
.map(String::length)
.collect(toList()); - map 메서드를 연결(chaining)할 수 도 있다
map을 chaining해서 Dish리스트에서 각 요리명의 길이를 추출하였다 1
2
3
4
5
6//각 연산의 출력 스트림 형태를 잘 살펴보자
List<Integer> dishNameLengths =
menu.stream() //Stream<Dish>
.map(Dish::getName) //Stream<String>
.map(String::length) //Stream<Integer>
.collect(toList()); //List<Integer>
5.2.2 스트림 평면화
- 위에서 map을 이용해서 리스트의 각 단어의 길이 반환 방법을 확인했다
- 이를 응용해서 리스트에서 고유 문자 로 이루어진 리스트를 반환해보자
- ex) [“Hello”, “World”] -> [“H”, “e”, “l”, “o”, “W”, “r”, “d”]
쉽게 해결하려다가 마주치는 문제점. 기대값과 다른 결과를 얻는다 1
2
3
4
5
6// 리스트 단어를 문자로 매핑후
// distinct로 필터링해서 쉽게 해결?
words.stream()
.map(word -> word.split(""))//문제점: 람다가 String[](문자열 배열)을 반환함
.distinct() // 따라서 Stream<String[]>이 스트림 형식
.collect(toList()); // 기대한 결과는 문자열 스트림인 Stream<String> - 위처럼 우리가 원하는 문자열 스트림인 Stream
이 아닌 Stream<String[]>이 나온다 - 해결 방법 :
flatMap
을 사용해서 이 문제를 해결할 수 있다
map과 Arrays.stream 활용
- 현재 위의 문제에서 필요한 것은 배열 스트림이 아닌 문자열 스트림이 필요하다
Arrays.stream()
: 특정 타입의 배열을 받아 그타입의 스트림을 만드는 메서드Arrays.stream()을 사용하여 문자열 배열을 스트림으로 만드는 예제 1
2String[] arrayOfWords = {"Goodbye", "World"};
Stream<String> streamOfworlds = Arrays.stream(arrayOfWords);- 위의 문제에 이 ‘Arrays.stream()’을 적용시켜보자
전혀 문제가 해결되지 않음을 확인하자 1
2
3
4
5
6
words.stream() //Stream<String>
.map(word -> word.split("")) // Stream<String[]>
.map(Arrays::stream) // Stream<Stream<String>>
.distinct() // Stream<Stream<String>>
.collect(toList()); - 스트림 리스트가 tream<Stream
>가 만들어지면서 문제 해결이 되지 않는다 - 문제 해결을 위해서는 먼저 각 단어를 개별 문자열로 이루어진 배열로 만든 후
각 배열을 별도의 스트림으로 만들어야한다
flatMap 사용
- flatMap : 각 배열을 스트림이 아닌 스트림의 컨텐츠 로 매핑한다
- map(Arrays::stream)과 달리 flatMap은 하나의 평면화된 스트림을 반환한다
flatMap을 사용하면 완전한 문제 해결이 가능하다 1
2
3
4
5
6List<String> uniqueCharacters =
words.stream()
.map(word -> word.split(""))
.flatMap(Arrays::stream)
.distinct()
.forEach(System.out::println); - 메서드들의 정의부분과 반환부분을 잘 살펴보자
예제에서 살펴볼 메서드들의 정의 부분 시그니처 1
2
3
4
5
6
7//Arrays.stream : 배열을 스트림으로 만든다
public static <T> Stream<T> stream(T[] array)
//map 선언
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
//flatmap 선언
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper); - flatMap은
- flatMap메서드는 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다
flatMap의 또 다른 사용 예
1 | class Foo { |
flatMap 숫자쌍
- 두개의 숫자 리스트 A,B가 주어졌을떄 A,B의 모든 조합 쌍 반환
- 두개의 map으로는 두 리스트를 반복해서 만드는 숫자쌍?
-> 답없음. Stream<Stream<int[]>>가 나와버림 - 결과를 stream<Integer[]>로 평면화한 스트림이 필요함 -> flatMap 사용
flatmap 사용후 Arrays.toString을 이용해 손쉽게 배열 출력 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17List<Integer> input1 = Arrays.asList(1, 2, 3);
List<Integer> input2 = Arrays.asList(3,4);
input1.stream()
.flatMap(i -> input2.stream() //평면화
.map(j -> new int[]{i,j})
)
.map(a -> Arrays.toString(a)) //배열출력
.forEach(System.out::println);
// result
/*
[1, 4]
[2, 3]
[2, 4]
[3, 3]
[3, 4]
*/ - 만약 위 쌍에서 숫자쌍의 합이 3으로 나누어 떨어지는 쌍만 반환하려면?
-> filter와 프레디케이트사용 –> 스트림의 요소 필터링
1 | List<Integer> input1 = Arrays.asList(1, 2, 3); |
5.3 검색과 매칭
- 특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리도 자주 이용되는 기능
- 스트림 API는 다양한 유틸리티 메서드를 지원한다
5.3.1 프레디케이트가 적어도 한 요소와 일치하는지 확인
- anyMatch : boolean을 반환하는 최종연산
menu에 채식 요리가 있는지 확인하는 예제 1
2
3if(menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println("the menu is (somewhat)vegetarian friendly");
}
5.3.2 프레디케이트가 모든 요소와 일치하는지 확인
- allMatch
메뉴가 건강식(모든 요리가 1000칼로리 이하)인가? 1
2boolean isHealthy = menu.stream()
.allMatch(d -> d.getCalories() < 1000);
noneMatch
- allMatch와 반대연산, 주어진 프레디케이트와 일치하는 요소가 없는지 확인 가능
메뉴가 건강식(모든 요리가 1000칼로리 이하)인가?를 noneMatch로 구현 1
2boolean isHealthy = menu.stream()
.noneMatch(d -> d.getCalories() >= 1000);
쇼트서킷
- 논리 연산자와 조합된 다른 연산식이나 조건식이 생략되는 경우를 쇼트서킷이라 한다
- 논리합은 둘중 하나면 참이므로 왼쪽항이 참이면 우측항 연산이 필요가 없다
- 논리곱은 둘다 참이여야 하므로 왼쪽창이 거짓이면 우측항 연산이 필요가 없다
- anyMatch, allMatch, noneMatch 모두 스트림 쇼트서킷 기법을 이용해서 모든 스트림의 요소를 전부 처리하지 않고 결과를 반환할 수 있다
- 무한한 스트림을 유한하게 줄여주는 limit도 쇼트 서킷 연산이다
5.3.3 요소 검색
- findAny : 스트림에서 임의 요소를 반환한다. 다른 스트림 연산과 연결해서 사용가능
채식 요리 선택 1
2
3
4Optional<Dish> dish =
menu.stream()
.filter(Dish::isVegetarian)
.findAny(); - 스트림 파이프 라인은 내부적으로 단일 과정으로 실행 가능하게 최적화된다
- 여기서는 쇼트 서킷을 사용해서 결과를 찾는 즉시 실행을 종료한다
Optional이란?
- 위코드에서 사용된 Optional
클래스(java.util.Optional)는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스다 - 위의 예제에서 findAny가 아무 요소도 반환하지 않아서 null이 될 수도 있다
- null은 쉽게 에러를 일으키므로 자바 8에서 이 Optional
가 만들어짐 - 나중에 다시 공부
- 일단은 값이 존재하는지 확인하고 값이 없을 때 어떻게 할건지 강제하는 기능을 제공한다고만 기억한다
- isPresent()
- Optional이 값을 포함하면 true, 포함하지 않으면 false를 반환한다
- ifPresent(Consumer
block) - 값이 있으면 주어진 블럭을 실행
- Consumer는 공부했듯이 T형식의 인수를 받으며 void를 반환하는 람다를 전달가능
- T get()
- 값이 존재하면 값을 반환하고, 값이 없으면 NoSuchElementException 일으킨다
- T orElse(T Other)
- 값이 있으면 값을 반환하고 값이 없으면 기본값을 반환한다
//값이 있으면 출력되고 값이 없으면 아무일도 일어나지 않는다 1
2
3
4menu.stream()
.filter(Dish::isVegetarian)
.findAny()
.ifPresent(d -> System.out.println(d.getName()));
- 값이 있으면 값을 반환하고 값이 없으면 기본값을 반환한다
- isPresent()
5.3.4 첫 번째 요소 찾기
1 | List<Integer> someNumber = Arrays.asList(1,2,3,4,5); |
- findFirst vs findAny
- 병렬실행에서는 첫번쨰 요소를 찾기가 어렵다
- 요소 반환 순서가 상관없다면 병렬 스트림에서 제약이 적은 findAny를 사용한다
5.4 리듀싱
- 지금까지 살펴본 최종 연산은 boolean(allMatch등), void(forEach), 또는 Optional객체(findAny등)을 반환 했다
- collect로 모든 스트림의 요소를 리스트로 모으는 방법도 살펴보았다
- 더 복잡한 질의를 위해선 Interger같은 결과가 나올 떄까지 스트림의 모든 요소를 반복적으로 처리해야한다
- 리듀싱 연산 : 모든 스트림 요소를 처리해서 값으로 돌출하는 질의
- 함수형 프로그래밍 언어로는 폴드(fold)라고 부르며, 이 과정이 마치 종이(스트림)를 작은 조각이 될 떄까지 반복해서 접는 것과 비슷하다는 의미이다
5.4.1 요소의 합
1 | int sum = 0; |
이 코드에는 2개의 파라미터가 사용 되었다
- sum 변수의 초기값 : 0
- 리스트의 모든 요소를 조합하는 연산 : +
만약 모든 숫자를 곱하는 연산을 할 때 또 위의 코드를 복사 붙여 넣고 고쳐야할까?
reduce를 이용하면 반복되는 패턴을 추상화 할 수 있다
- int sum = numbers.stream.reduce(0, (a,b) -> a+b);
reduce는 2개의 인수를 갖는다 - 초기값 0
- 두요소를 조합해서 새로운 값을 만드는 BinaryOperator
예제에서는 람다표현식을 사용하였다
이를 사용해서 다른 람다를 넘겨주면 곱셈 처리가 가능하다 - int product = numbers.stream.reduce(1, (a,b) -> a*b);
- 곱셈에선 처음 초기값을 1로 주었다
reduce 연산과정
- 반복이 없어도 reduce는 자동으로 반복해서 각 요소를 반복한다
- 위에서 숫자 스트림이 4,5,3,9라고 가정한다
- 처음 람다 첫번째 a에는 초기값 0, b에는 스트림의 첫번째인 4가 들어가서 4가 반환
- 누적값인 4로 다시 람다를 호출하여 a에는 4, b에는 다음 스트림인 5 =>9가 반환
- 그다음엔 마찬가지로 스트림 요소 3을 소모하며 12
- 마지막으로 누적값12와 스트림 마지막인 9로 람다를 호출하며 최종 21이 돌출 된다
자바 8에서는 Integer클래스에 두 숫자를 더하는 정적 sum메서드가 있으므로
사실 직접 람다 코드를 구현할 필요가 없다
1 | int sum = numbers.stream().reduce(0, Integer::sum); |
초기값 없음?
->초기값 받지 않도록 오버로드 된 reduce도 있으나 이 reduce는 Optional 객체를 반환
1 | Optional<Integer> sum = numbers.stream().reduce((a,b)->(a+b)); |
- 스트림에 아무 요소가 없으면 초기 값이 없으므로 reduce는 합계를 반환할 수 없다
따라서 합계가 없음을 가리킬 수 있도록 Optional로 객체를 감싼 결과를 반환한다
5.4.2 쵀대값과 최소값
- 두 요소에서 최대값만 반환하는 람다만 있으면 된다
- reduce는 스트림의 모든 요소를 소비해서 람다를 반복 수행해서 값을 생산한다
- 최대값 : Integer.max
- 최소값 : Integer.min
- 람다로 넘겨주어도 되지만 메서드 레퍼런스가 더 읽기 쉽다
- ex) map과 reduce 메서드를 이용한 menu 스트림의 요리개수
스트림 각 요소를 1 매핑후 reduce로 이들의 합계를 계산 1
2
3int count = menu.stream()
.map(d -> 1)
.reduce(0, (a,b)->a+b); - map-reduce pattern
- map과 reduce를 연결하는 기법을 맵 리듀스 패턴이라고 한다
- 쉽게 병렬화하는 특징 덕분에 구글이 웹 검색에 적용하면서 유명해졌다
reduce 메서드의 장점과 병렬화
- 기존 단계적 방법으로 합을 구하는 것과 reduce로 합을 구하는 것의 차이?
- reduce를 사용하면 내부 반복이 추상화되면서 내부에서 병렬로 실행 가능
- 기존 단계반복 합계에서는 sum을 공유해야하므로 쉽게 병렬이 어렵다
- 강제로 동기화해도 병렬화로 얻는 이득이 스레드간의 소모적인 경쟁 때문에 상쇄
- 기존 단계반복의 병렬화는 입력을 분할하고 분할한 입력을 더하고 더한 값을 합쳐야한다
- 기존 가변 누적자 패턴(mutable accumulator pattern)은 병렬화랑 너무 멀다
- 위 코드의 stream()을 parallelStream()으로 바꾸면 병렬로 실행이 가능하다
- 병렬로 실행한 대가
- reduce에 넘겨준 람다의 상태(인스턴스 변수등) 바뀌면 안된다
- 연산이 어떤 순서로 실행되더라도 결과가 바뀌지 않는 구여야 한다
스트림 연산 : stateful vs stateless
- 스트림을 이용하면 원하는 모든 연산을 쉽게 구현할 수 있으며 parallelStream으로 바꾸는 것만으로도 노력없이 병렬성을 얻을 수 있다
- 다만 여러 스트림 연산은 각각 다양한 연산을 수행하므로 내부적인 상태 고려 필요
- map,filter는 0 또는 결과를 출력 스트림으로 보내는 stateless 연산이다
- reduce, sum, max등은 결과를 누적할 내부 상태가 필요하다
- 위의 예제에서 내부 상태는 작은 값으로 int나 double을 사용한다
- 스트림에서 처리하는 요소 수와 상관없이 내부 상태의 크기는 한정(bound)되어 있다
- sorted나 distinct같은 연산은 filter,map처럼 겉보기에는 스트림을 입력으로 받아 다른 스트림으로 출력하는 것처럼 보여도 내부 요소를 정렬하고, 중복제거를 하기 위해서는 과거의 이력을 알고 있어야 한다
- 이러한 연산은 내부 상태를 갖는 연산 즉 stateful연산으로 간주 할 수 있다
5.5실전 연습
1 | import lombok.Data; |
1 | import lombok.Data; |
1 | Trader raoul; |
1 |
|
- .sorted(Comparator.comparing([Lambdas])) 사용을 주의 깊게
거래자가 근무하는 모든 도시를 중복없이 나열 1
2
3
4
5
6
7
8
9
10
public void test2(){
//스트림을 집합으로 변환하는 toSet()을 이용하여 Set<String>으로 하면 바로 중복을 제거 처리 할 수도 있다
List<String> result2 =
transactions.stream()
.map(t -> t.getTrader().getCity())
.distinct()
.collect(toList());
result2.forEach(System.out::println);
}문3: 케임브리지에서 근무하는 모든 거래자를 찾아서 이름 순으로 정렬 1
2
3
4
5
6
7
8
9
10
public void test3(){
List<String> result3 =
transactions.stream()
.filter(t -> t.getTrader().getCity() =="Cambridge" )
.map(t -> t.getTrader().getName())
.sorted()
.collect(toList());
result3.forEach(System.out::println);
}
1 |
|
- .reduce(“”,(n1, n2) -> n1+n2)는 모든 문자열을 반복적 연결 -> 새문자열 객체
- 효율성이 부족하므로 joining을 이용한다
- joining은 내부적으로 StringBuilder를 이용한다
문5: 밀라노에 거래자가 있는가? 1
2
3
4
5
6
7
public void test5(){
boolean result5 =
transactions.stream()
.anyMatch(t -> t.getTrader().getCity().equals("Milan"));
System.out.println(result5);
} - 나는 getCity()==”milano”로 했었다
- 기초가 부족한 나를 위해 설명하자면 자바에서 “스트링 리터럴은 풀로 관리한다”
- 그래서 “milano”의 스트링이 풀에 들어가 있고 그후 같은 인스턴스를 가르키고 있기에
==의 결과가 true가 나오게 된다 - equals를 쓰자
1 |
|
- 이것도 .filter(t -> t.getTrader().getCity()==”Cambridge”)로 했다
- equals를 쓰자
- .map( t -> t.getValue() )처럼 람다로 적었다. 메서드 레퍼런스를 꼭 생각하자
문7: 전체 트랜잭션 중 최대값 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void test7(){
//my answer
System.out.println(
transactions.stream()
.map(t -> t.getValue())
.reduce(Integer::max)
.orElse(-1)
);
//책내용
System.out.println(
transactions.stream()
.min(comparing(Transaction::getValue));
);
} - 스트림은 최대값,최소값을 계산하는데 사용할 키를 지정하는 Comparator를 인수로 받는 min, max를 제공한다. 이를 이용해서 위처럼 더 쉽게 해결이 가능하다
- 문제8은 최소값이니 역시 같은 원리
5.6 숫자형 스트림
1 | int calories = |
- 위 코드에는 박싱 비용이 숨어 있다
- map의 반환값이 Stream
이기에 기본형 int대신 Integer로 박싱되어 반환된다 - 따라서 개발자가 원하는 것처럼 sum()으로 직접 호출할 수 없다
- 인터페이스에는 sum메소드가 없다 (T에 따라서 sum연산 수행이 불가능)
- 이런 스트림 API 숫자 스트림을 효율적으로 처리할 수 있는 것이 제공된다
5.6.1 기본형 특화 스트림
- primitive stream specialization
- 스트림 API 숫자 스트림을 효율적으로 처리
- 자바 8에서는 3가지 기본형 지원
- IntStream : int 특화
- DoubleStream : double 특화
- LongStream : long 특화
- 각각 인터페이스는 sum, max같이 자주 사용되는 숫자관련 리듀싱 연산 메서드를 제공한다
- 필요할 떄 다시 객체 스트림으로 복원하는 기능도 제공한다
- 특화 스트림은 오직 박싱 과정에서의 효율성과 관련이 있으며 추가 기능 제공은 없다
숫자 스트림으로 매핑
- 스트림을 특화 스트림으로 변환
- mapToInt
- mapToDouble
- mapToLong
- map과 정확히 같은 기능을 수행하지만 Stream
대신 특화 스트림을 반환한다 특화형을 이용한 칼로리합 1
2
3
4
5
6
7
8
9
10
11
12
13int calories = menu.stream()
.mapToInt(Dish::getCalories)
.sum();
```
* 위에서 mapToInt는 모든 칼로리(Integer)추출 후 IntStream을 반환한다
(Stream<Integer>가 아님)
* 따라서 IntStream에서 제공하는 sum메서드를 이용해서 계산이 가능하다
* 스트림이 empty라면 기본값 0을 반환한다
* max,min,average등 다양한 유틸리티 메서드를 지원
##### 객체 스트림으로 복원
```java 특화 스트림을 객체 스트림으로 복원하기
Stream<Integer> stream = intStream.boxed(); - IntStream의 map 연산은 int인수->int반환인 람다(IntUnaryOperator)를 인수로 한다
- 정수가 아닌 다른 값을 반환하고 싶으면?
- 스트림 인터페이스의 일반적 연산을 사용행야하므로 일반 스트림으로 복원해야 한다
1 | ``` |