다양한 데이터 소스를 표준화된 방법으로 다루기 위한 것.
즉, 여러 개의 컬렉션 프레임워크마다 정렬하는 메서드와 같이 연산 방법, 메서드 호출이 모두 제각각이기 때문에 이를 하나로 통일해서 모두 공통으로 사용하고자 하는 것이다.
정의에서 다양한 데이터 소스는 자바에서 제공하는 수많은 컬렉션 프레임워크를 의미하고, 표준화된 방법은 하나의 통일된 방법이라고 생각하면 편할 것 같다.
그렇다면 스트림은 어떠한 특징이 있고, 어떤 기능을 제공할까?
스트림은 데이터 소스로부터 데이터를 읽기만할 뿐 변경하지는 않는다.
원본 데이터 소스에서의 데이터를 직접 조작하는 것이 아닌, 데이터를 읽어와서 사용자가 지정한 새로운 변수에 저장한다.
스트림은 Iterator처럼 일회용이다.
스트림이 최종 연산을 거치는 것은 스트림 내의 요소들을 소모하는 연산을 진행한 것이기 때문에 최종 연산을
진행한 스트림은 다시 사용할 수 없습니다.
strStream.forEach(System.out::println);
int numOfStr = strStream.count(); // 스트림이 이미 닫혀서 에러 발생!
IntStream intStream = new Random().ints(1, 46); // 1~45 범위 난수 생성 무한 스트림
intStream.distinct().limit(6).sorted() // 중간 연산 - 바로 실행되지 않는다.
.forEach(i -> System.out.print(i + " "));
위 코드에서 distinct()는 중복을 제거하는 메서드인데, 무한 스트림에서 중복을 제거하는 것은 불가능하죠. Java의 스트림은 여기서 지연된 연산을 통해 이 문제를 해결한다.
forEach() 메서드는 내부적으로 다음과 같이 선언되어 있기 때문이다.void forEach(Consumer<? super T> action){
Objects.requireNonNull(action);
for(T t : src)
action.accept(T);
}
스트림의 작업을 병렬로 처리한다.
많은 데이터를 처리할 때 parallel() 병렬로도 처리를 할 수 있어 성능을 개선할 수 있다.
기본형 스트림을 제공한다.
IntStream, LongStream, DoubleStream과 같이 기본형 스트림을 제공하여 오토박싱 & 언박싱(기본형 <-> 참조형 변수 형변환 연산)의 비효율을 없앨 수 있다.
또, 숫자 기반의 스트림인 것을 컴파일러가 인식하기 때문에 일반 스트림과 달리 숫자와 관련된 유용한 메서드를 많이 제공한다.
Collection 인터페이스의 stream()으로 컬렉션을 스트림으로 변환할 수 있다.
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> intStream = list.stream();
//배열은 다음과 같이!
Stream<T> Stream.of(T... values)
Stream<T> Stream.of(T[])
Stream<T> Arrays.stream(T[])
IntStream Arrays.stream(int[])
Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)
스트림 내에서 (여기서는 기본형 스트림) 다음과 같이 다양한 연산 메서드도 제공해준다.
int[] arr = {2, 5, 3, 1, 4};
IntStream intStream = Arrays.stream(arr);
// System.out.print("count = " + intStream.count()); // 5
System.out.print("average = " + intStream.average()); // OptionalDouble[3.0], Stream<Integer>는 average() 없음
무한 스트림을 이용할 때에는 반드시 limit()과 같이 무한한 연산에 끝을 지정해야 한다.
IntStream intStream = new Random().ints();
intStream.limit(5).forEach(System.out::println);
iterate(), generate() 람다식을 이용하여 무한 스트림을 만들 수도 있다.
iterate()은 초기값을 기준으로 UnaryOperator가 연산을 시작하는 람다식을 수행하기 때문에, 초기값(입력값)을 줘야한다. 반대로, generate()은 Supplier가 연산 결과값을 주기만 하기 때문에 입력값은 필요없다.
Stream<Integer> evenStream = Stream.iterate(0, n -> n + 2); // 무한 스트림
evenStream.limit(10).forEach(System.out::println);
Stream<Double> randomStream = Stream.generate(Math::random); // 무한 스트림
randomStream.limit(10).forEach(System.out::println);
파일 안의 텍스트들을 추출하여 문자열 스트림으로 생성하는 작업과 같이 파일 스트림도 만들 수 있다. 또는 비어있는 스트림도 가능하다.
Stream<Path> Files.list(Path dir) // Path는 파일 또는 디렉토리
Stream<String> Files.lines(Path path) // 파일 내용을 문자열로 추출
Stream<String> Files.lines(Path path, Charset cs)
Stream<String> lines() // BufferedReader 클래스의 메서드
Stream emptyStream = Stream.empty();
System.out.print(emptyStream.count()); // 0
스트림에서는 중간 연산과 최종 연산으로 나뉜다.
String[] strArr = { "dd", "aaa", "CC", "cc", "b" };
Stream<String> stream = Stream.of(strArr); // 문자열 배열이 소스인 스트림
Stream<String> filteredStream = stream.filter(); // 걸러내기(중간 연산)
Stream<String> distinctedStream = stream.distinct(); // 중복 제거(중간 연산)
Stream<String> sortedStream = stream.sort(); // 정렬(중간 연산)
Stream<String> limitedStream = stream.limit(5); // 스트림 자르기(중간 연산)
int total = stream.count(); // 요소 개수 세기(최종 연산), 스트림 또 못 씀!!!
스트림이 제공해주는 연산은 다음과 같다.
| 중간 연산 | 설명 |
|---|---|
Stream<T> distinct() | 중복을 제거 |
Stream<T> filter(Predicate<T> predicate) | 조건에 안 맞는 요소 제외 |
Stream<T> limit(long maxSize) | 스트림의 일부를 잘라낸다. |
Stream<T> skip(long n) | 스트림의 일부를 건너뛴다. |
Stream<T> peek(Consumer<T> action) | 스트림의 요소에 작업 수행 |
Stream<T> sorted() | 스트림의 요소를 정렬한다. |
Stream<T> sorted(Comparator<T> comparator) | 스트림의 요소를 정렬한다. |
Stream<R> map(Function<T, R> mapper) | 스트림의 요소를 변환한다. |
DoubleStream mapToDouble(ToDoubleFunction<T> mapper) | 스트림 요소를 double로 변환 |
IntStream mapToInt(ToIntFunction<T> mapper) | 스트림 요소를 int로 변환 |
LongStream mapToLong(ToLongFunction<T> mapper) | 스트림 요소를 long로 변환 |
Stream<R> flatMap(Function<T, Stream<R>> mapper) | 스트림의 요소를 변환하고 평탄화 |
DoubleStream flatMapToDouble(Function<T, DoubleStream> mapper) | 요소를 변환하고 double 스트림으로 평탄화 |
IntStream flatMapToInt(Function<T, IntStream> mapper) | 요소를 변환하고 int 스트림으로 평탄화 |
LongStream flatMapToLong(Function<T, LongStream> mapper) | 요소를 변환하고 long 스트림으로 평탄화 |
| 최종 연산 | 설명 |
|---|---|
void forEach(Consumer<? super T> action) | 각 요소에 지정된 작업 수행 |
void forEachOrdered(Consumer<? super T> action) | 순서 유지, 병렬 스트림 |
long count() | 스트림의 요소 수 반환 |
Optional<T> max(Comparator<? super T> comparator) | 스트림의 최대값 반환 |
Optional<T> min(Comparator<? super T> comparator) | 스트림의 최소값 반환 |
Optional<T> findAny() | 스트림의 요소 하나를 반환 |
Optional<T> findFirst() | 첫 번째 요소 반환 |
boolean allMatch(Predicate<T> p) | 모든 요소가 조건을 만족하는지 검사 |
boolean anyMatch(Predicate<T> p) | 하나라도 조건을 만족하는지 검사 |
boolean noneMatch(Predicate<T> p) | 모든 요소가 조건을 만족하지 않는지 검사 |
Object[] toArray() | 스트림의 모든 요소를 배열로 반환 |
<A> A[] toArray(IntFunction<A[]> generator) | 제공된 배열 생성기로 요소를 배열로 반환 |
Optional<T> reduce(BinaryOperator<T> accumulator) | 스트림의 요소를 하나씩 줄여가며(리듀스) 계산 |
T reduce(T identity, BinaryOperator<T> accumulator) | 초기값과 함께 스트림 요소를 줄여가며 계산 |
<U> U reduce(U identity, BiFunction<U,T,U> accumulator, BinaryOperator<U> combiner) | 병렬 계산 시 결과 병합에 사용 |
<R, A> R collect(Collector<? super T, A, R> collector) | 스트림의 요소를 수집하여 컬렉션에 담거나 특정 형태로 변환 |
<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner) | 스트림 요소를 축적하며 분할하고 결과를 컬렉션에 담아 반환 |
원래 이런 Java의 "기초" 같은 느낌의 내용은 블로그에서 안 다루려고 했다. 그런데, 결국 내가 이런 내용을 구글링 하다보니 그냥 내꺼에 정리 한 번 해야겠다는 생각으로 정리해봤다.
Stream은 알고리즘 문제를 풀면서 다른 사람의 풀이를 통해 처음 접해봤는데, 몇 개의 풀이를 계속 보다보니 다양한 컬렉션 프레임워크를 모두 같은 형태의 연산으로 결과를 도출할 수 있다는 점에서 메리트가 있는 것 같았다. 편리한 메서드들도 미리 정의되어 있어 코딩 속도 면에서도, 가독성 면에서도 좋은 것 같다.
예전에 Javascript 공부할 때 자주 이용했던 reduce(), filter(), map() 이랑 같은 역할이라 생각해도 될 듯 하다.
참고 및 출처
Java의 정석
남궁석의 정석코딩 유튜브 <[자바의 정석 - 기초편] ch14-17~22 스트림만들기> https://www.youtube.com/watch?v=AOw4cCVUJC4
남궁석의 정석코딩 유튜브 <[자바의 정석 - 기초편] ch14-23~25 스트림의 연산> https://www.youtube.com/watch?v=iY8ta9upajE