자바/모던 자바 인 액션

[모던 자바 인 액션] 5장. 스트림 활용

Rudtjs 2022. 9. 29. 08:57

스트림에서 쓸 수 있는 다양한 기술들을 알아보겠습니다.

 

5.1 필터링

스트림의 요소를 선택하는 방법, 즉 프레디케이트 필터링 방법과 고유 요소만 필터링을 할 수 있다.

 

5.1.1 프레디케이트로 필터링

  • 프레디케이트(boolean을 반환하는 함수)를 인수로 받아서 일치하는 모든 요소를 포함하는 스트림을 반환한다.
List<Dish> vegtarianMenu = menu.stream()
	.filter(Dish::isVegtarian) // 채식 o x 확인하는 메서드
	.collect(toList());

 

5.1.2 고유 요소 필터링

  • 스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드를 지원한다.
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 , 2, 4 가 출력해야 하지만 distinct를 사용하면 2가 중복되므로 2, 4가 출력된다.

 

5.2 스트림 슬라이싱

스트림의 요소를 선택하거나 스킵, 특정 크기로 줄이는 방법 등 다양한 방법의 새 기능이 생겼다.

 

5.2.1 프레디케이트를 이용한 슬라이싱

자바 9부터는 스트림의 요소를 효과적으로 선택할 수 있도록 takeWhile, dropWhile 메서드를 지원한다.

 

TAKEWHILE 활용

  • TAKEWHILE 메서드는 조건식에 부합되지 않을 때 반복 작업을 중단하게 해주는 기능이다.
  • filter를 사용해도 결과는 똑같이 뽑히지만 데이터가 수천 개가 넘어갈 때는 속도에서 큰 차이가 벌어질 수 있다.
List<Dish> sliceMenu1 =
specialMenu.stream()
      	   .takeWhile(d -> d.getCalories() < 320)
           .collect(toList());

 

DROPWHILE 활용

  • 결과값이 아닌 나머지 요소를 선택할 때 쓰는 메서드가 DropWhile이다.
List<Dish> sliceMenu2
	   = specialMenu.stream()
			.dropWhile(d -> d.getCalories() < 320)
			.collect(toList());

 

5.2.2 스트림 축소

  • 스트림은 주어진 값 이하의 크기를 갖게 해주는 limit 메서드를 지원해준다. 
List<Dish> sliceMenu2
	   = specialMenu.stream()
			.dropWhile(d -> d.getCalories() < 320)
			.limit(3)
			.collect(toList());

 

5.2.3 요소 건너뛰기

  • 스트림은 n개 요소를 제외한 스트림을 반환하는 skip 메서드를 지원한다. 만약 n개 이하의 요소를 포함하는 스트림에 skip을 호출하면 빈 스트림이 반환된다.
List<Dish> sliceMenu2
	   = specialMenu.stream()
			.dropWhile(d -> d.getCalories() < 320)
			.skip(3)
			.collect(toList());

 

 

5.3 매핑

Stream은 특정 객체에서 특정 데이터를 선택하는 기능을 제공해준다.

 

5.3.1 스트림의 각 요소에 함수 적용하기

스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각 요소에 적용되며 새로운 요소로 매핑된다.

List<String> dishNames = menu.stream()
	.map(Dish::getName)
	.collect(toList());

Dish::getName을 map 메서드로 전달해서 스트림의 요리명을 추출할 수 있다.

 

List에 있는 길이들을 알고 싶을 때는?

List<String> words = Arrays.asList("asd", "java", "modern");
List<Integer> wordLengths = words.stream()
	.map(String::length)
	.collect(toList());

map을 이용한 매핑을 할 수가 있다.

 

5.3.2 스트림 평면화

List에서 고유 문자로 이루어진 결과를 반환받고 싶으면 어떤 방식을 사용해야 할까?

 

map, Arrays.stream, flatMap 

우선 Arrays.stream() 메서드를 이용하여 배열 스트림 대신 문자열 스트림으로 만들어주고

String[] arrayOfWords = {"Hello", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);

파이프라인에 Arrays.stream() 메서드와 flatMap을 적용해주면 된다.

List<String> unique =
	words.stream()
		 .map(word -> word.split(""))
		 .flatMap(Arrays::stream)
		 .distinct()
		 .collect(toList());

flatMap()은 각 요소, 스트림에 대해 여러 값을 얻은 다음 값을 단일 출력 스트림으로 병합합니다.

flatMap() 적용 전 : [[H, e, l, l, o], [w, o, r, l, d]]   적용 후 : [H, e, l, l, o, W, o, r, l, d]

 

 

5.4 검색과 매칭

데이터 집합의 특정 속성을 검색하여 데이터를 처리해주는 다양한 유틸리티를 제공해준다.

 

anyMatch

  • 프레디케이트가 주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때 anyMatch를 이용한다.
if (menu.stream().anyMatch(Dish::isVegetarian)) {
	// 채식 요리가 한개라도 있다면 출력
}

allMatch

  • allMatch 메서드는 anyMatch와 달리 모든 요소를 확인한다.

NoneMatch

  • noneMatch는 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다.

findAny

  • findAny 메서드는 현재 스트림에서 순서에 상관없이 임의의 요소를 반환한다.
  • 결과가 없다면 null이 출력되므로 Optional을 함께 사용해준다.

findFirst

  • 스트림에서 첫 번째 요소를 찾고 싶을 때 사용한다.

Optional

  • Optional<T> 클래스는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스이다. 
  • 값이 없을때 처리 해야할 때 사용된다.

 

5.5 리듀싱

반복적이고 복잡한 질의를 표현하기 위해서는 리듀스 연산을 이용한다.

함수형 프로그래밍 언어에서는 종이를 계속해서 접는것과 비슷하다고하여 폴드라고 부른다.

 

reduce는 두 개의 인수를 갖는다.

  • 초깃값 0
  • 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>

 

5.5.1 요소의 합

먼저 for-each 루프를 이용해서 숫자 합을 구하는 코드를 만들어보자.

int sum = 0;
for ( int x : numbers ) {
  sum += x;
}

이 코드를 reduce를 사용해서 바꿔보자

int sum = numbers.stream().reduce(0, (a,b) -> a + b);

메서드 참조를 이용하여 더 간결하게 구현할 수 있다.

int sum = numbers.stream().reduce(0, Integer::sum);

 

reduce 메서드의 장점과 병렬화

단계적 반복으로 합계를 구할때는 sum 변수를 공유해야 하므로 쉽게 병렬화가 어렵다.

하지만 reduce를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게 된다.

물론 병렬로 실행하기 위해서는 연산이 어떤 순서로 실행되더라도 결과가 바뀌지 않는 구조여야 한다.