스트림(stream)이란?
함수형 프로그래밍을 기반으로 하며, 컬렉션 요소를 처리하는 연산을 지원하나. Stream은 요소를 저장하지 않으며, 소스 컬렉션으로부터 요소를 읽어오는 흐름이다. 이러한 Stream을 이용하여 컬렉션 요소를 처리하며, 중간 연산과 최종 연산을 이용하여 작업을 처리할 수 있다.
○ 스트림의 특징
- 선언형 프로그래밍: 데이터 처리 과정을 라이브러리 내부에서 처리하므로, 코드가 간결해지고 가독성이 높아진다.
- 병렬 처리: 요소를 여러 청크로 분할하고, 각각을 서로 다른 스레드에서 병렬 처리하여 빠른 처리 속도를 보장한다.
- 지연된 연산: Stream의 중간 연산은 지연 연산되므로 중간 연산은 실행되지 않고, 최종 연산이 호출될 때 한번에 처리된다. 이러한 방식은 효율적인 처리를 가능하게 한다.
- 순차 또는 병렬 집계 작업을 지원하는 오퍼레이션들의 모음이다.
- 데이터를 담고 있는 저장소 (컬렉션)이 아니다.
- 무제한일 수도 있다. (Short Circuit 메소드를 사용해서 제한할 수 있다.
스트림 파이프라인
0 또는 다수의 중개 오퍼레이션 (intermediate operation)과 한개의 종료 오퍼레이션 (terminal operation)으로 구성한다.
스트림의 데이터 소스는 오직 터미널 오퍼레이션을 실행할 때에만 처리한다.
중개 오퍼레이션
- Stream을 리턴한다.
- Stateless / Stateful 오퍼레이션으로 더 상세하게 구분할 수도 있다. (대부분은 Stateless지만 distinct나 sorted 처럼 이전 이전 소스 데이터를 참조해야 하는 오퍼레이션은 Stateful 오퍼레이션이다.
- filter(): 조건에 맞는 요소만 남기고 나머지 요소는 제외한다.
- map(): 요소들을 특정한 함수를 이용하여 변환한다.
- flatMap(): 요소들을 다른 요소로 대체한다.
- sorted(): 요소들을 정렬한다.
- distinct(): 중복 요소를 제거한다.
- peek(): 요소들을 참조하면서 중간 결과를 출력하거나 로깅한다.
종료 오퍼레이션
- Stream을 리턴하지 않는다.
- forEach(): 각 요소를 출력하거나 다른 연산을 수행한다.
- collect(): 스트림을 다른 컬렉션으로 변환한다.
- reduce(): 요소들을 누적하여 결과값을 반환한다.
- count(): 요소의 개수를 반환한다.
- anyMatch(): 조건에 맞는 요소가 하나라도 있는지 검사한다.
- allMatch(): 모든 요소가 조건에 맞는지 검사한다.
- noneMatch(): 모든 요소가 조건에 맞지 않는지 검사한다.
Stream API
1. 걸러내기 - Filter(Predicate)
- Filter() 메소드는 주어진 "Predicate" 조건을 만족하는 요소로 구성된 새로운 스트림을 반환한다.
- Predicate는 인터페이스로서, 주어진 값이 조건에 부합하는지 검사하고 true 또는 false를 반환한다. 따라서 filter() 메소드의 인자로 Predicate 객체를 전달하면 해당 스트림에서 조건에 맞는 요소만 걸러서 새로운 스트림으로 만들어준다.
예시) 문자열 리스트에서 길이가 5 이상인 문자열만 걸러내는 코드
List<String> words = Arrays.asList("apple", "banana", "kiwi", "peach");
List<String> longWords = words.stream()
.filter(s -> s.length() >= 5)
.collect(Collectors.toList());
위 코드에서 filter() 메소드는 s -> s.length() >= 5와 같은 람다 표현식을 인자로 받는다. 이 람다 표현식은 String 타입의 인자 s를 받아 해당 문자열의 길이가 5 이상인지 검사하고, true 또는 false를 반환한다. filter() 메소드는 이 조건을 만족하는 문자열만을 새로운 스트림으로 반환한다.
collect() 메소드를 이용하여 새로운 리스트로 수집하면, longWords 리스트에는 길이가 5 이상인 문자열만 담겨져 있다.
2. 변경하기 - Map(Function) 또는 FlatMap(Function)
- map() 메소드는 주어진 함수를 이용하여 각 요소를 변환하고, 이를 새로운 스트림으로 반환합니다.
예시) 문자열 리스트에서 각 문자열의 길이를 반환하는 코드
List<String> words = Arrays.asList("apple", "banana", "kiwi", "peach");
List<Integer> lengths = words.stream()
.map(s -> s.length())
.collect(Collectors.toList());
위 코드에서 map() 메소드는 s -> s.length()와 같은 람다 표현식을 인자로 받아, 각 문자열의 길이를 반환하도록 한다. 따라서 lengths 리스트에는 [5, 6, 4, 5]와 같은 정수들이 담겨져 있다.
- flatMap() 메소드는 스트림 내의 각 요소를 변환하고, 이를 하나의 스트림으로 연결한다. 이때 변환된 각 요소는 여러 개의 요소를 포함할 수 있다.
예시) 문자열 리스트에서 각 문자열을 단어로 분리한 후, 이를 하나의 리스트로 합치는 코드
List<String> words = Arrays.asList("apple banana", "kiwi peach", "orange");
List<String> flattenedWords = words.stream()
.flatMap(s -> Arrays.stream(s.split(" ")))
.collect(Collectors.toList());
위 코드에서 flatMap() 메소드는 s -> Arrays.stream(s.split(" "))와 같은 람다 표현식을 인자로 받아, 각 문자열을 공백을 기준으로 분리하여 스트림으로 반환한다. 그리고 flatMap() 메소드는 이렇게 반환된 각 스트림을 하나의 스트림으로 연결하여 반환한다. 따라서 flattenedWords 리스트에는 [apple, banana, kiwi, peach, orange]와 같은 문자열들이 담겨져 있다.
3. 생성하기 - generate(Supplier), Iterate(T seed, UnaryOperator)
- generate() 메소드는 주어진 Supplier 함수 객체를 사용하여 무한한 요소 시퀀스를 생성한다. 이 메소드는 상태가 없는 요소를 생성하는 데 유용하다.
예시) 0부터 시작하여 무한한 짝수 숫자를 생성하는 코드
Stream.generate(() -> 0)
.map(n -> n + 2)
.forEach(System.out::println);
- iterate() 메소드는 주어진 시드(seed) 값을 시작으로 주어진 UnaryOperator 함수 객체를 사용하여 무한한 요소 시퀀스를 생성한다. 이 메소드는 상태가 있는 요소를 생성하는 데 유용하다.
예시) 1부터 시작하여 2를 곱해가며 무한한 수열을 생성하는 코드
Stream.iterate(1, n -> n * 2)
.limit(10)
.forEach(System.out::println);
※두 메소드 모두 무한한 스트림을 생성하기 때문에, 제한(limit) 메소드 등을 사용하여 요소 개수를 제한해주는 것이 좋다. ※
4. 제한하기 - limit(long) , skip(long)
- limit() 메소드와 skip() 메소드는 중간 연산(intermediate operation)으로 사용되어, 스트림의 크기를 제한하거나 요소를 건너뛰는 데 사용된다.
- limit() 메소드는 스트림에서 처음 n개의 요소만 사용하도록 제한하는 데 사용된다.
예시) 0부터 시작하여 10개의 요소를 가진 스트림을 생성하고, limit() 메소드를 사용하여 처음 5개의 요소만 사용하는 코드
Stream.iterate(0, n -> n + 1)
.limit(5)
.forEach(System.out::println);
- limit() 메소드와 skip() 메소드를 함께 사용하여 원하는 위치부터 원하는 개수의 요소만을 사용할 수도 있다.
예시) 0부터 시작하여 10개의 요소를 가진 스트림을 생성하고, skip() 메소드를 사용하여 처음 5개의 요소를 건너뛰고 나서 limit() 메소드를 사용하여 다음 3개의 요소만 사용하는 코드
Stream.iterate(0, n -> n + 1)
.skip(5)
.limit(3)
.forEach(System.out::println);
5. 스트림에 있는 데이터가 특정 조건을 만족하는지 확인 - anyMatch(), allMatch(), noneMatch()
- anyMatch(Predicate<T> predicate)
최소한 하나의 요소가 주어진 조건(predicate)와 일치하는지 검사하여 결과를 반환한다. 즉, 스트림에 적어도 하나의 요소가 주어진 조건을 만족하면 true를 반환한다.
- allMatch(Predicate<T> predicate)
모든 요소가 주어진 조건(predicate)와 일치하는지 검사하여 결과를 반환한다. 즉, 스트림의 모든 요소가 주어진 조건을 만족하면 true를 반환한다.
- noneMatch(Predicate<T> predicate)
주어진 조건(predicate)를 만족하는 요소가 없는지 검사하여 결과를 반환한다. 즉, 스트림의 모든 요소가 주어진 조건을 만족하지 않으면 true를 반환한다.
예시)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// anyMatch 예시
boolean anyMatchResult = numbers.stream().anyMatch(num -> num > 3);
System.out.println("Any match result: " + anyMatchResult); // 출력 결과: true
// allMatch 예시
boolean allMatchResult = numbers.stream().allMatch(num -> num > 0);
System.out.println("All match result: " + allMatchResult); // 출력 결과: true
// noneMatch 예시
boolean noneMatchResult = numbers.stream().noneMatch(num -> num < 0);
System.out.println("None match result: " + noneMatchResult); // 출력 결과: true
위 예시 코드에서는 1부터 5까지의 정수를 담은 리스트를 스트림으로 변환한 후, anyMatch(), allMatch(), noneMatch() 메소드를 각각 호출하여 주어진 조건과 일치하는 요소가 있는지 검사하고 결과를 출력한다.
6. 개수 세기 - count()
count() 메소드는 스트림에서 요소의 개수를 반환하는 메소드로 중간 연산자가 아니며, 최종 연산자로 분류된다.
예시) 문자열 배열에서 길이가 5 이상인 문자열의 개수를 세는 방법
String[] words = {"apple", "banana", "cherry", "date", "elderberry", "fig", "grape"};
long count = Arrays.stream(words)
.filter(word -> word.length() >= 5)
.count();
System.out.println("Count of words with length >= 5: " + count);
// 출력값 : Count of words with length >= 5: 4
7. 스트림을 데이터 하나로 뭉치기 - reduce(identity, BiFunction), collect(), sum(), max()
- reduce(identity, BiFunction) 메소드는 스트림의 모든 요소를 하나의 값으로 축소(reduce)하여 반환한다.
identity 매개변수는 연산의 초기값을 제공하며, BiFunction은 이전 요소와 현재 요소를 결합하여 하나의 값을 만드는 데 사용된다.
예시)
int sum = Stream.of(1, 2, 3, 4, 5)
.reduce(0, (acc, x) -> acc + x);
System.out.println(sum); // 출력 결과: 15
위의 예제에서는 reduce() 메소드를 사용하여 Stream<Integer>에서 모든 요소를 더한 값을 계산하고 있다. 초기값은 0으로 설정하고, BiFunction은 이전 요소와 현재 요소를 더하여 새로운 값을 반환하도록 작성되었다. 따라서 이전 요소와 현재 요소를 더한 총합이 최종적으로 반환된다.
- collect() 메소드는 스트림의 요소를 수집하여 컬렉션(List, Set 등)이나 맵으로 변환한다.
예시)
List<Integer> list = Stream.of(1, 2, 3, 4, 5)
.filter(x -> x % 2 == 0)
.collect(Collectors.toList());
System.out.println(list); // 출력 결과: [2, 4]
위의 예제에서는 collect() 메소드를 사용하여 Stream<Integer>에서 짝수만 필터링하여 List로 수집하고 있다. Collectors.toList()는 List 컬렉션으로 변환하는 메소드이다.
- sum() 메소드는 IntStream, LongStream, DoubleStream 인터페이스에서 제공되며, 해당 스트림의 모든 요소의 합을 반환한다.
예시)
int sum = IntStream.of(1, 2, 3, 4, 5)
.sum();
System.out.println(sum); // 출력 결과: 15
- max() 메소드는 스트림의 요소 중 가장 큰 값을 반환한다.
예시)
Optional<Integer> max = Stream.of(1, 2, 3, 4, 5)
.max(Integer::compareTo);
System.out.println(max); // 출력 결과: Optional[5]
위의 예제에서는 max() 메소드를 사용하여 Stream<Integer>에서 가장 큰 값을 찾고 있다. Integer::compareTo는 비교 기준을 제공하는 메소드 참조이고 반환값은 Optional로 감싸져 있다. 가장 큰 요소가 없는 경우에는 Optional.empty()가 반환된다.
'Java' 카테고리의 다른 글
[자바] Excutor란? (0) | 2023.07.01 |
---|---|
[자바] Optional이란? (0) | 2023.06.29 |
[자바] 기본 메소드(Default Method)와 스태틱 메소드(Static Method) (0) | 2023.03.25 |
[자바] 메소드 레퍼런스 (0) | 2023.03.25 |
[자바] 람다표현식(Lambda Expressions) (0) | 2023.03.25 |