자바 스트림(Stream)은 Java 8부터 도입된 기능으로, 컬렉션, 배열, I/O 채널 등의 데이터 소스를 처리할 수 있는 선언적 API를 제공합니다. 스트림은 데이터를 변환하거나 필터링하는 연산을 사용해 다양한 작업을 수행할 수 있습니다. 스트림은 중간 연산과 최종 연산으로 나뉩니다. 중간 연산은 여러 개의 스트림을 연결하고, 최종 연산은 스트림을 소모하여 결과를 생성합니다.
스트림 생성 연산
Stream<T>:
일반적인 스트림으로, 어떤 타입의 객체든 처리할 수 있습니다. Stream<T>의 인스턴스는 Stream.of(), Arrays.stream() 또는 Collection.stream() 등의 방법으로 생성할 수 있습니다.
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamExample {
public static void main(String[] args) {
// 문자열 리스트 생성
List<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("cherry");
list.add("date");
// 문자열 리스트를 스트림 객체로 변환
Stream<String> stream = list.stream();
// 각 문자열을 대문자로 변환하고, 새로운 리스트로 변환
List<String> newList = stream.map(String::toUpperCase)
.collect(Collectors.toList());
// 새로운 리스트의 요소를 출력
for (String str : newList) {
System.out.println(str);
}
}
}
IntStream:
int 형식의 원시 데이터를 처리하기 위한 스트림입니다. 메모리 사용량과 박싱/언박싱 오버헤드를 줄이기 위해, 원시 데이터 타입에 대해 특수화된 스트림이 제공됩니다. IntStream은 IntStream.of(), IntStream.range(), IntStream.iterate() 등의 메서드로 생성할 수 있습니다.
import java.util.stream.IntStream;
public class IntStreamExample {
public static void main(String[] args) {
// 1부터 10까지의 숫자로 구성된 IntStream 객체 생성
IntStream stream1 = IntStream.range(1, 11);
// 1부터 10까지의 숫자를 포함하는 IntStream 객체 생성
IntStream stream2 = IntStream.rangeClosed(1, 10);
// 각 요소에 대해 람다식을 이용하여 처리
stream1.filter(n -> n % 2 == 0)
.forEach(System.out::println);
// 각 요소를 변환하여 새로운 스트림을 생성
IntStream stream3 = stream2.map(n -> n * n);
// 변환된 스트림의 요소를 배열로 변환
int[] arr = stream3.toArray();
// 배열의 요소를 출력
for (int i : arr) {
System.out.println(i);
}
}
}
LongStream:
long 형식의 원시 데이터를 처리하기 위한 스트림입니다. LongStream은 LongStream.of(), LongStream.range(), LongStream.iterate() 등의 메서드로 생성할 수 있습니다.
import java.util.stream.LongStream;
public class LongStreamExample {
public static void main(String[] args) {
// 1부터 10까지의 숫자로 구성된 LongStream 객체 생성
LongStream stream1 = LongStream.range(1L, 11L);
// 1부터 10까지의 숫자를 포함하는 LongStream 객체 생성
LongStream stream2 = LongStream.rangeClosed(1L, 10L);
// 각 요소에 대해 람다식을 이용하여 처리
long sum = stream1.filter(n -> n % 2 == 0)
.sum();
// 처리 결과를 출력
System.out.println(sum);
}
}
DoubleStream:
double 형식의 원시 데이터를 처리하기 위한 스트림입니다. DoubleStream은 DoubleStream.of(), DoubleStream.iterate(), Random.doubles() 등의 메서드로 생성할 수 있습니다.
import java.util.stream.DoubleStream;
public class DoubleStreamExample {
public static void main(String[] args) {
// double 타입 요소들로 구성된 DoubleStream 객체 생성
DoubleStream stream = DoubleStream.of(3.4, 4.5, 5.6, 6.7);
// 각 요소에 대해 람다식을 이용하여 처리
double sum = stream.filter(n -> n > 5.0)
.sum();
// 처리 결과를 출력
System.out.println(sum);
}
}
스트림 변환:
원시 데이터 스트림(IntStream, LongStream, DoubleStream)과 객체 스트림(Stream<T>) 간에 변환할 수 있습니다. 예를 들어, IntStream.mapToObj()를 사용하여 IntStream을 Stream<Integer>로 변환할 수 있습니다. 반대로 Stream.mapToInt()를 사용하여 Stream<Integer>를 IntStream으로 변환할 수 있습니다.
import java.util.Arrays;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
public class StreamConversionExample {
public static void main(String[] args) {
// int 배열을 IntStream으로 변환
int[] intArr = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(intArr);
// IntStream을 LongStream으로 변환
LongStream longStream = intStream.asLongStream();
// LongStream을 DoubleStream으로 변환
DoubleStream doubleStream = longStream.asDoubleStream();
// DoubleStream을 Stream<Double>으로 변환
Double[] doubleArr = doubleStream.boxed().toArray(Double[]::new);
// Stream<Double>을 Stream<Integer>로 변환
Arrays.stream(doubleArr)
.map(Double::intValue)
.forEach(System.out::println);
}
}
스트림의 중개 연산(intermediate operation)
스트림 API에 의해 생성된 초기 스트림은 중개 연산을 통해 또 다른 스트림으로 변환됩니다.
이러한 중개 연산은 스트림을 전달받아 스트림을 반환하므로, 중개 연산은 연속으로 연결해서 사용할 수 있습니다.
또한, 스트림의 중개 연산은 필터-맵(filter-map) 기반의 API를 사용함으로 지연(lazy) 연산을 통해 성능을 최적화할 수 있습니다.
스트림 API에서 사용할 수 있는 대표적인 중개 연산과 그에 따른 메소드는 다음과 같습니다.
1. 스트림 필터링 : filter(), distinct()
filter() 메소드는 해당 스트림에서 주어진 조건(predicate)에 맞는 요소만으로 구성된 새로운 스트림을 반환합니다.
또한, distinct() 메소드는 해당 스트림에서 중복된 요소가 제거된 새로운 스트림을 반환합니다.
distinct() 메소드는 내부적으로 Object 클래스의 equals() 메소드를 사용하여 요소의 중복을 비교합니다.
List<String> words = Arrays.asList("apple", "banana", "orange", "apple", "orange", "kiwi");
List<String> filteredWords = words.stream()
.filter(word -> word.length() > 4)
.distinct()
.collect(Collectors.toList());
2. 스트림 변환 : map(), flatMap()
map() 메소드는 해당 스트림의 요소들을 주어진 함수에 인수로 전달하여, 그 반환값들로 이루어진 새로운 스트림을 반환합니다. 만약 해당 스트림의 요소가 배열이라면, flatMap() 메소드를 사용하여 각 배열의 각 요소의 반환값을 하나로 합친 새로운 스트림을 얻을 수 있습니다.
List<String> words = Arrays.asList("apple", "banana", "orange");
// map() 예시
List<Integer> wordLengths = words.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(wordLengths); // [5, 6, 6]
// flatMap() 예시
List<String> letters = words.stream()
.flatMap(word -> Arrays.stream(word.split("")))
.distinct()
.collect(Collectors.toList());
System.out.println(letters); // [a, p, l, e, b, n, o, r, g]
위 코드는 String 자체가 배열이기 때문에 char로 하나씩 분리
3. 스트림 제한 : limit(), skip()
limit() 메소드는 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소만으로 이루어진 새로운 스트림을 반환합니다.
skip() 메소드는 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소를 제외한 나머지 요소만으로 이루어진 새로운 스트림을 반환합니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// limit() 예시
List<Integer> firstThreeNumbers = numbers.stream()
.limit(3)
.collect(Collectors.toList());
System.out.println(firstThreeNumbers); // [1, 2, 3]
// skip() 예시
List<Integer> afterSkippingThreeNumbers = numbers.stream()
.skip(3)
.collect(Collectors.toList());
System.out.println(afterSkippingThreeNumbers); // [4, 5, 6, 7, 8, 9, 10]
4. 스트림 정렬 : sorted()
sorted() 메소드는 해당 스트림을 주어진 비교자(comparator)를 이용하여 정렬합니다. 이때 비교자를 전달하지 않으면 기본적으로 사전 편찬 순(natural order)으로 정렬하게 됩니다.
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5);
// 오름차순으로 정렬하는 예시
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedNumbers); // [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]
// 내림차순으로 정렬하는 예시
List<Integer> reverseSortedNumbers = numbers.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
System.out.println(reverseSortedNumbers); // [9, 6, 5, 5, 5, 4, 3, 3, 2, 1, 1]
5. 스트림 연산 결과 확인 : peek()
peek() 메소드는 결과 스트림으로부터 요소를 소모하여 추가로 명시된 동작을 수행합니다. 이 메소드는 원본 스트림에서 요소를 소모하지 않으므로, 주로 연산과 연산 사이에 결과를 확인하고 싶을 때 사용합니다. 따라서 개발자가 디버깅 용도로 많이 사용합니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> processedNumbers = numbers.stream()
.peek(num -> System.out.println("Processing number: " + num))
.filter(num -> num % 2 == 0)
.peek(num -> System.out.println("Filtered number: " + num))
.map(num -> num * num)
.peek(num -> System.out.println("Mapped number: " + num))
.collect(Collectors.toList());
System.out.println(processedNumbers); // [4, 16]
스트림의 최종 연산(terminal operation)
스트림 API에서 중개 연산을 통해 변환된 스트림은 마지막으로 최종 연산을 통해 각 요소를 소모하여 결과를 표시합니다.즉, 지연(lazy)되었던 모든 중개 연산들이 최종 연산 시에 모두 수행되는 것입니다. 이렇게 최종 연산 시에 모든 요소를 소모한 해당 스트림은 더는 사용할 수 없게 됩니다.
1. 요소의 출력 : forEach()
앞선 수업에서 자주 사용한 forEach() 메소드는 스트림의 각 요소를 소모하여 명시된 동작을 수행합니다.
반환 타입이 void이므로 보통 스트림의 모든 요소를 출력하는 용도로 많이 사용합니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.filter(num -> num % 2 == 0)
.forEach(num -> System.out.println(num + " is even."));
// 출력 결과:
// 2 is even.
// 4 is even.
2. 요소의 소모 : reduce()
스트림의 최종 연산은 모두 스트림의 각 요소를 소모하여 연산을 수행하게 됩니다. 하지만 reduce() 메소드는 첫 번째와 두 번째 요소를 가지고 연산을 수행한 뒤, 그 결과와 세 번째 요소를 가지고 또다시 연산을 수행합니다. 이런 식으로 해당 스트림의 모든 요소를 소모하여 연산을 수행하고, 그 결과를 반환하게 됩니다.또한, 인수로 초깃값을 전달하면 초깃값과 해당 스트림의 첫 번째 요소와 연산을 시작하며, 그 결과와 두 번째 요소를 가지고 계속해서 연산을 수행하게 됩니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("Sum of numbers: " + sum); // Sum of numbers: 15
3. 요소의 검색 : findFirst(), findAny()
findFirst()와 findAny() 메소드는 해당 스트림에서 첫 번째 요소를 참조하는 Optional 객체를 반환합니다.
두 메소드 모두 비어 있는 스트림에서는 비어있는 Optional 객체를 반환합니다.
findFirst()와 findAny() 메소드는 모두 스트림에서 첫 번째 요소를 반환하지만, 동작 방식에서 차이가 있습니다.
findFirst() 메소드는 스트림의 첫 번째 요소부터 시작하여 조건에 맞는 첫 번째 요소를 찾아 반환합니다. 이때 스트림의 첫 번째 요소가 조건에 맞는 경우, 반환된 결과는 항상 스트림에서의 첫 번째 요소가 됩니다. 따라서 스트림이 정렬되어 있을 경우, 정렬된 순서대로 첫 번째 요소를 반환하며, 정렬되어 있지 않은 경우, 임의의 첫 번째 요소를 반환합니다.
반면에 findAny() 메소드는 스트림에서 임의의 요소를 반환합니다. 이때, 스트림의 어떤 요소든 조건에 맞으면 반환됩니다. 따라서 스트림이 정렬되어 있을 경우, 첫 번째 요소가 반환될 가능성이 있지만, 정렬되어 있지 않은 경우에는 더 많은 요소가 반환될 가능성이 있습니다. 이러한 특성 때문에 findAny() 메소드는 병렬 처리에서 유용하게 사용될 수 있습니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstEvenNumber = numbers.stream()
.filter(num -> num % 2 == 0)
.findFirst();
System.out.println("First even number: " + firstEvenNumber.get()); // First even number: 2
Optional<Integer> anyEvenNumber = numbers.stream()
.filter(num -> num % 2 == 0)
.findAny();
System.out.println("Any even number: " + anyEvenNumber.get()); // Any even number: 2 (실행할 때마다 다를 수 있음)
4. 요소의 검사 : anyMatch(), allMatch(), noneMatch()
해당 스트림의 요소 중에서 특정 조건을 만족하는 요소가 있는지, 아니면 모두 만족하거나 모두 만족하지 않는지를 다음 메소드를 사용하여 확인할 수 있습니다.
1. anyMatch() : 해당 스트림의 일부 요소가 특정 조건을 만족할 경우에 true를 반환함.
2. allMatch() : 해당 스트림의 모든 요소가 특정 조건을 만족할 경우에 true를 반환함.
3. noneMatch() : 해당 스트림의 모든 요소가 특정 조건을 만족하지 않을 경우에 true를 반환함.
세 메소드 모두 인수로 Predicate 객체를 전달받으며, 요소의 검사 결과는 boolean 값으로 반환합니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean anyEvenNumber = numbers.stream()
.anyMatch(num -> num % 2 == 0);
System.out.println("Any even number: " + anyEvenNumber); // Any even number: true
boolean allEvenNumbers = numbers.stream()
.allMatch(num -> num % 2 == 0);
System.out.println("All even numbers: " + allEvenNumbers); // All even numbers: false
boolean noneNegativeNumbers = numbers.stream()
.noneMatch(num -> num < 0);
System.out.println("None negative numbers: " + noneNegativeNumbers); // None negative numbers: true
5. 요소의 통계 : count(), min(), max()
count() 메소드는 해당 스트림의 요소의 총 개수를 long 타입의 값으로 반환합니다.
또한, max()와 min() 메소드를 사용하면 해당 스트림의 요소 중에서 가장 큰 값과 가장 작은 값을 가지는 요소를 참조하는 Optional 객체를 얻을 수 있습니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
long count = numbers.stream().count();
System.out.println("Number of elements: " + count); // Number of elements: 5
Optional<Integer> min = numbers.stream().min(Integer::compare);
System.out.println("Minimum element: " + min.get()); // Minimum element: 1
Optional<Integer> max = numbers.stream().max(Integer::compare);
System.out.println("Maximum element: " + max.get()); // Maximum element: 5
6. 요소의 연산 : sum(), average()
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().mapToInt(Integer::intValue).sum();
System.out.println("Sum of elements: " + sum); // Sum of elements: 15
OptionalDouble avg = numbers.stream().mapToDouble(Double::valueOf).average();
System.out.println("Average of elements: " + avg.getAsDouble()); // Average of elements: 3.0
7. 요소의 수집 : collect()
collect() 메소드는 인수로 전달되는 Collectors 객체에 구현된 방법대로 스트림의 요소를 수집합니다. 또한, Collectors 클래스에는 미리 정의된 다양한 방법이 클래스 메소드로 정의되어 있습니다. 이 외에도 사용자가 직접 Collector 인터페이스를 구현하여 자신만의 수집 방법을 정의할 수도 있습니다.
스트림 요소의 수집 용도별 사용할 수 있는 Collectors 메소드는 다음과 같습니다.
1. 스트림을 배열이나 컬렉션으로 변환 : toArray(), toCollection(), toList(), toSet(), toMap()
2. 요소의 통계와 연산 메소드와 같은 동작을 수행 : counting(), maxBy(), minBy(), summingInt(), averagingInt() 등
3. 요소의 소모와 같은 동작을 수행 : reducing(), joining()
4. 요소의 그룹화와 분할 : groupingBy(), partitioningBy()
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
.filter(num -> num % 2 == 0)
.collect(Collectors.toList());
System.out.println("Even numbers: " + evenNumbers); // Even numbers: [2, 4]
생성 -> 중간 -> 최종
import java.util.Arrays;
import java.util.List;
public class ComplexStreamExample {
public static void main(String[] args) {
// 숫자 리스트 생성
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 2, 3, 4);
// 리스트에서 중복되지 않는 홀수들의 합을 계산
int sum = numbers.stream()
.filter(n -> n % 2 == 1) // 홀수만 필터링
.distinct() // 중복 제거
.mapToInt(Integer::intValue) // int 타입으로 변환
.reduce(0, Integer::sum); // 합을 계산
// 계산된 합을 출력
System.out.println(sum);
}
}
'Java' 카테고리의 다른 글
[java] 프로그래머스 : 정수 삼각형 (0) | 2023.04.04 |
---|---|
[java] 프로그래머스 : N으로 표현 (0) | 2023.04.04 |
[Java] For문 보다 Stream? (0) | 2023.04.01 |
[자료 구조] 그래프 : DFS, BFS (0) | 2023.03.30 |
JUnit : IntelliJ , 단위 테스트 (0) | 2023.03.29 |