Stream이란
- Java 8 이전에는 배열, 컬렉션을 다루기 위하여 반복문(for, for-each)를 사용하였다.
- Java 8 이후에는 stream (데이터의 흐름)을 통하여 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합하여 원하는 결과를 얻을 수 있다.
- 람다를 사용하여 코드의 양을 줄이고 간결하게 표현할 수 있다.
- 병렬처리가 가능하다. 하나의 작업을 둘 이상의 작업으로 나누어 동시에 진행하는 것을 병렬처리라 한다.
- 구성
- 생성 -> 가공 -> 결과 순으로 처리할 수 있다.
Stream 종류
- 기본 타입형 스트림
- Stream은 제네릭을 사용하지만, 기본 타입 스트림을 생성하여 제공하고 있다.
- int, long, double이 있다.
- 사용예시
- range, rangeClosed의 차이는 범위
IntStream intStream = IntStream.range(1, 5); // 1, 2, 3, 4 LongStream longStream = LongStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5
- 박싱 하기 - 제네릭을 사용하지 않기 떄문에 오토박싱이 이뤄지지 않는다. 필요한 경우 boxed() 메서드 사용
Stream<Integer> stream = IntStream.range(1, 5).boxed();
- range, rangeClosed의 차이는 범위
- 문자열 스트림
- 문자열을 character로 하여 ASCII 코드 값으로 저장할 수 있다.
IntStream charStream = "STREAM".chars();
- 문자열을 character로 하여 ASCII 코드 값으로 저장할 수 있다.
- 파일 스트림
- 자바 NIO 의 Files 클래스의 lines 메소드는 해당 파일의 각 라인을 스트링 타입의 스트림으로 만들어준다.
Stream<String> lineStream = Files.lines(Paths.get("file.txt"), StandardCharsets.UTF_8);
- 자바 NIO 의 Files 클래스의 lines 메소드는 해당 파일의 각 라인을 스트링 타입의 스트림으로 만들어준다.
병렬 스트림(Parallel Stream)
- parallel stream을 활용하면 병렬 스트림을 쉽게 생성할 수 있다.
- 내부적으로 Java 7 부터 도입된 Fork / Join Framework를 사용할 수 있다.
생성
- 배열 스트림
- 이미 생성된 배열을 stream으로 변경하여 사용
String[] arr = new String[]{ "a", "b", "c"}; Stream<String> stream = Arrays.stream(arr); Stream<String> streamOfArrayPart = Arrays.stream(arr, 1, 3); // {"b", "c"}
- 이미 생성된 배열을 stream으로 변경하여 사용
- 컬렉션 스트림
- 컬렉션 타입(Collection, List, Set) 의 경우 인터페이스에 추가된 디폴트 메소드 stream을 활용
List<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream = list.stream(); Stream<String> parallelStream = list.parallelStream(); // 병렬처리 스트림
- 컬렉션 타입(Collection, List, Set) 의 경우 인터페이스에 추가된 디폴트 메소드 stream을 활용
- Stream.builder()
- 빌더패턴을 활용하여 stream을 생성할 수 있다.
Stream<String> builderStream = Stream.<String>builder() .add("a") .add("b") .add("c").build();
- 빌더패턴을 활용하여 stream을 생성할 수 있다.
- Stream.generate()
- generate 메소드를 이용하여 Supplier
에 해당하는 람다로 값을 넣을 수 있다. - Supplier
는 인자는 없고 T를 리턴하는 @FunctionalInterface - limit을 통해 특정 사이즈를 제한해야 한다.
Stream<String> generateStream = Stream.generate(()-> "a").limit(5);
- generate 메소드를 이용하여 Supplier
- Stream.iterate()
- 초기 값과 해당 값을 다루는 람다를 이용하여 스트림에 들어갈 요소를 만든다.
- limit을 통해 특정 사이즈를 제한해야 한다.
Stream<Integer> iteratedStream = Stream.iterate(10, (data) -> data + 5).limit(3);
- 스트림 연결하기
Stream<Integer> stream1 = Stream.<Integer>builder().add(1).add(2).build(); Stream<Integer> stream2 = Stream.iterate(3, (data) -> data + 1).limit(3); Stream<Integer> concatedStream = Stream.concat(stream1, stream2);
가공
메소드명 | 특징 |
---|---|
filter | 함수형 인터페이스인 Predicate를 인자로 받으며 조건에 맞는 데이터만 stream한다. |
map | 함수형 인터페이스인 Function을 인자로 받으며 데이터를 가공할 수 있다. |
flatMap | 함수형 인터페이스인 Function을 인자로 받으며 데이터를 가공할 수 있다. map과의 차이는 원소를 최소한으로 나누어 가공한다는 점이다. |
sorted | stream을 정렬한다. Comparator를 인자로 받아 정렬 조건을 줄 수 있다. |
peek | 함수형 인터페이스인 Consumer를 인자로 받으며 stream 중간에 확인할 때 사용된다. |
- 전체 요소 중에서 중간단계 API를 이용하여 원하는 것만 뽑아낼 수 있다.
-
stream을 리턴하기 떄문에 method chaining이 가능하다
- Filtering
Stream<T> filter(Predicate<? super T> predicate);
- 스트림 내 요소들을 하나씩 평가하여 걸러내는 작업
- 인자로 받는 Predicate는 boolean을 리턴하는 함수형 인터페이스로 평가식이 들어가게 된다.
- 예제
List<String> before = Arrays.asList("Mary", "Kevin", "Test"); long aNameCount = before.stream().filter((data) -> data.contains("a")).count(); // 1
- Map
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
- 스트림 내 요소들을 하나씩 특정 값으로 매핑시킨 후 매핑시킨 값을 다시 스트림으로 변환하는 중간 연산을 담당한다
- 이 때 변환하기 위한 람다를 인자로 받는다.
- 예제
List<String> before = Arrays.asList("Mary", "Kevin", "Test"); List<String> after = before.stream().map((data) -> data.toUpperCase()).collect(Collectors.toList());
- flatMap
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
- Array나 Object로 감싸져 있는 모든 원소를 단일 원소 스트림으로 반환한다.
- 예제
String[][] namesArray = new String[][]{ {"kim", "taeng"}, {"mad", "play"}, {"kim", "mad"}, {"taeng", "play"}}; Arrays.stream(namesArray) .flatMap(innerArray -> Arrays.stream(innerArray)) .filter(data -> data.length() > 3) .collect(Collectors.toSet()).forEach(System.out::println); // play, taeng
- map과 flatMap 차이
- map은 입력한 원소를 그대로 스트림으로 반환
- flatMap은 입력한 원소를 가장 작은 단위의 단일 스트림으로 반환
- 예제
String[][] namesArray = new String[][]{ {"kim", "taeng"}, {"mad", "play"}, {"kim", "mad"}, {"taeng", "play"}}; Arrays.stream(namesArray) .flatMap(innerArray -> Arrays.stream(innerArray)) .filter(data -> data.length() > 3) .collect(Collectors.toSet()).forEach(System.out::println);; // play, taeng Arrays.stream(namesArray) .map(innerArray -> Arrays.stream(innerArray).filter(data -> data.length() > 3).collect(Collectors.toSet())) .collect(HashSet::new , Set::addAll, Set::addAll).forEach(System.out::println); // play, taeng
- 2차원 배열을 stream으로 생성, flatMap을 사용하면 String 단위로 변경이 가능하여 바로 Set을 만들 수 있다.
- map에 경우 stream으로 리턴되기 때문에 안에서 filter를 걸고 set으로 만들었다.
- collect() 메서드
<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);
- supplier는 새로운 결과 컨테이너를 만든다.
- accumulator는 결과에 추가 요소를 통합하기 위한 역할을 한다.
- combiner는 계산 결과를 결합하는 역할을 담당한다.
-
Sorting
List<Integer> list = Arrays.asList(30, 10, 20, 50, 40, 60); list.stream().filter(data -> data > 20) .sorted() .collect(Collectors.toList()).forEach(System.out::print); // 30 40 50 60 list.stream().filter(data -> data > 20) .sorted(Comparator.reverseOrder()) .collect(Collectors.toList()).forEach(System.out::print); // 60 50 40 30
- 정렬의 방법으로 Comparator를 이용한다.
- 인자 없이 그냥 호출할 경우 그냥 오름차순으로 정렬한다.
- 정렬의 방법으로 Comparator를 이용한다.
- peek
- 스트림 내 요소들 각각을 대상으로 특정 연산을 수행하는 메소드
- Consumer를 인자로 받기 때문에 return 객체가 없다.
결과
- 가공한 stream을 가지고 결과값을 만들어내는 단계
-
스트림을 끝내는 최종 작업
- Calculating
- 최소, 최대, 합, 평균 등 기본형 타입으로 결과를 만들어 낼 수 있다.
- 비어있는 stream에 경우 count와 sum은 0을 리턴, 최소 최대 평균의 경우 표현할 수 없기 때문에 Optional을 이용해 리턴
- Reduction
// 1개 (accumulator) Optional<T> reduce(BinaryOperator<T> accumulator); // 2개 (identity) T reduce(T identity, BinaryOperator<T> accumulator); // 3개 (combiner) <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
- stream의 데이터를 변환하지 않고, 더하거나 빼는 등의 연산을 수행하여 하나의 값을 만들 때 reduce를 사용
- accumulator에 파라미터로는 (total, n) 값이 나온다.
- identity: 초기값
- combinar: 병렬처리에서 사용
- 파라미터
- accumulator: 각 요소를 처리하는 계산 로직, 각 요소가 올 때마다 중간 결과를 생성하는 로직
- identity: 계산을 위한 초기값으로 스트림이 비워서 계산할 내용이 없더라도 이 값은 리턴
- combiner: 병렬(parallel) 스트림을 나눠 계산한 결과를 하나로 합치는 동작하는 로직 / 병렬스트림이 아닌경우 실행되지 않는다
- 예제
int reducedParams = Stream.of(1, 2, 3) .reduce(10, Integer::sum, (a, b) -> { System.out.println("combinar was called"); return a + b; }); System.out.println(reducedParams); // 16
- stream의 데이터를 변환하지 않고, 더하거나 빼는 등의 연산을 수행하여 하나의 값을 만들 때 reduce를 사용
- Collecting
- Collectors가 제공하는 객체를 사용
Collectors 메소드 특징 toList() List로 반환 toSet() Set으로 반환 joining() 스트림에서 작업한 결과를 하나의 스트링으로 이어 붙일 수 있다. delimeter, prefix, suffix를 인자로 받는다 averageInt() 숫자 값(Integer value)의 평균(arithmetic mean)을 구할 수 있다. summingInt() 숫자 값의 합을 나타낸다. mapToInt 등을 활용하면 더 쉽게 사용할 수 있다 summarizingInt() 만약 합계와 평균 등이 필요할 경우 한번에 얻을 수 있는 방법. IntSummaryStatistics{count=5, sum=86, min=13, average=17.20000, max23} 형태의 객체를 리턴받는다. groupingBy() 특정 조건으로 요소들을 그룹지을 수 있습니다. 여기서 받는 인자는 함수형 인터페이스 Function 이다. partitioningBy() partitioningBy 은 함수형 인터페이스 Predicate 를 받습니다. Predicate 는 인자를 받아서 boolean 값을 리턴합니다. true, false로 그루핑된다. collectingAndThen() 특정 타입으로 결과를 collect 한 이후에 추가 작업이 필요한 경우에 사용할 수 있다. of() 직접 collector 를 만들어 사용. accumulator 와 combiner 는 reduce 에서 살펴본 내용과 동일합니다. - Matching
- 조건식 람다 Predicate를 받아 해당 조건을 만족하는 요소가 있는지 체크한 결과 리턴
- 세가지 메소드
- anyMatch: 하나라도 조건을 만족하는 요소가 있는지
- allMatch: 모두 조건을 만족하는지
- noneMatch: 모두 조건을 만족하지 않는지
- Iterating
- forEach는 요소를 돌면서 실행하는 최종 작업
- peek와는 중간 작업과 최종 작업의 차이가 있다.
Stream 동작 순서
List<String> list = Arrays.asList("Jpa", "Hello", "Spring", "RxJava");
list.stream().filter((data) -> {
System.out.println("FILTER: " + data);
return data.contains("a");
}).map((data) -> {
System.out.println("MAP: " + data);
return data.toUpperCase();
}).forEach(System.out::println);
// 출력
FILTER: Jpa
MAP: Jpa
JPA
FILTER: Hello
FILTER: Spring
FILTER: RxJava
MAP: RxJava
RXJAVA
-
모든 결과가 첫번째 filter를 수행하고 다음 map으로 넘어가는 구조가 아닌 한 요소가 모든 파이프라인을 거쳐 결과를 만들고 다음 요소로 넘어간다.
-
성능 향상
- 파이프라인을 통해 수직적으로 진행하기 때문에 모든 요소에 map을 하는 것이 아닌, filter를 수행 후 map을 실행한다.
- map의 호출 빈도가 낮춰진다.
- 이와 같은 불필요한 연산을 막아주는 것이 skip, filter, distinct 등이 있다.
스트림 특징
- 스트림 재사용
List<String> toDoList = list.stream() .filter(data -> data.contains("a")) .map(data -> data.toUpperCase()) .collect(Collectors.toList()); Optional<String> firstToDo = toDoList.stream().findFirst(); Optional<String> anyToDo = toDoList.stream().findAny();
- 받은 결과를 필요할 때마다 stream으로 다시 변경하여 선택할 수 있다.
- 반환된 Stream을 받은 인스턴스에 경우 재사용할 수 없다. 여러번 결과를 반환할 때 오류가 발생한다.
- 지연 처리
- stream을 결과를 만드는 작업이 있을 때 작업을 수행한다.
- Null-safe 스트림 생성
- Optional을 이용하여 null에 안전한(Null-safe) 스트림을 생성할 수 있다.
출처
- https://futurecreator.github.io/2018/08/26/java-8-streams/
- https://madplay.github.io/post/difference-between-map-and-flatmap-methods-in-java
- https://kchanguk.tistory.com/56