본문 바로가기
Language/Java

Java - 스트림 활용

by 밍상 2022. 3. 3.

데이터를 어떻게 처리할지는 스트림 API가 관리하므로 편리하게 데이터 관련 작업을 할 수 있다.

 따라서 스트림 API 내부적으로 다양한 최적화가 이루어질 수 있다. 스트림 API는 내부 반복 뿐 아니라 코드를 병렬로 실행할지 여부도 결정할 수 있다.

 

1. 필터링

1.1 프레디케이트로 필터링

스트림 인터페이스는 filter 메서드를 지원한다. filter 메서드는 프레디케이트를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.

List<Dish> vegetarianMenu = 
	menu.stram()
		.filter(Dish::isVegetarian)
        .collect(toList());

1.2 고유 요소 필터링

스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드도 지원한다(고유 여부는 스트림에서 만든 객체의 hashcode, equals로 결정된다).

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.1 프레디케이트를 이용한 슬라이싱

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

 

takeWhile 활용

칼로리 순으로 정렬돼 있는 요리 리스트에서 어떻게 320 칼로리 이하의 요리를 선택할 수 있을까?

filter를 사용할 수 있지만 그렇게 되면 리스트 뒤쪽 부분도 의미 없이 필터링을 진행해야한다.

이때 takeWhile 연산을 이용하면 모든 스트림에 프레디케이트를 적용해 스트림을 슬라이스할 수 있다.

List<Dish> slicedMenu
	= specialMenu.stream()
    			 .takeWhile(dish -> dish.getCalories() < 320)
                 .collect(toList());

dropWhile 활용

나머지 요소를 선택하려면 어떻게 해야 할까? 즉 320칼로리보다 큰 요소는 어떻게 탐색할까?

dropWhile을 이용해 이 작업을 완료할 수 있다.

List<Dish> slicedMenu
	= specialMenu.stream()
    			 .dropWhile(dish -> dish.getCalories() < 320)
                 .colelct(toList());

dropWhile은 takeWhile과 정반대의 작업을 수행한다. dropWhile은 프레디케이트가 처음으로 거짓이 되는 지점까지 발견된 요소를 버린다. 프레디케이트가 거짓이 되면 그 지점에서 작업을 중단하고 남은 모든 요소를 반환한다. dropWhile은 무한한 남은 요소를 가진 무한 스트림에서도 동작한다.

 

2.2 스트림 축소

스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다. 스트림이 정렬되어 있으면 최대 요소 n개를 반환할 수 있다.

	List<Dish> dishes = specialMenu.stream()
    							   .filter(dish -> getCalories() > 300)
                                   .limit(3)
                                   .collect(toList());

2.3 요소 건더뛰기

스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다. n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림이 반환된다.

List<Dish> dishes = menu.stream()
						.filter(d -> d.getCalories() > 300)
                        .skip(2)
                        .collect(toList());

3. 매핑

특정 객체에서 특정 데이터를 선택하는 작업은 데이터 처리 과정에서 자주 수행되는 연산이다. 예를 들어 SQL의 테이블에서 특정 열만 선택할 수 있다. 스트림 API의 map과 flatMap 메서드는 특정 데이터를 선택하는 기능을 제공한다.

 

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

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

다음은 Dish::getName을 map 메서드로 전달해서 스트림의 요리명을 추출하는 코드다.

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

그렇가면 각 요리명이 길이를 알고 싶다면 어떻게 해야 할까?

다음 코드처럼 다른 map 메서드를 연결할 수 있다.

List<Integer> dishNameLengths = menu.stream()
									.map(Dish::getName)
                                    .map(String::length)
                                    .collect(toList())

3.2 스트림 평면화

메서드 map을 이용해서 리스트의 각 단어의 길이를 반환하는 방법을 확인했다. 이를 응용해서 리스트에서 고유 문자로 이루어진 리스트를 반환해보자. 예를 들어 ["Hello", "World"] 리스트가 있다면 결과로 ["H", "e", "l", "o", "W", "r", "d"] 를 포함하는 리스트가 반환되어야 한다.

words.stream()
	 .map(word -> word.split("")) //각 단어를 개별 문자를 포함하는 배열로 반환
     .flatMap(Arrays::stream) //생성된 스트림을 하나의 스트림으로 평면화
     .distinct()
     .collect(toList());

