일상기록
Java - 스트림(stream) 본문
스트림(stream)이란?
데이터를 다룰 때, 컬렉션이나 배열에 데이터를 담고 원하는 결과를 얻기 위해 for문과 Iterator를 이용해서 코드를 작성해왔다. 이러한 방식은 작성된 코드도 길고 재사용성도 떨어진다. 또 다른문제는 데이터 소스마다 다른 방식으로 다뤄야한다는 것이다. Collection이나 Iterator와 같은 인터페이스를 이용해서 컬렉션을 다루는 방식을 표준화하기는 했지만 각 컬렉션 클래스에는 같은 기능의 메소드들이 중복해서 정의되어 있다. 예를 들면 List를 정렬할 때는 Collections.sort()를 사용하고 배열을 정렬할 때는 Arrays.sort()를 사용해야한다.
이러한 문제점을 해결하기 위해 만든 것이 '스트림(stream)'이다. 스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메소드들을 정의해 놓았다. 데이터 소스를 추상화하였다는 것은, 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되었다는 것과 코드의 재사용성이 높아진다는 것을 의미한다.
스트림을 사용하면 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.
String[] strArr = { "aaa", "bbb", "ccc" };
List<String> strList = Arrays.asList(strArr);
// 스트림 생성
Stream<String> strStream1 = strList.stream();
Stream<String> strStream2 = Arrays.stream(srtArr);
// 화면에 출력
strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);
보다 간결하며 재사용성도 높다.
스트림은 데이터 소스를 변경하지 않는다.
스트림은 데이터 소스로 부터 데이터를 읽기만할 뿐, 데이터 소스를 변경하지 않는다는 차이가 있다. 필요하면 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수도있다.
// 정렬된 결과를 List에 담아서 반환한다.
List<String> sortedList = strStream2.sorted().collect(Collectors.toList());
스트림은 일회용인다.
스트림은 Iterator처럼 일회용이다. Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼, 스트림도 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하다면 스트림을 다시 생성해야한다.
strStream1.sorted().forEach(System.out::println);
int numOfStr = strStream1.count(); // 에러 스트림이 이미 닫힘
스트림은 작업을 내부 반복으로 처리한다.
스트림을 이용한 작업이 간결할 수 있는 비결중의 하나가 바로 '내부 반복'이다. 내부 반복이라는 것은 반복문을 메소드의 내부에 숨길 수 있다는 것을 의미한다. forEach()는 스트림에 정의된 메소드중 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다.
for(String str : strList)System.out.println(str); ➔ stream().forEach(System.out::println);
즉 forEach()는 메소드 안으로 for문을 넣은것이다.
void forEach(Consumer<? super T> action) {
Object.requireNonNull(action); // 매개변수의 널 체크
for(T t : src) { // 내부 반복
action.accept(T);
}
}
스트림의 연산
스트림이 제공하는 다양한 연산을 이용해서 복잡한 작업들을 간단히 처리할 수 있다. 마치 데이터베이스에 SELECT문으로 질의(쿼리, query)하는 것과 같다.
스트림이 제공하는 연산은 중간 연산과 최종 연산으로 분류할 수 있는데, 중간 연산은 연산결과를 스트림으로 변환하기 때문에 중간 연산을 연속해서 연결할 수 있다. 반면에 최종 연산은 스트림의 요소를 소모하면서 연산을 수행하므로 단 한번만 연산이 가능하다.
중간 연산 연산결과가 스트림인 연산, 스트림에 연속해서 중간 연산할 수 있음
최종 연산 연산 결과가 스트림이 아닌 연산, 스트림의 요소를 소모하므로 단 한번만 가능
중간 연산은 map(), flatMap()이 핵심이다.
최종 연산은 reduce(), collect()가 핵심이다.
지연된 연산
스트림 연산에서 한 가지 중요한 점은 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다는 것이다. 스트림에 대해 distinct()나 sort()같은 중간 연산을 호출해도 즉각적인 연산이 수행되는 것은 아니라는 것이다. 중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야하는지를 지정해주는 것일 뿐이다. 최종 연산이 수행되어야 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다.
병렬 스트림
스트림으로 데이터를 다룰 때의 장점 중 하나가 바로 병렬 처리가 쉽다는 것이다. fork&join프레임웍으로 작업을 병렬처리하는 것을 공부했는데 병렬 스트림은 내부적으로 이 프레임웍을 이용해서 자동적으로 연산을 병렬로 수행한다. 병렬로 사용할 시 그저 스트림에 parallel()이라는 메소드를 호출해서 병렬로 연산을 수행하도록 지시하면 될 뿐이다. 반대로 병렬로 처리되지 않게 하려면 sequential()을 호출하면 된다. 모든 스트림은 기본적으로 병렬 스트림이 아니므로 sequential()을 호출할 필요가 없다. sequential()메소드는 parallel()을 호출한 것을 취소할 때만 사용한다.
parallel()과 sequential()은 새로운 스트림을 생성하는 것이 아니라 스트림의 속성을 변경하는 것
int sum = strStream.parallel() // strStream을 병렬 스트림으로 전환
.mapToInt(s -> s.length())
.sum();
앞서 병렬처리를 공부했듯이 병렬처리가 항상 더 빠른 결과를 얻게 해주는 것이 아니다.
'Java' 카테고리의 다른 글
Java - 스레드(thread) (1) | 2023.04.20 |
---|---|
Java - 스트림(Stream) 만들기 (0) | 2023.04.19 |
Java - 메소드 참조 (0) | 2023.04.19 |
Java - Function의 합성, Predicate의 결합 (0) | 2023.04.19 |
java.util.function패키지 (0) | 2023.04.18 |