스트림 API
- 스트림은 데이터 소스를 추상화하기 위해, 다양한 방식으로 저장된 데이터를 읽고 쓰기 위한 공통된 방법을 제공한다.
- 즉, 데이터 소스가 무엇이든 간에 같은 방식으로 다룰 수 있게 함으로써 코드의 재사용성을 높인다.
- 스트림을 이용하면 배열이나 컬렉션 뿐만 아니라 파일에 저장된 데이터도 모두 같은 방법으로 다룰 수 있다.
특징
- 스트림은 외부 반복을 통해 작업하는 컬렉션과 달리 내부 반복을 통해 작업을 수행한다.
- 스트림은 재사용이 가능한 컬렉션과는 달리 단 한번만 사용할 수 있다.
- 스트림은 데이터 소스로부터 데이터를 읽기만 할뿐 원본 데이터를 변경하지 않는다.
- 스트림의 연산은 필터-맵 기반의 API를 사용하여 지연 연산을 통해 성능을 최적화한다.
- 스트림은 parallelStream() 메서드를 통한 병렬처리를 지원한다.
동작 흐름
- 스트림 생성
- 스트림 중개 연산 (스트림 변환)
- 스트림 최종 연산 (스트림 사용)
스트림 생성
1. 컬렉션
- 모든 컬렉션의 최고 조상인 Collection 인터페이스에는 stream() 메서드가 정의되어 있으므로, Collection 인터페이스를 구현한 모든 컬렉션 클래스들은 stream() 메서드로 스트림을 생성할 수 있다.
- stream() 메서드는 해당 컬렉션을 소스로 하는 스트림을 반환한다.
// 컬렉션에서 스트림 생성
Stream<Integer> stream = list.stream();
// forEach() 메소드를 이용한 스트림 요소의 순차 접근
stream.forEach(System.out::println);
2. 배열
- 배열을 소스로 하는 스트림을 생성하는 메서드는 Arrays에 static 메서드로 정의되어 있다.
Stream<T> Stream.of(T... values)
Stream<T> Stream.of(T[])
Stream<T> Arrays.stream(T[])
Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)
- 또한 기본 타입인 int, long, double 형을 저장할 수 있는 배열에 관한 스트림은 java.util.stream 패키지의 IntStream, LongStream, DoubleStream 인터페이스로 각각 제공된다.
3. 가변 매개변수
- Stream 클래스의 of() 메서드를 사용하면 가변 매개변수를 전달받아 스트림을 생성할 수 있다.
// 가변 매개변수에서 스트림 생성
Stream<Double> stream = Stream.of(4.2, 2.5, 3.1, 1.9);
stream.forEach(System.out::println);
4. 람다 표현식
- 람다식을 매개변수로 전달받아 해당 람다식에 의해 반환되는 값을 요소로 하는 무한 스트림을 생성하기 위해 Stream 클래스에는 iterate()와 generate() 메서드가 정의되어 있다.
- iterate() 메서드는 seed로 명시된 값을 람다식에 사용하여 반환된 값을 다시 seed로 사용하는 방식으로 무한 스트림을 생성한다.
- 반면에 generate() 메서드는 매개변수가 없는 람다식을 사용하여 반환된 값으로 무한 스트림을 생성한다.
IntStream stream = Stream.iterate(2, n -> n + 2); // 2, 4, 6, 8, 10, ...
5. 빈 스트림
- empty() 메서드를 이용하면 요소가 하나도 없는 비어있는 스트림을 생성할 수 있다.
- 만약 스트림에 연산을 수행한 결과가 하나도 없을 때는 null보다는 빈 스트림을 반환하는 것이 더 좋다.
Stream emptyStream = Stream.empty();
long count = emptyStream.count(); // count = 0
스트림 중개 연산
- 스트림 API에 의해 생성된 초기 스트림은 중개 연산을 통해 또다른 스트림으로 변환할 수 있다.
- 중개 연산은 스트림을 전달받아 스트림을 반환하므로, 연속으로 연결해서 사용 가능하다.
- 또한 중개 연산은 어떤 작업이 수행되어야 하는지를 지정해주는 것으로, 최종 연산이 수행되어야만 스트림의 요소들이 중개 연산을 거쳐 최종 연산에서 소모된다.
스트림 필터링 - filter(), distinct()
- filter() 메서드느 해당 스트림에서 주어진 조건에 맞는 요소만으로 구성된 새로운 스트림을 반환한다.
- 또한 distinct() 메서드는 해당 스트림에서 중볻괸 요소가 제거된 새로운 스트림을 반환한다.
- distinct() 메서드는 내부적으로 Object 클래스의 equals() 메서드를 사용하여 요소의 중복을 비교한다.
// 스트림에서 중복된 요소를 제거함.
stream1.distinct().forEach(e -> System.out.print(e + " "));
// 스트림에서 홀수만을 골라냄.
stream2.filter(n -> n % 2 != 0).forEach(e -> System.out.print(e + " "));
스트림 변환 - map(), flatMap()
- map() 메서드는 해당 스트림 요소들을 주어진 함수에 인자로 전달하여, 그 반환값들로 이루어진 새로운 스트림을 반환한다.
- 스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환해야할 때 사용한다.
// 매개변수 T 타입을 R 타입으로 변환해서 반환하는 함수 지정
Stream map(Function<? super T, ? extens R> mapper)
stream.map(s -> s.length()).forEach(System.out::println);
- 만약 해당 스트림의 요소가 배열이라면 flatMap() 메서드를 사용하여 각 배열의 각 요소의 반환값을 하나로 합친 새로운 스트림을 얻을 수 있다.
- 즉, Stream<T[]>를 Stream으로 변환한다.
String[] arr = {"I study hard", "You study JAVA", "I am hungry"};
Stream<String> stream = Arrays.stream(arr);
stream.flatMap(s -> Stream.of(s.split(" +"))).forEach(System.out::println);
스트림 제한 - limit(), skip()
- limit() 메서드는 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소만으로 이루어진 새로운 스트림을 반환한다.
- skip() 메서드는 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소를 제외한 나머지 요소만으로 이루어진 새로운 스트림을 반환한다.
스트림 정렬 - sorted()
- sorted() 메서드는 해당 스트림을 주어진 comparator를 이용하여 정렬하며, 이때 comparator를 전달하지 않으면 기본적으로 naturl order로 정렬한다.
stream.sorted(Comparator.reverseOrder()).forEach(s -> System.out.print(s + " "));
스트림 연산 결과 확인 - peek()
- peek() 메서드는 결과 스트림으로부터 요소를 소모하여 추가로 명시된 동작을 수행한다.
- 원본 스트림에서 요소를 소모하지 않으므로, 주로 연산과 연산 사이에 결과를 확인하고 싶을 때 사용한다.
스트림 최종 연산
- 중개 연산을 통해 변환된 스트림은 마지막으로 최종 연산을 통해 각 요소를 소모하여 결과를 표시한다.
- 즉, 지연되었던 모든 중개 연산들이 최종 연산 시에 모두 수행되는 것
- 최종 연산 시에 모든 요소를 소모한 스트림은 더는 사용할 수 없으며, 최종 연산의 결과는 단일값이나 스트림 요소가 담긴 배열 또는 컬렉션
요소의 출력 - forEach()
- forEach() 메서드는 스트림의 각 요소를 소모하여 명시된 동작을 수행한다.
- 반환 타입이 void이므로 보통 스트림의 모든 요소를 출력하는 용도로 많이 사용한다.
stream.forEach(System.out::println);
요소의 소모 - reduce()
- reduce() 메서드는 첫 번째와 두 번째 요소를 가지고 연산을 수행한 뒤, 그 결과와 세 번째 요소를 가지고 또다시 연산을 수행한다.
- 이런 식으로 해당 스트림의 모든 요소를 소모하여 연산을 수행한 뒤 그 결과를 반환한다.
- 또한 인수로 초기값을 전달하면 초기값과 해당 스트림의 첫 번째 요소와 연산을 시작한다.
Optional reduce(BinaryOperator accumulator)
Optional<Integer> result = stream.reduce((s1, s2) -> s1 + s2);
result.ifPresent(System.out::println);
요소의 검색 - findFirst(), findAny()
- findFirst()와 findAny() 메서드는 해당 스트림에서 첫 번째 요소를 참조하는 Optional 객체를 반환하며, 비어있는 스트림의 경우 비어있는 Optional 객체를 반환한다.
- 병렬 스트림인 경우에는 findAny() 메서드를 사용해야 정확한 연산 결과가 반환된다.
요소의 검사 - anyMatch(), allMatch(), noneMatch()
- 해당 스트림의 요소 중에서 특정 조건을 만족하는 요소가 있는지, 모두 만족하는지, 모두 만족하지 않는지를 확인한다.
- 세 메서드 모두 인수로 Perdicate 객체를 전달받으며, 요소의 검사 결과는 boolean 값으로 반환된다.
요소의 통계 - count(), min(), max()
- count() 메서드는 해당 스트림의 요소의 총 개수를 long 타입의 값으로 반환한다.
- max()와 min() 메서드를 사용하면 해당 스트림의 요소 중에서 가장 큰 값과 가장 작은 값을 가지는 요소를 참조하는 Optional 객체를 얻을 수 있다.
요소의 연산 - sum(), average()
- IntStream이나 DoubleStream과 같은 기본 타입 스트림에는 해당 스트림의 모든 요소에 대해 합과 평균을 구할 수 있는 sum()과 average() 메서드가 정의되어 있다.
- average() 메서드는 각 기본 타입으로 래핑 된 Optional 객체를 반환한다.
public class StreamEx {
public static void main(String[] args) {
String[] strArr = {
"Inheritance", "Java", "Lambda", "stream",
"OptionalDouble", "IntStream", "count", "sum"
};
Stream.of(strArr).forEach(System.out::println);
boolean noEmptyStr = Stream.of(strArr).noneMatch(s -> s.length() == 0);
Optional<String> sWord = Stream.of(strArr)
.filter(s -> s.charAt(0) == 's').findFirst();
System.out.println("noEmptyStr = " + noEmptyStr);
System.out.println("sWord = " + sWord);
IntStream intStream1 = Stream.of(strArr).mapToInt(String::length);
IntStream intStream2 = Stream.of(strArr).mapToInt(String::length);
IntStream intStream3 = Stream.of(strArr).mapToInt(String::length);
IntStream intStream4 = Stream.of(strArr).mapToInt(String::length);
int count = intStream1.reduce(0, (a, b) -> a + 1);
int sum = intStream2.reduce(0, Integer::sum);
OptionalInt max = intStream3.reduce(Integer::max);
OptionalInt min = intStream4.reduce(Integer::min);
System.out.println("count = " + count);
System.out.println("sum = " + sum);
System.out.println("max = " + max);
System.out.println("min = " + min);
}
}
요소의 수집 - collect()
- collect() 메서드는 인수로 전달되는 Collectors 객체에 구현된 방법대로 스트림의 요소를 수집한다.
- Collectors 클래스에는 미리 정의된 다양한 방법이 클래스 메서드로 정의되어 있으며, 그 외에도 사용자가 직접 Collector 인터레이스를 구현하여 수집 방법을 정의할 수도 있다.
Collectors 메서드
- 스트림을 배열이나 컬렉션으로 반환: toArray(), toCollection(), toList(), toSet(), toMap()
- 요소의 통계와 연산: counting(), maxBy(), minBy(), summingInt(), averagingInt()
- 요소의 소모: reducint(), joining()
- 요소의 그룹화와 분할: groupingBy(), partitioningBy()
public class CollectorEx {
public static void main(String[] args) {
String[] strArr = {"aaa", "bbb", "ccc"};
Stream<String> strStream = Stream.of(strArr);
String result = strStream.collect(new ConcatCollector());
System.out.println(Arrays.toString(strArr));
System.out.println("result = " + result);
}
}
class ConcatCollector implements Collector<String, StringBuilder, String> {
// 수집 결과를 저장할 공간 제공
@Override
public Supplier<StringBuilder> supplier() {
return StringBuilder::new;
}
// 스트림의 요소를 어떻게 supplier()가 제공한 공간에 누적할 것인지 정의
@Override
public BiConsumer<StringBuilder, String> accumulator() {
return StringBuilder::append;
}
// 여러 쓰레드에 의해 처리된 결괄르 어떻게 합칠 것인지 정의 (병렬 스트림)
@Override
public BinaryOperator<StringBuilder> combiner() {
return StringBuilder::append;
}
// 결과를 최종적으로 변환할 방법 제공
@Override
public Function<StringBuilder, String> finisher() {
return StringBuilder::toString;
}
// 컬렉터가 수행하는 작업의 속성에 대한 정보 제공
@Override
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
}
'Java > Java' 카테고리의 다른 글
Optional 클래스 (0) | 2022.03.28 |
---|---|
람다 표현식 (0) | 2022.03.28 |
Enum 클래스 (0) | 2022.03.28 |
Arrays, Collections (0) | 2022.03.28 |
Comparable과 Comparator (0) | 2022.03.28 |