4. 검색과 매칭

특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리도 자주 사용된다. 스트림 API는 allMatch, anyMatch, noneMatch, findFirst, findAny 등 다양한 유틸리티 메서드를 제공한다.

 

4.1 프레디케이트가 적어도 한 요소와 일치하는지 확인

프레디케이트가 주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때 anyMatch 메서드를 이용한다.

 

4.2 프레디케이트가 모든 요소와 일치하는지 검사

allMatch 메서드는 anyMatch와 달리 스트림의 모든 요소가 주어진 프레디 케이트와 일치하는지 검사한다.

noneMatch 메서드는 allMatch와 반대 연산을 수행한다. 즉, noneMatch는 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다. 

 

4.3 요소 검색

findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다. findAny 메서드를 다른 스트림 연산과 연결해서 사용할 수 있다. 예를 들어 다음 코드처럼 filter와 findAny를 사용해서 채식 요리를 선택할 수 있다.

Optional<Dish> dish = 
	menu.stream()
    	.filter(Dish::isVegetarian)
        .findAny();

스트림 파이프라인은 내부적으로 단일 과정으로 실행할 수 있도록 최적화된다. 즉, 쇼트서킷을 이용해서 결과를 찾는 즉시 실행을 종료한다.

 

4.3 첫 번째 요소 찾기

리스트 또는 정렬된 연속 데이터로부터 생성된 스트림처럼 일부 스트림에는 논리적인 아이템 순서가 정해져 있을 수 있다.

이럴 때는 findAny를 사용할 수 있다.

 

※ findFirst와 findAny는 언제 사용하나?

그런데 왜 findFirstfindAny 메서드가 모두 필요할까? 바로 병렬성 때문이다. 병렬 실행에서는 첫 번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.

5. 리듀싱

지금까지 살펴본 최종 연산은 불리언, void, 또는 Optional 객체를 반환했다. 이번에는 리듀스 연산을 사용해서 스트림 요소를 조합해서 더 복잡한 질의를 표현하는 방법을 살펴보자.

 

5.1 요소의 합

reduce를 이용하면 다음과 같이 스트림의 모든 요소를 더할 수 있다.

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

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

  • 초깃값 0
  • 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>. 예제에서는 람다 표현식 (a, b) -> a + b 를 사용했다.

초기값 없음

초기값을 받지 않도록 오버로드된 reduce도 있다. 그러나 이 reduce는 Optional 객체를 반환한다.

스트림에 아무 요소도 없는 상황을 위해 Optional을 반환하게 된다.

Optional<integer> sum = numbers.stream().reduce((a, b) -> (a + b));

5.2 최댓값과 최솟값

최대, 최솟값을 찾을 때도 reduce를 활용할 수 있다.

Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);

7. 숫자형 스트림

7.1 기본형 특화 스트림

자바 8에서는 세 가지 기본형 특화 스트림을 제공한다. 스트림 API는 박싱 비용을 피할 수 있도록 'int 요소에 특화된 IntStream', 'double 요소에 특화된 DoubleStream', 'long 요소에 특화된 LongStream' 을 제공한다. 각각의 인터페이스는 sum, max와 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다. 또한 필요할 때 다시 객체 스트림으로 복원하는 기능도 제공한다.

 

숫자 스트림으로 매핑

스트림을 특화 스트림으로 변환할 때는 mapToInt, mapToDouble, mapToLong 세 가지 메서드를 가장 많이 사용한다. 이들 메서드는 map과 정확히 같은 기능을 수행하지만, Stream<T> 대신 특화된 스트림을 반환한다.

int calories = menu.stream() //Stream<Dish>	반환
				   .mapToInt(Dish::getCalories) //IntStream 반환
                   .sum();

 

객체 스트림으로 복원하기

boxed 메서드를 이용하면 숫자 스트림을 다시 원상태인 특화되지 않은 스트림으로 복원할 수 있다.

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();

7.2 숫자 범위

프로그램에서는 특정 범위의 숫자를 이용해야 하는 상황이 자주 발생한다. 자바 8의 IntStream과 LongStream에서는 range와 rangeClosed라는 두 가지 정적 메서드를 제공한다. 두 메서드 모두 첫 번째 인수로 시작값을, 두 번째 인수로 종료값을갖는다. range 메서드는 시작값과 종료값이 결과에 포함되지 않는 반면 rangeClosed는 시작값과 종료값이 결과에 포함된다는 점이 다르다.

