1. 필터링
- 프레이케이트로 필터링
→ 프레디케이트(return boolean)를 인수로 받아 일치하는 모든 요소를 포함하는 스트림을 반환
1
2
3
4
5
6
7
|
List<Dish> vegetarianMenu =
Dish.menu.stream()
.filter(Dish::isVegetarian)
.collect(toList());
System.out.println(vegetarianMenu);
// [french fries, rice, season fruit, pizza]
|
- 고유 요소 필터링
→ 스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드 지원
1
2
3
4
5
6
7
|
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(System.out::println);
// 2
// 4
|
2. 스트림 슬라이싱(JAVA 9)
- 프레디케이트를 이용한 슬라이싱
1. takeWhite: 무한 스트림을 포함한 모든 스트림에 프레디케이트를 적용해 스트림 슬라이스
1
2
3
4
5
6
7
|
List<Dish> slicedMenu1
= Dish.specialMenu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
.collect(toList());
System.out.println(slicedMenu1);
// [seasonal fruit, prawns]
|
2. dropWhile: 프레디케이트가 처음 거짓이 되는 지점까지 발견된 요소를 버리고, 남은 모든 요소를 반환
1
2
3
4
5
6
7
|
List<Dish> slicedMenu2
= Dish.specialMenu.stream()
.dropWhile(dish -> dish.getCalories() < 320)
.collect(toList());
System.out.println(slicedMenu2);
// [rice, chicken, french fires]
|
- 스트림 축소
1. limit: 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환
1
2
3
4
5
6
7
8
|
List<Dish> dishes
= Dish.specialMenu.stream()
.filter(dish -> dish.getCalories() > 300)
.limit(2)
.collect(toList());
System.out.println(dishes);
// [rice, chicken]
|
- 요소 건너뛰기
1. skip: 입력 값만큼 요소를 무시
1
2
3
4
5
6
7
8
9
|
List<Dish> skipDishes
= Dish.specialMenu.stream()
.filter(dish -> dish.getCalories() > 100)
.skip(2)
.limit(2)
.collect(toList());
System.out.println(skipDishes);
// [chicken, french fires]
|
3. 매핑
- 스트림 API의 map과 flatMap 메서드는 특정 데이터를 선택하는 기능 제공
- 스트림의 각 요소에 함수 적용하기
→ 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑
→ map은 변환에 가까운 매핑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
List<String> dishNames =
Dish.menu.stream()
.map(Dish::getName)
.collect(toList());
System.out.println(dishNames);
// [pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon]
List<Integer> intDishNames =
Dish.menu.stream()
.map(Dish::getName)
.map(String::length) // map 메서드 연결
.collect(toList());
System.out.println(intDishNames);
// [4, 4, 7, 12, 4, 12, 5, 6, 6]
|
- 스트림 평면화
→ map으로 고유문자 반환 리스트를 만들 때 distinct로 처리 불가능
1
2
3
4
5
6
7
8
9
10
11
12
13
|
List<String> words = new ArrayList<>(Arrays.asList("Hello", "World"));
List<String[]> world =
words.stream()
.map(word -> word.split(""))
.distinct()
.collect(toList());
for(String[] sArr: world) {
System.out.println(Arrays.toString(sArr));
}
// [H, e, l, l, o]
// [W, o, r, l, d]
|
→ flatMap: 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑
하나의 평면화된 스트림을 반환
1
2
3
4
5
6
7
8
9
|
List<String> uniqueCharacters =
words.stream()
.map(word -> word.split(""))
.flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화
.distinct()
.collect(toList());
System.out.println(uniqueCharacters);
// [H, e, l, o, W, r, d]
|
4. 검색과 매칭
- 특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리에도 자주 사용
- allMatch, anyMatch, noneMatch, findFirst, findAny 등 메서드 제공
- 프레디케이트가 적어도 한 요소와 일치하는지 확인
→ anyMatch
1
|
assertTrue(Dish.menu.stream().anyMatch(Dish::isVegetarian));
|
- 프레디케이트가 모든 요소와 일치하는지 검사
→ allMatch
1
|
assertTrue(Dish.menu.stream().allMatch(dish -> dish.getCalories() < 1000));
|
- 프레디케이트가 모든 요소와 일치하지 않는지 검사
→ noneMatch
1
|
assertTrue(Dish.menu.stream().noneMatch(dish -> dish.getCalories() >= 1000));
|
- anyMatch, allMatch, noneMatch, findFirst, findAny 는 스트림 쇼트서킷 기법(자바의 &&, || 연산)
→ 모든 스트림 요소를 처리하지 않고 결과 반환
- 요소검색
1. 임의의 요소 검색
→ findAny
1
2
3
4
5
|
Dish.menu.stream()
.filter(Dish::isVegetarian)
.findAny()
.ifPresent(dish -> System.out.println(dish.getName()));
// french fries
|
2. 첫번째 요소 검색
→ 일부 스트림에는 논리적인 아이템 순서가 정해져 있을 수도 있음
→ findFirst
1
2
3
4
5
6
7
8
|
List<Integer> someNumbers = Arrays.asList(1,2,3,4,5);
Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream()
.map(n -> n * n)
.filter(n -> n % 3 == 0)
.findFirst();
System.out.println(firstSquareDivisibleByThree.orElse(null));
// 9
|
3. findFirst와 findAny는 언제 사용
→ 병렬성: 병렬 실행에서는 첫번째 요소를 찾기 어렵기 때문
→ 요소의 반환 순서가 상관 없다면 병렬 스트림에서는 제약이 적은 findAny 사용
4. Optional
→ 값의 존재나 부재 여부를 표현하는 컨테이너 클래스
→ ifPresent는 Optional 값을 포함하면 참, 아니면 거짓 반환
5. 리듀싱
- 리듀싱 연산이란 모든 스트림 요소를 처리해서 값으로 도출하는 연산
- for-each 루프를 사용해서 값 도출
1
2
3
4
5
6
7
8
|
List<Integer> numbers = Arrays.asList(4, 5, 3, 9);
int sum = 0;
for(int x : numbers) {
sum += x;
}
System.out.println(sum);
// 21
|
→ 리스트에서 숫자가 하나 남을 때까지 reduce 과정 반복
- reduce 사용(2개의 인수)
1
2
3
4
|
int sum2 = numbers.stream().reduce(0, (a,b) -> a+b);
System.out.println(sum2);
// 21
|
→ 초기값 0
→ 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>
- reduce 동작
→ a에 초기값 0이 들어가고, b에는 4이 들어감
→ 누적값 = 4
→ a에 누적값 8이 들어가고, b에는 5가 들어감.
→ 누적값 = 9
→ 반복
→ 메서드 참조로 코드 간결화
1
2
3
4
|
int sum3 = numbers.stream().reduce(0, Integer::sum);
System.out.println(sum3);
// 21
|
- 초깃값이 없는 경우
→ 초기값을 받지 않도록 오버로드된 reduce 존재
→ 그러나 이 reduce는 Optional 객체 반환
→ 스트림에 아무 요소도 없는 경우 reduce 합계가 반환될 수 없어 Optional 사용
1
2
3
4
|
Optional<Integer> sum4 = numbers.stream().reduce(Integer::sum);
System.out.println(sum4.orElse(null));
// 21
|
- 최댓값과 최솟값
→ 최댓값과 최솟값
1
2
3
4
5
6
7
|
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
System.out.println(max.orElse(null));
// 9
System.out.println(min.orElse(null));
// 3
|
- reduce 메서드의 장점과 병렬화
- 스트림 연산: 상태 없음과 상태 있음
- 중간연산과 최종연산
6. 숫자형 스트림
- reduce를 이용한 스트림 요소의 합
1
2
3
4
|
int calories = Dish.menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
// 4300
|
위 과정에서는 내부적으로 합계를 계산하기 위해서 Integer가 int형으로 언박싱, 따라서 박싱 비용이 소모
- 기본형 특화 스트림(primitive stream specialization)
자바 8에서는 세 가지 기본형 특화 스트림을 제공
→ 스트림 API에서 박싱 비용을 피할 수 있는 특화 스트림
1. IntStream: int 요소에 특화
2. DoubleStream: double 요소에 특화
3. LongStream: long 요소에 특화
각각의 인터페이스는 숫자 스트림의 합계를 계산하는 sum, 최댓값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공
- 숫자 스트림으로 매핑
→ 스트림을 특화 스트림으로 변환할 때 mapToInt, mapToDouble, mapToLong 세가지 메서드를 가장 많이 사용
→ map과 같은 기능을 수행하지만, 특화 스트림을 반환
1
2
3
4
|
int calories2 = Dish.menu.stream()
.mapToInt(Dish::getCalories)
.sum();
// 4300
|
- 객체 스트림으로 복원
→ boxed 메서드를 이용해 특화 스트림을 일반 스트림으로 변환
1
2
|
IntStream intStream = Dish.menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
|
- OptionalInt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
OptionalInt calories3 = Dish.menu.stream()
.mapToInt(Dish::getCalories)
.max();
System.out.println(calories3.orElse(0));
// 800
OptionalInt calories4 = Dish.menu.stream()
.mapToInt(Dish::getCalories)
.min();
System.out.println(calories4.orElse(0));
// 120
OptionalDouble calories5 = Dish.menu.stream()
.mapToInt(Dish::getCalories)
.average();
System.out.println(calories5.orElse(0));
// 477.77777777777777
|
- 숫자 범위
자바 8의 IntStream과 LongSteam에서는 range와 rangeClosed라는 두 가지 정적 메서드를 제공
1
2
3
4
5
6
7
|
IntStream evenNumbers = IntStream.rangeClosed(1, 100) .filter(n -> n % 2 == 0);
System.out.println(evenNumbers.count());
// 50
IntStream evenNumbers2 = IntStream.range(1, 100) .filter(n -> n % 2 == 0);
System.out.println(evenNumbers2.count());
// 49
|
→ rangeClose(1, 100)은 1과 100을 포함하며 range(1, 100)은 1과 100을 제외
- 피타고라스의 수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
Stream<int[]> pythagoreanTriples =
IntStream.rangeClosed(1, 10).boxed()
.flatMap(a -> IntStream.rangeClosed(a, 10) // 0.평준화된 스트림으로 변
.filter(b -> Math.sqrt(a * a + b * b) % 1 == 0) // 1.필터링: a, b 두 개의 수만 있을 때 제곱근이 정수인지 확인
//.boxed() // 3.b값 생성: map은 Stream의 각 요소를 int 배열로 변환하기 때문
.mapToObj(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)}) // boxed 메서드 대신 IntStream의 mapToObj 메서드 사용
);
pythagoreanTriples.forEach(t -> System.out.println(t[0] + ", " + t[1] + ", " + t[2]));
Stream<int[]> pythagoreanTriples2 = IntStream.rangeClosed(1, 10).boxed()
.flatMap(a -> IntStream.rangeClosed(a, 10)
.mapToObj(b -> new double[]{a, b, Math.sqrt(a * a + b * b)})
.filter(t -> t[2] % 1 == 0)) // 제곱근을 두번 계산하지 않도록 필터링
.map(array -> Arrays.stream(array).mapToInt(a -> (int) a).toArray());
pythagoreanTriples2.forEach(t -> System.out.println(t[0] + ", " + t[1] + ", " + t[2]));
// 3, 4, 5
// 6, 8, 10
|
cs |
7. 스트림 만들기
- 값으로 스트림 만들기
임의의 수를 인수로 받는 정적 메서드 Stream.of를 이용해여 생성
1
2
3
4
5
6
7
8
|
Stream<String> stream = Stream.of("Mordern", "Java", "In", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
// MORDERN
// JAVA
// IN
// ACTION
Stream<String> emptyStream = Stream.empty(); // 스트림 비우기
|
- null이 될 수 있는 객체로 스트림 만들기
때로는 null이 될 수 있는 객체를 스트림으로 생성(객체가 null = 빈 스트림)
1
2
3
4
5
6
7
8
9
10
|
String homeVal = System.getProperty("home");
Stream<String> homeValStream =
homeVal == null ? Stream.empty() : Stream.of(homeVal);
Stream<String> homeValStream2 =
Stream.ofNullable(System.getProperty("home")); // ofNullable으로 처리
Stream<String> values =
Stream.of("conf", "home", "user").flatMap(key -> Stream.ofNullable(System.getProperty(key)));
// flatMap으로 일괄처리
|
- 배열로 스트림 만들기
Arrays.stream을 이용하여 스트림 생성
1
2
|
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum(); // int[]을 IntStream으로 변환
|
- 파일로 스트림 만들기
파일 처리에 사용하는 NIO API(비블록 I/O)도 스트림 API를 사용 가능
1
2
3
4
5
6
7
8
9
10
11
12
|
String fileName = "/Users/jang/IdeaProjects/halfjang/modern/src/main/resources/data.txt";
long uniqueWord = 0;
try(Stream<String> lines =
// Stream은 자원을 자동으로 해제할 수 있는 AutoCloseable임으로 try-finally가 필요 없음
Files.lines(Paths.get(fileName), Charset.defaultCharset())) {
uniqueWord = lines.flatMap(line -> Arrays.stream(line.split(" "))) // flatMap으로 고유 단어 수 계산
.distinct()
.count();
System.out.println(uniqueWord);
} catch (IOException e) {
System.out.println("ERROR::" + e.getMessage());
}
|
- 함수로 무한 스트림 만들기
스트림 API는 함수에서 스트림을 만들 수 있는 두 정적 메서드 Stream.iterate와 Stream.generate를 제공
두 연산을 이용해서 무한 스트림(언바운드 스트림) 생성 가능
→ 무한 스트림은 limit으로 스트림 크기 제한 필수
1. iterate
1
2
3
4
5
6
7
8
9
10
11
12
13
|
Stream.iterate(0, n -> n + 2) // 초깃값 0부터 무한으로 +2 수행
.limit(10) // 10번만 수행
.forEach(System.out::println);
// 0
// 2
// 4
// 6
// 8
// 10
// 12
// 14
// 16
// 18
|
1.1 피보나치 수열
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
Stream.iterate(new int[]{0, 1}, t-> new int[] {t[1], t[0] + t[1]})
.limit(10)
.map(t -> t[0])
.forEach(System.out::println);
// 0
// 1
// 1
// 2
// 3
// 5
// 8
// 13
// 21
// 34
|
1.2 predicate 지원
1
2
3
4
5
6
7
8
9
10
11
12
13
|
IntStream.iterate(0, n -> n < 10, n -> n + 4)
.forEach(System.out::println);
// filter는 작업 종료 시점을 알지 못해 사용 불가능
// takeWhile로 사용가능, 근데 IntelliJ가 길다고 줄이라고 함
IntStream.iterate(0, n -> n + 4)
.takeWhile(n -> n < 10)
.forEach(System.out::println);
// 0
// 4
// 8
|
2. generate
→ iterate와 달리 생산된 각 값을 연속적으로 계산하지 않음
1
2
3
4
5
6
7
8
|
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
// 0.6580158393225022
// 0.4608897593955207
// 0.9981947118300856
// 0.7621314798469475
// 0.2660159676673133
|
2.1 피보나치 수열
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
|
IntSupplier fib = new IntSupplier() {
private int previous = 0;
private int current = 1;
@Override
public int getAsInt() {
int oldPre = this.previous;
int nextVal = this.previous + this.current;
this.previous = this.current;
this.current = nextVal;
return oldPre;
}
};
IntStream.generate(fib).limit(10).forEach(System.out::println);
// 0
// 1
// 1
// 2
// 3
// 5
// 8
// 13
// 21
// 34
|
사용된 익명 클래스와 람다는 비슷한 연산을 수행하지만,
익명 클래스에서 getAsInt는 메서드의 연산은 상태 필드를 정의할 수 있는 점이 다름
→ 부작용이 발생할 수 있음(가변)
→ 람다는 부작용이 없음(불변)
스트림을 병렬로 처리하려면 불변 상태 기법을 고수
참고: 모던 자바 인 액션