본문 바로가기
자바

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

by __Minnie_ 2025. 4. 24.
필터링

 

1. filter

filter메서드는 프레디케이트를 인자로 받는 메서드로 프레디케이트를 만족하는 요소를 반환한다.

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

 

 

2. distinct

distinct는 중복을 제거할 때 사용된다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 1, 2, 7, 4);
numbsers.filter(i -> i % 2 == 0).distinct().forEach(System.out.printIn);

 

 

스트림 슬라이싱

 

1. takeWhile

takeWhile은 정렬된 요소에서 조건을 만족하는 요소들을 슬라이싱하는 기능이다. 정렬된 요소를 기반으로 동작하기 때문에 320이라는 조건을 만족하지 않는 요소가 발견되는 순간 결과를 반환한다. 요소가 정렬되어 있기 때문에 그 이상은 더 볼 필요가 없기 때문이다. 정렬되지 않은 요소라면 filter를 사용해야겠지만 정렬된 요소라면 takeWhile을 사용하여 성능을 높일 수 있다. 

List<Dish> slicedMenu1 = specialMenu.stream().takeWhile((dish) -> {
            return dish.getCalories() < 320;
        }).collect(Collectors.toList());

 

 

2. dropWhile

dropWhile은 takeWhile과 정반대이다. 주어진 조건을 제외한 나머지 요소들을 반환한다. 

List<Dish> slicedMenu2 = specialMenu.stream().dropWhile((dish) -> {
            return dish.getCalories() < 320;
        }).collect(Collectors.toList());

 

 

스트림 축소

 

1. limit

limit을 사용하여 스트림의 크기를 제한할 수 있다. 아래처럼 사용하면 필터링을 만족하는 요소 3개를 반환한다. 

List<Dish> dishesLimit3 = Dish.menu.stream().filter((d) -> {
            return d.getCalories() > 300;
        }).limit(3).collect(Collectors.toList());

 

 

2. skip

skip은 처음 n개의 요소를 건너뛰고 나머지 요소를 반환한다. 아래처럼 사용하면 필터링을 만족하는 요소중 처음 2개를 제외한 요소들을 반환한다. 

List<Dish> dishesSkip2 = Dish.menu.stream().filter((d) -> {
            return d.getCalories() > 300;
        }).skip(2).collect(Collectors.toList());

 

 

매핑

 

매핑이란 기본적으로 각 요소에 특정 처리를 수행하여 새로운 요소로 매핑하는 것을 의미한다. 변환에 가까운 의미이다. 

 

1. map

map을 사용하면 입력된 요소에 특정 처리를 수행하여 새로운 값으로 반환한다. 예제의 경우 Dish라는 객체를 받아서 메뉴 이름의 스트링으로 반환하는 것을 볼 수 있다.

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

 

 

2. flatMap

flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑하는 기능을 제공한다.

 

예를 들어서 ["Hello", "world"]라는 배열이 있다고 했을 때,  이를 ['H', 'e', 'l', 'o', 'w', 'r', 'd']처럼 분해하여 중복을 제거하려고 해보자. 

Stream var10000 = words.stream()
	.map(word -> word.split(" "))
        .map(Arrays::stream)
        .distinct()
        .collect(toList());

위처럼 map만을 사용하게 되면 [Stream<String>, Stream<String>]의 형태로 생성되어 우리가 원하는 결과가 생성되지 않는다.

 

Stream var10000 = words.stream()
	.map(word -> word.split(" "))
        .flatMap(Arrays::stream)
        .distinct()
        .collect(toList());

위처럼 flatMap을 적용해주어야, Stream<String>이 평면화가 되어서 우리가 원하던 ['H', 'e', 'l', 'o', 'w', 'r', 'd']의 결과가 생성된다. 

 

 

검색과 매칭

 

allMatch, anyMatch, noneMatch, findFirst, findAny 등을 제공한다. 이 메서드들은 모두 프레디케이트를 인자로 받고 프레디케이트 조건에 따라 검색을 수행한다. 

 

anyMatch의 경우 프레디케이드 조건에 적어도 하나의 요소가 일치하는지 확인하여 boolean을 반환한다. allMatch의 경우 모든 요소가 일치하는지 판단한다. noneMatch는 모든 요소가 주어진 프레디케이트와 일치하지 않는 것을 확인한다. 

 