IntStream evenNumbers = IntStream.rangeClose(1, 100)
								 .filter(n -> n % 2 ==0);

7.3 숫자 스트림 활용: 피타고라스 수

Stream<int[]> pythagoreanTriples = 
	IntStream.rangeClosed(1, 100).boxed()
    		 .flatMap(a ->
             		IntStream.rangeClosed(a, 100)
                    	  	 .filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
                             .mapToObj(b ->
                             	new int[]{a, b, (int)Math.sqrt(a*a + b*b)}
                             );

8. 스트림 만들기

stream메서드로 컬렉션에서 스트림을 얻을 수 있었다. 그뿐만 아니라 범위의 숫자에서 스트림을 만드는 방법도 설명했다. 이외에도 다양한 방법으로 스트림을 만들 수 있다.

 

8.1 값으로 스트림 만들기

임의의 수를 인수로 받는 정적 메서드 Stream.of를 이용해서 스트림을 만들 수 있다. 예를 들어 다음 코드는 Stream.of로 문자열 스트림을 만드는 예제다. 스트림의 모든 문자열을 대문자로 변환한 후 문자열을 하나씩 출력한다.

Stream<String> stream = Stream.of("Modern ", "Java ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);

8.2 null이 될 수 있는 객체로 스트림 만들기

자바 9에서는 null이 될 수 있는 객체를 스트림으로 만들 수 있는 새로운 메서드가 추가되었다. 때로는 null이 될 수 있는 객체를 스트림으로 만들어야 할 수 있다. 이럴때 Stream.ofNullable을 이용하면 다음과 같이 코드를 구현할 수 있다.

Stream<String> homeValueStream
	= Stream.ofNullable(System.getProperty("home"));

 

8.3 배열로 스트림 만들기

int[] numbers = {1, 2, 3, 4, 5}
int sum = Arrays.stream(numbers).sum();

 

8.4 파일로 스트림 만들기

파일을 처리하는 등의 I/O 연산에 사용하는 자바의 NIO API도 스트림 API를 활용할 수 있도록 업데이트됐다. java.nio.file.Files의 많은 정적 메서드가 스트림을 반환한다.

 

8.5 함수로 무한 스트림 만들기

스트림 API는 함수에서 스트림을 만들 수 있는 두 정적 메서드 Stream.iterate와 Stream.generate를 제공한다. 두 연산을 이용해서 무한 스트림, 즉 고정된 컬렉션에서 고정된 스트림을 만들었던 것과는 달리 크기가 고정되지 않은 스트림을 만들 수 있다.

iterate와 generate에서 만든 스트림은 요청할 때마다 주어진 함수를 이용해서 값을 만든다. 따라서 무제한으로 값을 계산할 수 있다.

 

iterate 메서드

Stream.iterate(0, n -> n+2)
	  .limit(10)
      .forEach(System.out::println);

iterate 메서드는 초깃값과 람다를 인수로 받아서 새로운 값을 끊임없이 생산할 수 있다. 기본적으로 iterate는 기존 결과에 의존해서 순차적으로 연산을 수행한다. iterate는 요청할 때마다 값을 생산할 수 있으며 끝이 없으므로 무한 스트림을 만든다. 이러한 스트림을 언바운드 스트림이라고 표현한다.

 

generate 메서드

iterate와 비슷하게 generate도 요구할 때 값을 계산하는 무한 스트림을 만들 수 있다. 하지만 iterate와 달리 generate는 생산된 각 값을 연속적으로 계산하지 않는다. generate는 Supplier<T>를 인수로 받아서 새로운 값을 생산한다.

Stream.generate(Math::random)
	  .limit(5)
      .forEach(System.out::println);

 

'Language > Java' 카테고리의 다른 글

Wrapper Class vs Primitive Type  (0) 2022.03.29
Java - 스트림으로 데이터 수집  (0) 2022.03.03
Java - 스트림(stream)  (0) 2022.03.03
Java - 테스트  (0) 2022.01.28
Java - 문자열 2  (0) 2022.01.24