자바 스트림(Stream)은 자바 8에서 도입된 데이터 처리 기능으로 데이터 컬렉션에 함수형 프로그래밍 스타일을 적용하여 효율적으로 처리할 수 있다.
스트림은 연속된 데이터 요소를 다루며, 중간 연산과 최종 연산을 통해 복잡한 데이터 처리 작업을 간결하게 표현한다.
스트림을 사용하면, 기존의 반복문과 조건문을 사용하는 코드를 간단하게 대체할 수 있으며 또한, 스트림은 병렬 처리를 지원하여 성능 향상에 도움이 된다.
스트림은 내부적으로 Spliterator라는 인터페이스를 사용해 데이터를 처리하는데 데이터 구조에 따라 다양한 방식의 Spliterator 인터페이스 구현체를 제공한다.
이를통해 각 데이터구조에 따라 최적화된 처리가 가능하다.
.stream()
을 만나는 순간 StreamSupport.stream
메서드를 사용해 적절한 스트림을 생성한다.
이때 spliterator()
메서드를 호출하여 각 데이터 구조에 맞는 Spliterator를 얻는다.
각 데이터 구조 클래스(ArrayList, LinkedList, HashSet 등)는 spliterator()
메서드를 오버라이드하여 해당 데이터 구조에 맞는 Spliterator 구현체를 반환하게 된다.
Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
예를들면 다음과 같은 데이터 구조에 맞는 Spliterator 구현체를 제공받는다.
즉, .stream()
메서드를 호출할 때, 각 데이터 구조 클래스가 알고 있는 자신의 구조에 대한 정보를 바탕으로 적절한 Spliterator 구현체를 생성하고 반환한다.
이렇게 생성된 Spliterator는 스트림의 구현에 사용되어 데이터 구조에 따라 최적화된 처리를 수행할 수 있다.
데이터 구조에 대한 세부 사항을 몰라도 스트림을 사용하여 데이터를 처리할수 있는데, 그이유는 스트림이 고수준의 추상화를 제공하고 있기 때문이라고 생각한다.
지연 연산(Lazy Evaluation)은 실제로 값이 필요한 시점까지 연산을 미루는 걸 의미한다.
중간연산은 스트림을 반환하지만, 최종연산이 호출되기 전까지 수행되지 않는다.
스트림은 중간연산을 누적하고, 최종연산이 호출될 때 한꺼번에 처리한다.
이로인해 성능 및 메모리 효율을 향상시킬 수 있다.
반복문과 조건문으로도 유사한 성능을 얻을 수 있지만 스트림을 사용하면 매우 편리하게 구현할 수 있다.
List<Integer> numbers = IntStream.rangeClosed(1, 100)
.boxed()
.collect(Collectors.toList());
// 50보다 큰 첫 번째 짝수를 찾는 로직
numbers.stream()
.filter(number -> number > 50)
.filter(number -> number % 2 == 0)
.findFirst()
.orElse(0);
// 반복문으로
int result = 0;
for (Integer number : numbers) {
if (number > 50 && number % 2 == 0) {
result = number;
break;
}
}
성능향상
필요한 데이터만 처리하여 불필요한 연산을 줄일 수 있다.
예를 들어, filter 연산을 먼저 수행한 후 map 연산을 수행하면 필터링된 요소들에 대해서만 map 연산이 수행되어 연산량이 줄어든다.
메모리 효율성
필요한 시점에만 연산을 처리함으로써, 메모리에 저장되어야 할 중간 결과물이 줄어든다.
예를들어, filter와 map연산을 처리할 경우 지연 연산이 없다면 filter 연산을 수행한 결과를 새로운 컬렉션에 저장한 후 그 결과를 기반으로 map 연산을 수행해야 하는데 이렇게 되면 중간 결과를 저장하기 위한 추가 메모리가 필요하게 된다.
Parallel Stream은 기존의 스트림에 비해 여러 코어에서 동시에 실행될 수 있는 스트림이다. Parallel Stream은 내부적으로 ForkJoinPool을 사용하여 작업을 여러 스레드로 분할한다.
Parallel Stream은 내부적으로 ForkJoinPool을 사용하여 작업을 여러 스레드로 분할한다.
// 커스텀 할 별도의 ForkJoinPool을 생성
ForkJoinPool customForkJoinPool = new ForkJoinPool(4);
// 별도의 ForkJoinPool에서 병렬 스트림을 실행
customForkJoinPool.submit(() -> {
numbers.parallelStream().reduce(0, Integer::sum);
}).join();
customForkJoinPool.shutdown();
paraller() 을 만나는 순간
IntStream.rangeClosed(1, 100)
.parallelStream()
.filter(i -> i % 2 == 0)
.boxed()
.collect(Collectors.toList());
trade-off
사용전략
추상화
가독성
성능 최적화
병렬처리
스트림은 객체지향 프로그래밍의 여러 원칙과 개념을 잘 활용하고 있다.
SRP - 단일 책임 원칙
List<String> names = Arrays.asList("tom","john","eille"...);
List<String> filterNames = names.stream()
.filter(name -> name.length() >= 5)
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
Stream 인터페이스에 정의된 메서드들은 하나의 책임만을 가지고 있다.
Stream<T> filter(Predicate<? super T> predicate);
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
Stream<T> sorted(Comparator<? super T> comparator);
ISP - 인터페이스 분리 원칙
Stream API 는 여러 작은 인터페이스로 구성되어 있다.
public interface Stream<T> extends BaseStream<T, Stream<T>> {}
public interface IntStream extends BaseStream<Integer, IntStream> {}
public interface DoubleStream extends BaseStream<Double, DoubleStream> {}
public interface LongStream extends BaseStream<Long, LongStream> {}
DIP-의존 역전 원칙
ArrayList<Integer> arrayList = new ArrayList<>(Arrays.asList(8, 2, 11, 21, 14, 7, 19));
LinkedList<Integer> linkedList = new LinkedList<>(Arrays.asList(8, 2, 11, 21, 14, 7, 19));
int arrayListSum = arrayList.stream()
.filter(i -> i > 10)
.mapToInt(i -> i * i)
.reduce(0, Integer::sum);
int linkedListSum = linkedList.stream()
.filter(i -> i > 10)
.mapToInt(i -> i * i)
.reduce(0, Integer::sum);
Stream은 고수준의 추상화를 제공하기 때문에 우리는 세부 구현사항을 몰라도 동일한 방법으로 편하게 사용할 수 있다.
ArrayListSpliterator<E> implements Spliterator<E> {}
구현체 반환LLSpliterator<E> implements Spliterator<E> {}
구현체 반환이러한 점이 우리가 코드를 작성할 때, 어떤 자료구조에서든 .stream() 메서드 하나로 편리하게 사용할 수 있는 이유이다.
필터링, 맵핑, 정렬등의 작업을 수행
filter(Predicate<T>)
: 조건에 맞는 요소만 걸러내는 작업을 수행map(Function<T, R>)
: 각 요소를 다른 형태의 요소로 변환하는 작업을 수행 flatMap(Function<T, Stream<R>>)
: 각 요소를 여러 개의 요소로 변환한 후, 하나의 스트림으로 합침distinct()
: 중복을 제거한 요소만 포함sorted()
: 스트림의 요소를 정렬, 기본적으로 요소의 자연 순서에 따라 정렬되며, Comparator를 인자로 전달하여 사용자 지정 정렬을 할 수도 있다.peek(Consumer<T>)
: 스트림의 요소를 소비하지 않고, 특정 작업을 수행할 수 있다.결과를 반환하거나 특정 동작을 수행
forEach(Consumer<T>)
: 스트림의 모든 요소에 대해 작업을 수행toArray()
: 스트림의 요소를 배열로 반환reduce()
: 스트림의 요소를 줄여 하나의 결과 값을 만듬collect()
: 스트림의 결과를 수집하여 다양한 형태로 반환, 주로 리스트, 셋, 맵 등의 컬렉션을 반환할 때 사용min(Comparator<T>)
: 스트림의 요소 중 최소값을 찾는다. Comparator를 인자로 전달하여 사용자 지정 기준으로 최소값을 찾을 수 있다.max(Comparator<T>)
: 스트림의 요소 중 최대값을 찾는다. Comparator를 인자로 전달하여 사용자 지정 기준으로 최대값을 찾을 수 있다.count()
: 스트림의 요소 개수를 반환anyMatch(Predicate<T>)
: 스트림의 요소 중 하나라도 주어진 조건에 만족하면 true를 반환allMatch(Predicate<T>)
: 스트림의 모든 요소가 주어진 조건에 만족하면 true를 반환noneMatch(Predicate<T>)
: 스트림의 모든 요소가 주어진 조건에 만족하지 않으면 true를 반환findFirst()
: 스트림의 첫 번째 요소를 반환findAny()
: 스트림의 임의의 요소를 반환