스트림(Stream)
스트림(Stream)은 '데이터의 흐름’입니다. 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있습니다. 또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있습니다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있습니다.
스트림은 자바8부터 추가된 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자입니다. Iterator와 비슷한 역할을 하지만 람다식으로 요소 처리 코드를 제공하여 코드가 좀 더 간결하게 할 수 있다는 점과 내부 반복자를 사용하므로 병렬처리가 쉽다는 점에서 차이점이 있습니다.
스트림에 대한 내용은 크게 세 가지로 나눌 수 있습니다.
- 생성하기 : 스트림 인스턴스 생성.
- 가공하기 : 필터링(filtering) 및 맵핑(mapping) 등 원하는 결과를 만들어가는 중간 작업(intermediate operations).
- 결과 만들기 : 최종적으로 결과를 만들어내는 작업(terminal operations).
1. 생성하기
배열 스트림
String[] arr = new String[]{"Bumblebee", "b", "c"};
Stream<String> stream = Arrays.stream(arr); // return Bumblebee, b, c
Stream<String> streamOfArrayPart = Arrays.stream(arr, 1, 3); // return b, c
컬렉션 스트림
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> colStream = list.stream(); // return a, b, c
Stream<String> parallelStream = list.parallelStream(); // 병렬 처리 스트림
Stream.builder()
Stream<String> builderStream =
Stream.<String>builder()
.add("kakao").add("naver").add("google")
.build(); // return kakao, naver, google
Stream.generate()
생성되는 스트림은 크기가 정해져있지 않고 무한(infinite)하기 때문에 특정 사이즈로 최대 크기를 제한해야 합니다.
Stream<String> generatedStream =
Stream.generate(() -> "Bumblebee").limit(3); // return Bumblebee, Bumblebee, Bumblebee
Stream.iterate()
초기값 설정 후 해당 값을 람다를 통해서 스트림에 들어갈 요소를 만들 수 있다.
Stream<Integer> iteratedStream = Stream.iterate(10, n -> n + 2).limit(3); // return 10, 12, 14
기본 타입형 스트림
리스트나 배열을 이용해서 기본 타입(int, long, double) 스트림을 생성할 수 있습니다.
IntStream intStream = IntStream.range(1, 5); // return 1,2,3,4
LongStream longStream = LongStream.rangeClosed(1, 5); // return 1,2,3,4,5
Java 8 의 Random 클래스는 난수를 가지고 세 가지 타입의 스트림(IntStream, LongStream, DoubleStream)을 만들어낼 수 있습니다.
DoubleStream doubles = new Random().doubles(3);
병렬 스트림 Parallel Stream
스트림 생성 시 사용하는 stream 대신 parallelStream 메소드를 사용해서 병렬 스트림을 쉽게 생성할 수 있습니다.
내부적으로는 쓰레드를 처리하기 위해 자바 7부터 도입된 Fork/Join framework 를 사용합니다.
각 작업을 쓰레드를 이용해 병렬 처리됩니다.
Stream<Product> parallelStream = productList.parallelStream(); // 병렬 스트림 생성
boolean isParallel = parallelStream.isParallel(); // 병렬 여부 확인
다시 시퀀셜(sequential) 모드로 돌리고 싶다면 다음처럼 sequential 메소드를 사용합니다.
parallelStream.sequential(); // sequential
boolean isParallel = parallelStream .isParallel();
스트림 연결하기
Stream<String> stream1 = Stream.of("kakao", "naver");
Stream<String> stream2 = Stream.of("google");
Stream<String> concat = Stream.concat(stream1, stream2);
concat.forEach(a -> System.out.print(a+", ")); // kakao, naver, google
2. 가공하기
전체 요소 중에서 다음과 같은 API 를 이용해서 내가 원하는 것만 뽑아낼 수 있습니다.
이러한 가공 단계를 중간 작업(intermediate operations)이라고 하는데, 이러한 작업은 스트림을 리턴하기 때문에 여러 작업을 이어 붙여서(chaining) 작성할 수 있습니다.
Filtering
필터(filter)은 스트림 내 요소들을 하나씩 평가해서 걸러내는 작업입니다.
List<String> itCompany = Arrays.asList("kakao", "naver", "google"); // 테스트 dataset
Stream<String> filterStream = itCompany.stream().filter(company -> company.contains("a")); // return kakao, naver
Mapping
맵(map)은 스트림 내 요소들을 하나씩 특정 값으로 변환해줍니다. 이 때 값을 변환하기 위한 람다를 인자로 받습니다.
Stream<String> mapStream = itCompany.stream().map(String::toUpperCase); // return KAKAO, NAVER, GOOGLE
Sorting
List<String> sortStream = itCompany.stream()
.sorted()
.collect(Collectors.toList()); //return google, kakao, naver,
List<String> sortReverseStream = itCompany.stream()
.sorted(Comparator.reverseOrder()) //역순
.collect(Collectors.toList()); // return naver, kakao, google
Comparator 의 compare 메소드는 두 인자를 비교해서 값을 리턴합니다.
List<String> compareSortStream = itCompany.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList()); // return kakao, naver, google
List<String> collect = itCompany.stream()
.sorted((s1, s2) -> s2.length() - s1.length())
.collect(Collectors.toList()); // reutrn google, kakao, naver,
peek
확인해본다는 단어 뜻처럼 특정 결과를 반환하지 않는 함수형 인터페이스 Consumer 를 인자로 받습니다.
IntStream.of(1, 3, 5, 7, 9)
.peek(System.out::println)
.sum(); //return 1, 3, 5, 7, 9
3. 결과 만들기
가공한 스트림을 가지고 내가 사용할 결과값으로 만들어내는 단계입니다. 따라서 스트림을 끝내는 최종 작업(terminal operations)입니다.
Calculating
스트림 API 는 다양한 종료 작업을 제공합니다. 최소, 최대, 합, 평균 등 기본형 타입으로 결과를 만들어낼 수 있습니다.
long count = IntStream.of(1, 2, 3, 4, 5).count(); //return 5
long sum = LongStream.of(1, 2, 3, 4, 5).sum(); //return 15
만약 스트림이 비어 있는 경우 count 와 sum 은 0을 출력하면 됩니다. 하지만 평균, 최소, 최대의 경우에는 표현할 수가 없기 때문에 Optional을 이용해 리턴합니다.
OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min(); //return OptionalInt[1]
OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max(); //return OptionalInt[9]
Reduction
스트림은 reduce라는 메소드를 이용해서 결과를 만들어냅니다.
reduce 메소드는 총 세 가지의 파라미터를 받을 수 있습니다.
- accumulator : 각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직.
- identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴.
- combiner : 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직.
1. accumulator
OptionalInt reduced =
IntStream.range(1, 4) //[1, 2, 3]
.reduce((a, b) -> {
return Integer.sum(a, b);
}); // return OptionalInt[6]
2. identity
10은 초기값이고, 스트림 내 값을 더해서 결과는 16이 됩니다.
여기서 람다는 메소드 참조(method reference)를 이용해서 넘길 수 있습니다.
int reducedTwoParams =
IntStream.range(1, 4) // [1, 2, 3]
.reduce(10, Integer::sum); // return 16
3.combiner
Combiner 는 병렬 처리 시 각자 다른 쓰레드에서 실행한 결과를 마지막에 합치는 단계입니다.
따라서 병렬 스트림에서만 동작합니다.
Integer reducedParams = Stream.of(1, 2, 3)
.reduce(10, // identity
Integer::sum, // accumulator
(a, b) -> {
System.out.println("combiner was called"); // 병렬 스트림이 아니어서 호출 안됨
return a + b;
}); // return 16
결과는 다음과 같이 36이 나옵니다. 먼저 accumulator 는 총 세 번 동작합니다. 초기값 10에 각 스트림 값을 더한 세 개의 값(10 + 1 = 11, 10 + 2 = 12, 10 + 3 = 13)을 계산합니다. Combiner 는 identity 와 accumulator 를 가지고 여러 쓰레드에서 나눠 계산한 결과를 합치는 역할입니다. 12 + 13 = 25, 25 + 11 = 36 이렇게 두 번 호출됩니다.
Integer reducedParallel = Arrays.asList(1, 2, 3)
.parallelStream() // 병렬 스트림
.reduce(10,
Integer::sum,
(a, b) -> {
System.out.println("combiner was called"); // 두번 호출 됨
return a + b;
}); // return 36
Collecting
- Product.java
public class Product {
int count;
String name;
public Product(int count, String name) {
this.count = count;
this.name = name;
}
public String getName() {
return this.name;
}
public int getAmount() {
return this.count;
}
}
// 테스트 dataset
List<Product> productList = Arrays.asList(new Product(23, "potatoes"),
new Product(14, "orange"),
new Product(13, "lemon"),
new Product(23, "bread"),
new Product(13, "sugar"));
Collectors.toList()
List<String> collectorCollection = productList.stream()
.map(Product::getName)
.collect(Collectors.toList());
// return potatoes, orange, lemon, bread, sugar
Collectors.joining()
String listToString = productList.stream()
.map(Product::getName)
.collect(Collectors.joining()); // reutrn potatoesorangelemonbreadsugar
- delimiter : 각 요소 중간에 들어가 요소를 구분시켜주는 구분자
- prefix : 결과 맨 앞에 붙는 문자
- suffix : 결과 맨 뒤에 붙는 문자
String listToString2 = productList.stream()
.map(Product::getName)
.collect(Collectors.joining(", ", "<", ">"));
// reutrn <potatoes, orange, lemon, bread, sugar>
Collectors.averageingInt()
Double averageAmount = productList.stream()
.collect(Collectors.averagingInt(Product::getAmount)); // return 17.2
Collectors.summingInt()
Integer summingAmount = productList.stream()
.collect(Collectors.summingInt(Product::getAmount)); // return 86
IntStream 으로 바꿔주는 mapToInt 메소드를 사용해서 좀 더 간단하게 표현할 수 있습니다.
Integer summingAmount = productList.stream()
.mapToInt(Product::getAmount)
.sum(); // return 86
Collectors.summarizingInt()
IntSummaryStatistics statistics =productList.stream()
.collect(Collectors.summarizingInt(Product::getAmount));
//return IntSummaryStatistics{count=5, sum=86, min=13, average=17.200000, max=23}
참고
- Java 스트림 Stream (1) 총정리(https://futurecreator.github.io/2018/08/26/java-8-streams/)
- [Java] 자바 스트림(Stream) 사용법 & 예제(https://coding-factory.tistory.com/574)