[Java] Stream API 사용하기
Stream API
Java 8에서 소개된 Stream API는 컬렉션, 배열 등의 데이터 소스로부터 데이터를 받아와서 원하는 형태로 가공하거나 필터링할 수 있는 기능을 제공한다.
Stream API를 사용하면 데이터 처리 작업을 병렬로 수행할 수 있어서 멀티스레드 환경에서의 성능을 향상시킬 수 있다.
또한 데이터 처리 과정을 람다식과 함께 사용하여 한 줄로 간결하게 표현할 수 있어 가독성을 높여준다.
https://sjh9708.tistory.com/190
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
Stream API는 스트림을 생성한 후, 중간 연산들과 최종 연산으로 구성된다. 중간 연산은 스트림을 변환하거나 필터링하는 등의 작업을 수행하고, 최종 연산은 결과를 생성하거나 반환하는 작업을 수행한다.
위의 코드에서는 filter()가 중간 연산에 해당하고, collect()가 최종 연산에 해당한다.
Stream 생성하기
컬렉션으로부터 스트림 생성하기
List<String> list = Arrays.asList("apple", "banana", "cherry");
Stream<String> stream = list.stream();
배열로부터 스트림 생성하기
String[] array = {"a", "b", "c"};
Stream<String> stream = Arrays.stream(array);
컬렉션과 배열로부터 stream() 메서드를 호출하여 스트림 객체를 생성할 수 있다.
스트림 객체 직접 생성하기
Stream<String> stream = Stream.of("apple", "banana", "cherry");
Stream의 of()메서드를 사용하여 임의의 요소로부터 스트림 객체를 직접 생성 가능하다.
숫자 범위로부터 스트림 생성하기
IntStream stream1 = IntStream.range(1, 10); // 1부터 9까지 (끝 값은 포함되지 않음)
IntStream stream2 = IntStream.rangeClosed(1, 10); // 1부터 10까지 (끝 값이 포함됨)
range() 메서드를 통해 일련의 숫자 범위에 대한 스트림을 생성 가능하다.
위와 같이 객체를 위한 Stream 외에도 int, double 같은 원시 자료형들을 사용하기 위한 특수한 종류의 Stream(IntStream, LongStream, DoubleStream) 들을 사용할 수 있다.
중간 연산
filter()
조건에 맞는 요소를 걸러내는 연산을 수행한다.
다음은 컬렉션에서 짝수만을 걸러내기 위해 filter를 사용하였다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // 출력: [2, 4, 6]
map()
각 요소를 변환한다.
다음에서는 String List의 각각의 Element들의 길이를 계산한 후 원래 값에서 계산한 새로운 값으로 변환하는 연산을 수행한 것이다.
List<String> words = Arrays.asList("hello", "world", "java");
List<Integer> wordLengths = words.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(wordLengths); // 출력: [5, 5, 4]
메서드 참조 연산
중간에 map에서 :: 연산이 있는 것을 볼 수 있다. 이것은 메서드 참조라는 기능으로서, 람다식을 간결하게 표현하기 위한 방법 중 하나이다. String::length는 String 클래스의 String.length() 메서드를 나타낸다. 위의 코드를 똑같이 적으면 아래와 같다.
List<Integer> wordLengths = words.stream()
.map(x -> x.length())
.collect(Collectors.toList());
sorted()
요소를 정렬한다.
List<Integer> numbers2 = Arrays.asList(3, 1, 4, 2, 5);
List<Integer> sortedNumbers = numbers2.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedNumbers); // 출력: [1, 2, 3, 4, 5]
distinct()
중복을 제거한다.
List<Integer> numbers3 = Arrays.asList(1, 2, 2, 3, 3, 4, 5);
List<Integer> distinctNumbers = numbers3.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(distinctNumbers); // 출력: [1, 2, 3, 4, 5]
peek()
각 요소를 조회하거나 중간 결과를 확인한다. Stream에 영향을 주지 않고, 단지 특정 연산을 수행하여 "확인해 볼 뿐"이다.
numbers.stream()
.filter(n -> n % 2 == 0)
.peek(n -> System.out.println("Even number: " + n))
.collect(Collectors.toList());
flatMap()
각 요소에 대해 매핑 함수를 적용한 후, 매핑된 결과를 하나의 스트림으로 평면화하는 역할을 한다.
중첩된 구조를 가진 스트림에서 내부 스트림을 펼쳐서 단일 스트림으로 만들어주는 역할을 한다.
아래의 코드는 flatMap()이 많이 사용되는 부분 중 하나인 단어 빈도를 세는 코드이다.
살펴보면 우선 map()에 의해서 스트림 요소가 분할되어 중첩된 구조를 가지는 스트림이 되었다.
flatMap()은 각 단어를 개별 요소로 갖는 스트림을 평면화하여 모든 단어를 하나의 스트림으로 만든다. 그런 다음 collect()와 groupingBy()를 사용하여 단어를 그룹화하고 그 빈도를 카운트하여 결과를 Map 형태로 얻어냈다.
List<String> strings = Arrays.asList("Java is a programming language", "Python is also a programming language", "C++ is yet another programming language");
Map<String, Long> wordFrequency = strings.stream()
.map(line -> line.split("\\s+")) // 각 문자열을 단어로 분할
.flatMap(Arrays::stream) // 각 단어를 개별 요소로 갖는 스트림 생성 후 평면화
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); // 각 단어를 그룹화하고 빈도를 카운트
System.out.println("Word frequency: " + wordFrequency);
//Word frequency: {a=2, Java=1, C++=1, another=1, yet=1, also=1, language=3, is=3, programming=3, Python=1}
최종 연산
foreach()
각 요소를 순회하며 연산을 반복 처리할 수 있다.
peek()와 유사하지만, forEach()는 스트림의 요소를 소비하는 최종 연산으로 작용한다. 따라서 스트림의 각 요소를 순회하면서 작업을 수행할 수 있으면서, 실제 요소에 영향을 줄 수도 있으며 반환값이 존재하지 않는다.
//foreach
numbers.stream()
.filter(n -> n % 2 != 0) // 홀수 필터링
.forEach(oddNumbers::add); // 홀수를 리스트에 추가
System.out.println("Odd numbers: " + oddNumbers); // [1, 3, 5]
//peek
List<Integer> oddNumbers2 = new ArrayList<>();
List<Integer> result = numbers.stream()
.filter(n -> n % 2 != 0) // 홀수 필터링
.peek(oddNumbers2::add) // 홀수를 리스트에 추가
.collect(Collectors.toList()); // 필터링된 홀수를 리스트에 추가
System.out.println("Odd numbers: " + oddNumbers2); // [1, 3, 5]
System.out.println("result: " + result); // [1, 3, 5]
reduce()
요소를 결합하여 하나의 값으로 산출한다.
아래의 코드는 원래 숫자 10에서 스트림의 모든 요소들을 더해서 결과값을 산출하는 예시이다.
int number = 10;
List<Integer> numbers6 = Arrays.asList(1, 2, 3, 4, 5);
int sum2 = numbers6.stream().reduce(number, Integer::sum);
System.out.println(sum2); // 출력: 25
reduce() 연산은 map() 연산과 함께 자주 사용된다. 이는 데이터셋에 대해서 데이터를 변환한 후 집계하는 과정으로 유용하기 때문이다. 아래의 코드는 스트림의 모든 요소들을 제곱한 후, 합계를 집계하는 코드이다.
List<Integer> numbers5 = Arrays.asList(1, 2, 3, 4, 5);
// map 연산을 통해 각 요소를 제곱한 후, reduce 연산을 통해 모든 요소를 더한다.
int product = numbers5.stream()
.map(n -> n * n) // 각 요소를 제곱
.reduce(0, (a, b) -> a + b); // 모든 요소를 더함
System.out.println("Product: " + product); // 출력: Product: 56
collect()
스트림의 요소를 다양한 종류의 결과로 수집하고 싶은 경우 사용할 수 있다. 어떤 방식으로 요소를 수집할 것인지를 결정하는 Collector를 인자로 받아서 처리한다.
List, Map, Set으로 스트림의 요소 결과로 수집하기
List<String> strings = Stream.of("a", "b", "c")
.collect(Collectors.toList());
Set<Integer> numbers = Stream.of(1, 2, 3, 4, 5)
.collect(Collectors.toSet());
Map<String, Integer> map = Stream.of("apple", "banana", "cherry")
.collect(Collectors.toMap(Function.identity(), String::length));
스트림의 요소들의 총계 구하기
long count = Stream.of(1, 2, 3, 4, 5)
.collect(Collectors.counting());
스트림의 요소들을 문자열로 연결하기
String result = Stream.of("Java", "is", "fun")
.collect(Collectors.joining("!"));
System.out.println(result); // Java!is!fun
스트림의 요소를 지정된 키로 그룹화하기
단어의 길이가 같은 스트림의 요소끼리 그룹화하는 코드이다.
Map<Integer, List<String>> groupedByLength = Stream.of("apple", "banana", "cherry")
.collect(Collectors.groupingBy(String::length));
System.out.println(groupedByLength);
//{5=[apple], 6=[banana, cherry]}
스트림의 요소를 지정된 조건으로 분할하기
단어가 b로 시작하는 단어와, 그렇지 않은 단어로 분할하는 코드이다.
Map<Boolean, List<String>> partitioned = Stream.of("apple", "banana", "cherry")
.collect(Collectors.partitioningBy(s -> s.startsWith("b")));
System.out.println(partitioned);
//{false=[apple, cherry], true=[banana]}
통계
Stream의 요소들을 통계치를 계산할 수 있는 최종 연산들이 존재한다.
- max(), min(), sum(), count(), average()
min이나 max 또는 average는 Stream이 비어있는 경우 때문에 Optional로 값이 반환된다.
int[] arr= {1, 3, 2, 6, 8, 5};
int sum = Arrays.stream(arr).sum(); //arr 배열의 element들의 총 합을 반환
int count = (int) Arrays.stream(arr).count(); //arr 배열의 element의 개수 반환
OptionalDouble average = Arrays.stream(arr).average(); //arr 배열의 element들의 평균값
OptionalInt max = Arrays.stream(arr).max(); //arr 배열의 element들의 max값.
References
https://mangkyu.tistory.com/114
'Backend > Java' 카테고리의 다른 글
[Java] Collection(컬렉션) : 자료구조 및 시간복잡도 관점으로 보기 (1) | 2024.03.07 |
---|---|
[Java] 문자열(String) : 메서드, 형변환 및 StringBuilder & StringBuffer (0) | 2024.03.05 |
[Java] 람다식(Lambda Expression)과 함수형 프로그래밍 (1) | 2024.02.25 |
[Java] 내부 클래스(Inner Class) (멤버, 정적, 지역, 익명 내부 클래스) (0) | 2024.02.25 |
[Java] Generic Type (일반 타입) (0) | 2024.02.25 |