1. 자바 스트림(Streams)이란?
기존의 배열 또는 컬렉션 인스턴스는 for, foreach 같은 반복문을 통해 하나씩 핸들링하는 방식이었기에 로직이 복잡할수록 코드양도 많아지고 루프를 여러 번 도는 경우도 발생하였다. 그에 비해 자바 8에서 추가된 스트림(Streams)은 람다를 사용할 수 있는 기술 중 하나로 다음과 같은 특징을 가지고 있다.
- 스트림은 데이터의 흐름이라는 뜻으로 컬렉션에 저장되어 있는 요소들을 하나씩 순회하면서 처리할 수 있는 코드패턴이다.
- 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산관 데이터베이스와 비슷한 연산을 지원한다.
- 배열/컬렉션의 함수 여러 개를 조합하여 원하는 결과를 필터링, 가공된 결과 추출 가능하며 람다식으로 코드를 간결하게 표현할 수 있다.
- 하나의 작업을 둘 이상 작업으로 잘게 쪼개 동시에 처리하며 스레드를 이용하여 많은 요소들을 빠르게 처리 가능한다.
- 컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조인데 비해, 스트림은 요청할 때만 요소를 계산하는 고정된 자료 구조이다.
스트림에 사용에 대한 상세한 내용은 다음 3가지로 나눌 수 있다.
- 스트림 생성 - 스트림 인스턴스를 생성한다.
- 스트림 가공 - 필터링/매핑 등 원하는 결과를 가공한다.
- 스트림 결과 생성 - 스트림 결과를 만들어 내는 작업을 한다.
2. 스트림 생성
2-1. 배열 스트림 (Array Streams)
Array.stream() 메서드에 배열의 시작, 끝 인덱스를 인자로 넣으면 배열의 일부만 순회하는 스트림 객체를 생성할 수 있다. (끝 인덱스는 포함되지 않는다.)
String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
Stream<String> streamOfArrayPart = Arrays.stream(arr, 1, 3); // 1~2 요소 [b, c]
2-2. 컬렉션 스트림 (Collection Streams)
컬렉션 인터페이스의 Stream 메서드로 스트림을 생성할 수 있다.
public interface Collection<E> extends Iterable<E> {
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
// ...
}
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Stream<String> parallelStream = list.parallelStream(); // 병렬 처리 스트림
2-3. 비어있는 스트림 (Empty Streams)
요소가 없을 때 null 대신 사용 가능한 비어있는 스트림도 생성이 가능하다.
public Stream<String> streamOf(List<String> list) {
return list == null || list.isEmpty()
? Stream.empty()
: list.stream();
}
2-4. 빌더 (Builder)
배열이나 컬렉션을 통한 생성이 아닌 직접 원하는 값을 넣어 생성할 수도 있다. build 메서드를 통해 스트림을 리턴한다.
Stream<String> builderStream =
Stream.<String>builder()
.add("Eric").add("Elena").add("Java")
.build();
2-5. Generator
Stream.generate()를 사용하면 Supplier <T>에 해당하는 람다로 값을 넣을 수 있다. 생성되는 스트림의 크기는 무한대 이기 때문에, 특정 사이즈로 최대 크기를 제한해야 한다. "Hi"라는 문자열을 5개 만들어 내는 스트림이다. (제한을 5로 걸지 않는다면 무한대로 생성할 것이다.)
Stream<String> generatedStream =
Stream.generate(() -> "Hi").limit(5);
2-5. iterate
Stream.iterate()를 사용하면 초기값과 해당값을 다루는 람다를 사용하여 스트림에 들어간 요소를 만든다. 이 또한 generator와 동일하게 크기가 무한대이기에 특정 사이즈로 제한해야 한다. 초기값 30부터 + 2씩 증가하는 스트림이다.
Stream<Integer> iteratedStream =
Stream.iterate(30, n -> n + 2).limit(5);
2-6. 기본 타입형 Streams
원시타입 (int, long, double) 스트림을 제네릭을 사용하지 않고 직접 다룰 수도 있다. range, rangeClose는 범위의 차이이다. 자바 8의 Randm 클래스는 세 가지 타입의 스트림(IntStream, LongStream, DoubleStream)이 생성 가능하다.
IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]
DoubleStream doubles = new Random().doubles(3); // 난수 3개 생성
2-7. 파일 스트림 (File Streams)
자바 NIO의 Files 클래스는 파일의 각 라인을 스트링타입의 스트림으로 만들어 준다. 다음은 file.txt 파일의 데이터를 utf-8로 인코딩하여 줄 단위로 읽는 스트림이다.
Stream<String> lineStream =
Files.lines(Paths.get("file.txt"),
Charset.forName("UTF-8"));
2-8. 병렬 스트림 (Parallel Streams)
Stream 대신 parallelStream 메서드를 사용하면 병렬 스트림을 바로 생성할 수 있다.
// 병렬 스트림 생성
Stream<Product> parallelStream = productList.parallelStream();
// 배열을 이용한 병렬 스트림 생성
Arrays.stream(arr).parallel();
// 병렬 여부 확인
boolean isParallel = parallelStream.isParallel();
컬렉션과 배열이 아닌 경우는 parallel 메서드를 사용하여 처리가능하다.
IntStream intStream = IntStream.range(1, 150).parallel();
boolean isParallel = intStream.isParallel();
혹은 sequentail 메서드로 다시 되돌릴 수 있다.
IntStream intStream = intStream.sequential();
boolean isParallel = intStream.isParallel();
2-9. 스트림 연결 (Concat)
Concat 메서드를 통해 스트림을 합쳐 새로운 스트림을 생성할 수 있다.
Stream<String> stream1 = Stream.of("Java", "Scala", "Groovy");
Stream<String> stream2 = Stream.of("Python", "Go", "Swift");
Stream<String> concat = Stream.concat(stream1, stream2);
3. 스트림 가공
3-1. Filter
// 인자로 받는 predicate는 boolean형을 리턴하는 평가식이 들어가야한다.
Stream<T> filter(Predicate<? super T> predicate);
스트림 내의 요소를 "필터링"하여 원하는 결과만 걸러내는 작업이다. 조건식에 부합하는 요소만 선별한 스트림을 리턴한다. 다음은 "a"를 포함한 데이터만 뽑아낸 스트림 객체를 리턴하는 예제이다.
Stream<String> stream =
names.stream()
.filter(name -> name.contains("a"));
3-2. Map
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
스트림 내의 요소를 하나씩 특정값으로 변환한다. 변환하기 위한 람다를 인자로 받는다. 스트림 내의 값이 input이 되어 특정 로직을 거친 후 리턴 스트림에 담기게 된다. 다음은 스트림 내의 요소를 대문자로 치환한 스트림을 리턴한다.
Stream<String> stream =
names.stream()
.map(String::toUpperCase);
3-3. flatMap
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
flatMap 메서드의 인자로 받는 mapper는 리턴 타입이 리턴 타입이 Stream이다. 중첩구조를 한 단계 제거하고 단일 컬렉션으로 만들어주는 역할을 한다. 다음은 중첩된 리스트 예제이다.
List<List<String>> list =
Arrays.asList(Arrays.asList("a"),
Arrays.asList("b"));
flatMap으로 한 껍데기를 벗겨내서 조금 플랫 한 리스트로 변경, 중첩 구조를 제거할 수 있다.
List<String> flatList =
list.stream()
.flatMap(Collection::stream) // (e) -> collection.stream(e)의 축약
.collect(Collectors.toList());
혹은 객체에 적용하게 되면 다음과 같다. 다음은 학생 객체를 가진 스트림에서 점수만 뽑아 새로운 스트림을 만들어 평균을 구하는 작업으로 map 만으로는 한 번에 할 수 없는 기능이다.
students.stream()
.flatMapToInt(student ->
IntStream.of(student.getKor(),
student.getEng(),
student.getMath()))
.average().ifPresent(avg ->
System.out.println(Math.round(avg * 10)/10.0));
3-4. sorted
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator); //정렬 시 별도 비교 로직이 있다면
sorted 메서드를 통해 요소를 정렬할 수 있다. 다음과 같이 그냥 호출할 경우 오름차순으로 정렬된다. 정렬할 때 값을 비교하는 별도 로직이 있다면 Comparator를 인자로 넘겨주면 된다.
IntStream.of(14, 11, 20, 39, 23)
.sorted()
.boxed()
.collect(Collectors.toList());
3-5. peek
Stream<T> peek(Consumer<? super T> action);
특정 결과를 반환하지 않고 스트림 내 요소들 각각에 특정 작업을 수행할 뿐이다. 결과에 영향을 주지 않기에 중간중간 결과를 확인할 때 사용 가능하다.
int sum = IntStream.of(1, 3, 5, 7, 9)
.peek(System.out::println)
.sum();
다음과 같은 방식으로 중간 처리과정을 로깅하는 것도 가능하다.
4. 스트림 결과 생성
4-1. 통계값
최소, 최대, 합, 평균 등 기본 형 타입으로 결과를 생성할 수 있다.
long count = IntStream.of(1, 3, 5, 7, 9).count();
long sum = LongStream.of(1, 3, 5, 7, 9).sum();
int max = IntStream.range(1, 10).max();
int min = IntStream.range(1, 10).min();
int avg = IntStream.range(1, 10).average();
스트림이 비어있는 경우 count와 sum은 0을 출력하게 되지만, 최대 최소는 표현할 수 없기에 Optional을 이용해 리턴한다.
OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max();
혹은 스트림에서 ifPresent를 사용해 바로 처리할 수 있다.
DoubleStream.of(1.1, 2.2, 3.3, 4.4, 5.5)
.average()
.ifPresent(System.out::println);
4-2. Reduce
중간 연산을 거친 값들은 reduce 메서드로 결괏값을 생성한다.
reduce는 다음의 3가지 파라미터를 받을 수 있다.
- accumulator - 각 요소를 처리하는 계산 로직
- identity - 계산을 위한 초기값 (스트림이 비어서 계산할 내용이 없어도 이 값은 리턴됨)
- combiner - 병렬 스트림에서 나눠 계산한 결과를 하나로 합치는 로직
// 스트림에서 나오는 값들을 accumulator 함수로 누적
Optional<T> reduce(BinaryOperator<T> accumulator);
// 동일하게 accumulator 함수로 누적하지만 초기값(identity)이 있음
T reduce(T identity, BinaryOperator<T> accumulator);
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
먼저 인자를 1개만(accumulator) 받는 경우를 보면,
OptionalInt reduced =
IntStream.range(1, 4) // [1, 2, 3]
.reduce((a, b) -> {
return Integer.sum(a, b);
});
다음 예제에서는 두 값을 더하는 람다를 넘겨주고 있기에, 배열의 모든 합을 더한 6이 결과가 된다.
다음은 인자가 2개인(accumulator, identity) 경우를 보면
int reducedTwoParams =
IntStream.range(1, 4) // [1, 2, 3]
.reduce(10, Integer::sum); // method reference
최초 값이 10, 스트림 내의 합계 총합계를 더하기에 16을 이 결과가 된다.
마지막으로 인자가 3개인(accumulator, identity, combiner) 경우를 보면
Integer reducedParallel = Arrays.asList(1, 2, 3)
.parallelStream()
.reduce(10,
Integer::sum,
(a, b) -> {
System.out.println("combiner was called");
return a + b;
});
먼저 초기값 10에 각 스트림의 값인 1,2, 3용을 더하여 11,12,13을 생성한다. Combiner는 identity와 accumulator를 가지고 여러 스레드에서 나눠 계산한 결과를 합치기에 11+12+13 = 36을 결과로 반환한다. Combiner는 병렬 처리 시 각 스레드에서 실행한 결과를 마지막에 합치기에 병렬 스트림에서만 작동한다.
4-3. Collect
자바 스트림을 이용하는 가장 많은 패턴 중 하나로써, 요소의 일부를 필터링하고 값을 변형하여 새로운 Collection을 생성한다.
- Collectors.toList() - 작업결과를 담은 리스트를 반환한다.
- Collectors.joining() - 스트림 작업 결과를 하나의 String으로 이어서 반환한다. delimeter, prefix, suffix 등을 사용해 문자열을 조합할 수 있다. 다음은 스트림 요소를 [] 안에 쉼표를 기준으로 연결한 스트링을 반환하는 예제이다.
String listToString =
productList.stream()
.map(Product::getName)
.collect(Collectors.joining(", ", "[", "]"));
- Collectors.averageingInt() - 숫자값의 평균을 반환한다.
Double averageAmount =
productList.stream()
.collect(Collectors.averagingInt(Product::getAmount));
- Collectors.summingInt() - 숫자 값의 합을 반환한다.
- Collectors.summarizingInt() - 합계와 평균에 대한 정보를 한 번에 반환한다. (IntSummaryStatistics 객체에는 개수, 합계, 평균, 최소, 최대에 대한 정보가 담겨있다.)
IntSummaryStatistics statistics =
productList.stream()
.collect(Collectors.summarizingInt(Product::getAmount));
- Collectors.groupingBy() - 특정 조건으로 요소들을 그룹 지을 수 있다. 어떤 요소가 얼마나 많이 분포하고 있는지 Map타입으로 반환한다. 같은 수량이면 리스트로 묶어서 반환한다.
4-4. Foreach
스트림에서 반환된 각각의 값에 대해 작업을 하고 싶을 때 사용한다. 특정 값을 리턴하지는 않는다. 다음은 1~999 중 짝수만 출력하는 예제이다.
Set<Integer> evenNumber = IntStream.range(1, 1000).boxed()
.filter(n -> (n%2 == 0))
.forEach(System.out::println);
4-5. Matching
조건식마다 predicate를 받아서 만족하는 요소가 있는지를 체크한 결과를 리턴한다.
// 하나라도 만족하는 요소가 있는지
boolean anyMatch(Predicate<? super T> predicate);
// 모든 조건을 만족하는지
boolean allMatch(Predicate<? super T> predicate);
// 모든 조건을 만족하지 않는지
boolean noneMatch(Predicate<? super T> predicate);
참고
https://futurecreator.github.io/2018/08/26/java-8-streams/
'Java' 카테고리의 다른 글
[Java] 가상 스레드 (Virtual Threads)란? 자바 21의 가상스레드 (Virtual Thread) 도입 (0) | 2023.10.25 |
---|---|
[Java] Switch와 else-if의 효율성 비교 (Switch와 else-if 중에 어떤 걸 사용해야 할까?) (1) | 2023.10.24 |
[Java] 클래스 로딩 과정(Java Class Loading Process)이란? (0) | 2023.10.23 |