allMatch, anyMatch, noneMatch는 쇼트 서킷을 활용한다. 예를 들어서 allMatch의 경우 프레디케이트를 만족하지 않는 요소가 한개라도 발생하면 바로 false를 반환하는 것을 말한다.

 

findAny는 현재 스트림에서 임의의 요소를 반환하고, findFirst는 현재 스트림에서 첫번재 요소를 반환한다. 이들은 보통 filter와 함께 사용된다. 순서가 중요한 스트림에서는 findFirst를 순서가 중요하지 않은 요소에서는 findAny를 사용할 수 있다. 이 둘은 병렬 처리에서 성능의 차이가 있는데, 병렬 연산에서는 첫번째 요소를 찾기 어렵기 때문에 순서가 중요하지 않은 경우라면 findAny를 사용해서 성능을 최적화할 수 있다. 

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

 

 

리듀싱

 

모든 스트림 요소를 처리해서 하나의 값을 도출하는 연산을 리듀싱이라고 한다.

 

reduce 메서드는 기본적으로 첫번째 라인처럼 초기값, 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T> 인터페이스를 인자로 받는다. 그러나 두번째 라인처럼 초기값을 없앨 수 있다. 초기값을 없애면 반환값이 옵셔널이 된 것을 볼 수 있는데, 이는 리듀싱 연산시 스트림에 값이 없으면 초기값을 반환하는데, 초기값이 선언되어 있지 않으면 반환할 값이 없기 때문이다. 

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

Optional<Integer> sum = (Integer)numbers.stream().reduce((a, b) -> {return a + b;});

 

최댓값과 최솟값은 아래처럼 간단하게 구현할 수 있다. 

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

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

 

 

숫자형 특화 스트림

 

2025.04.24 - [자바] - [모던 자바 인 액션] 스트림이란에서 설명했듯이 int -> Integer (박싱), Integer -> int(언박싱)에는 비용이 든다. 

 

우리가 위에서 봤던 예제들을 보면 내부적으로 reduce에서 값을 계산할 때, 언박싱을 수행해야 한다. 

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

 

이르 언박싱 과정을 줄이기 위해서 우리는 숫자형 스트림을 사용할 수 있다. 기본적으로 IntStream, DoubleStream, LongStream이 있다. 우리는 mapToInt, mapToDouble, mapToLong 메서드를 통해서 숫자형 특화 스트림으로 변환한다.

 

아래처럼 사용하게 되면 getCalories에서 Integer타입의 값들을 추출하여 IntStream으로 변환한다. 

int sum2 = numbers.stream().mapToInt(Dish::getCalories).sum()

 

InteStream을 다시 Stream<Integer>로 변환할 때는 boxed를 사용할 수 있다.

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

 

 

숫자 범위

IntStream, LongStream에서는 rangeClosed, range 메서드를 제공한다. 두 메서드 모두 시작값과 종료값을 인자로 받고, range는 시작값과 종료값을 포함하지 않고 두 값 사이의 값들의 스트림을 생성하고, rangeClosed는 시작값과 종료값을 포함하여 스트림을 생성한다.

IntStream evenStream = IntStream.rangeCloase(0, 100).filter(n -> n % 2 == 0);

 

 

 

스트림 생성

 

1. stream.of

입력된 인자로 구성된 스트림 생성

Stream<String> stream = Stream.of("Modern", "action", "java");

 

 

2. Stream.ofNullable

stream.of는 null값은 포함할 수 없는데, ofNullable을 사용하면 null값이 될 수 있는 값도 스트림으로 생성할 수 있다. 

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

 

3. Array.stream

배열을 인수로 받아서 스트림 생성

int sum - Arrays.stream(numbers).sum();

 

3. iterator

iterator는 무한 스트림을 생성할 수 있다. 이 메서드는 인자로 초기값과 람다를 받아서, 초기값에 인자를 계속 적용해 나가면서 값을 계속 생성한다. 

 

예제처럼 limit과 함께 사용하면 고정된 크기의 스트림을 생성할 수 있다. 

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

 

4. generate

generate는 iterator와 비슷하게 무한 스트림을 생성하지만 Supplier<T>를 받아서 새로운 값을 생성한다. 

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