스트림이란? 다양한 데이터소스를 표준화된 방법으로 다루기 위한 것이다.
지금까지는 컬렉션이나 배열에 데이터를 담고 원하는 결과를 얻기 위해 for문과 Iterator를 이용해서 코드를 작성해왔다. 이 방식은 코드도 길어지고 재사용성도 떨어지는 방법이다. 따라서 JDK1.8이상부터는 스트림을 제공하여 정말로 다양한 데이터소스들을 표준화하여 사용할 수 있다.
스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았다. 데이터 소스를 추상화했으므로, 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 됐다.
스트림을 이용하면, 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.
예를 들어, 문자열 배열과 같은 내용의 문자열을 저장하는 List가 있을 때,
String[] strArr = { "aaa", "ddd", "ccc" };
List<String> strList = Arrays.asList(strArr);
이 두 데이터 소스를 기반으로 하는 스트림은 아래와 같이 생성할 수 있다.
Stream<String> strStream1 = Arrays.stream(strArr); // 스트림 생성
Stream<String> strStream2 = strList.stream(); // 스트림 생성
이 두 스트림으로 데이터 소스의 데이터를 읽어서 정렬하고 화면에 출력하는 방법은 다음과 같다. 데이터 소스가 정렬되는 것이 아니라는 것에 유의한다.
strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);
두 스트림의 데이터 소스는 서로 다르지만, 정렬하고 출력하는 방법은 완전히 동일하다.
스트림은 아래와 같은 방식으로 만들어진다.
1. 스트림만들기 2. 중간연산(0~n번) 3. 최종연산(0~1번)
중간 연산 - 연산결과가 스트림 그 자체인 연산, 반복적으로 적용가능.
최종 연산 - 연산결과가 스트림이 아닌 연산, 단 한번만 적용가능(스트림의 요소를 소모)
stream.distinct().limit(5).sorted().forEach(System.out::println);
//스트림.중간연산().중간연산().중간연산().최종연산();
// 중간연산은 몇번이든 들어와도 된다.
// 최종연산은 단 한번만 여기서는 forEach(System.out::println).
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.sorted(); // 정렬(중간 연산)
Stream<String> limitedStream = stream.limit(5); // 스트림 자르기(중간 연산)
int total = stream.count(); // 요소 개수 세기(최종 연산)
1. 스트림은 데이터 소스를 변경하지 않는다.
그리고 스트림은 데이터 소스로 부터 데이터를 읽기만할 뿐, 데이터 소스를 변경하지 않는다는 차이가 있다. 필요하다면, 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수도 있다.
// 정렬된 결과를 새로운 List에 담아서 반환.
List<String> sortedList = strStream2.sorted().collect(Collectors.toList());
2. 스트림은 일회용이다.
스트림은 Iterator처럼 일회용이다. Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼, 스트림도 한번 사용하면 닫혀서 다시 사용할 수가 없다. 필요하다면 Iterator처럼 다시 생성해야 한다.
strStream1.sorted().forEach(System.out::println);
int numOfstr = strStrea1.count(); // 에러. 스트림을 이미 사용해서 닫힘.
3. 스트림은 작업을 내부 반복으로 처리한다.
스트림의 작업이 간결할 수 있는 비결이 '내부 반복'이다. 내부 반복이라는 것은 반복문을 메서드의 내부에 숨겼다는 것을 의미한다. forEach()는 스트림에 정의된 메서드 중의 하나로, 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다.
for(String str : strList)
System.out.println(str)
// ↓
stream.forEach(System.out::println);
즉 forEach()는 메서드 안으로 for문을 넣은 것이다. 수행할 작업은 매개변수로 받는다.
void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action); // 매개변수의 널 체크
for(T t : src) { // 내부 반복
action.accept(T);
}
}
4. 최종 연산 전까지 중간연산이 수행되지 않는다. - 지연된 연산
스트림 연산은 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다. 스트림에 대해 distinct()나 sort()같은 중간 연산을 호출해도 즉각적인 연산이 수행되지 않고 최종 연산시 어떤 중간 연산을 해야하는지 지정하는 것이다. 이렇게 중간 연산을 지정하고 최종 연산을 하면 비로소 중간 연산을 거쳐 최종 연산에서 소모된다.
IntStream intStream = new Random().ints(1, 46); // 1~45범위의 난수 생성 무한 스트림
intStream.distinct().limit(6).sorted() // 중간 연산
.forEach(i -> System.out.print(i + ", ")); // 최종 연산
5. Stream<Integer>와 IntStream
요소의 타입이 T인 스트림은 기본적으로 Stream<T>이지만, 오토박싱 & 언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림, IntStream,LongStream, DoubleStream이 제공된다. 일반적으로 Stream<Integer>대신 IntStream을 사용하는 것이 더 효율적이고, IntStream에는 int타입의 값으로 작업하는데 유용한 메서드들이 포함돼 있다.
6. 병렬 스트림
스트림으로 데이터를 다룰 때 장점 중 하나가 바로 병렬 처리가 쉽다는 것이다. 병렬 스트림은 내부적으로 Java에서 제공하는 fork & join프레임 워크를 이용해서 자동적으로 연산을 병렬로 수행한다. 우리가 할일이라고는 그저 스트림에 parallel()이라는 메서드를 호출해서 병렬 연산을 수행하도록 지시하면 된다. 반대로 병렬로 처리되지 않게 하려면 sequential()을 호출한다. 모든 스트림은 기본적으로 병렬 스트림이 아니기에 이미 parallel()을 호출한 것을 취소할 때만 사용한다.
int sum = strStream.parallel() // strStream을 병렬 스트림으로 전환
.mapToInt(s -> s.length())
.sum();