본문 바로가기
JAVA/Java

[Java] Stream

by 민트맛녹차 2022. 9. 23.

스트림(Stream)

int[] arr = {1, 2, 3, 4, 5}

/* example1 */
IntStream stm1 = Arrays.stream(arr);	        // 스트림 생성
IntStream stm2 = stm1.filter(n -> n%2 == 1);	// 중간 연산
int sum = stm2.sum();	                        // 최종 연산

/* example2 */
int sum = Arrays.stream(arr)	                // 스트림 생성
                .filter(n -> n%2 == 1)	        // 중간 연산
                .sum();	                        // 최종 연산

Java 에서 데이터의 흐름을 생성할 수 있는데, 이러한 흐름을 스트림(Stream)이라고 한다. 배열이나 컬렉션 인스턴스를 다룰 때 for문이나 iterator를 사용하여 접근하였다. Java 8에서 스트림이 추가되어 배열과 컬렉션 등을 함수형으로 처리해 코드를 간결하게 표현할 수 있고, 병렬처리 또한 가능하게 되었다.

example1 처럼 메서드 호출 마다 반환하여 사용할 수 있지만, 일반적으로 example2 처럼 메서드를 체이닝 하여 한 문장으로 완성하여 사용한다.

스트림은 스트림의 생성, 스트림을 가공하는 중간 연산, 결과값을 반환하는 최종연산 순서로 연산이 진행된다. 스트림 연산은 효율과 성능을 고려하여 지연(Lazy) 처리 방식으로 동작한다. 최종 연산 메서드가 호출 되어야 중간 연산의 결과가 스트림에 반영되고, 이어서 최종 연산의 결과가 스트림에 반영되는 것이다.

 

스트림 생성

배열 

public static <T> Stream<T> stream(T[] array) { ... }

Arrays 클래스에 정의되어 있는 stream 메서드이다. stream 메서드를 사용하면 배열을 이용하여 스트림 인스턴스를 생성할 수 있다.

아래와 같이 기본 자료형 값의 반환을 위한 stream 도 제공한다.

public static IntStream stream(int[] array) { ... }
public static IntStream stream(int[] array, int startInclusive, int endExclusive) { ... }

public static DoubleStream stream(double[] array) { ... }
public static DoubleStream stream(double[] array, int startInclusive, int endExclusive) { ... }

public static LongStream stream(double[] array) { ... }
public static LongStream stream(double[] array, int startInclusive, int endExclusive) { ... }

 

컬렉션

default Stream<E> stream();

Collecion<E> 인터페이스에 디폴트 메서드로 정의되어 있는 stream 메서드이다.

 

of

static <T> Stream<T> of(T t) { ... }
static <T> Stream<T> of(T... values) { ... }

of 메서드를 사용하면 스트림 생성에 필요한 데이터를 직접 전달할 수 있다.

Stream.of("Toy", "Robot", "Box")
        .forEach(n -> System.out.print(n + " "));	// Toy Robot Box

String[] arr = {"Toy", "Robot", "Box"};
Stream.of(arr)
        .forEach(n -> System.out.print(n + " "));	// Toy Robot Box

List<String> list = Arrays.asList("Toy", "Robot", "Box");
Stream.of(list)
        .forEach(n -> System.out.print(n + " "));	// [Toy, Robot, Box]

여러 인자가 들어가거나 배열의 경우 데이터의 개수만큼 스트림이 생성되지만,  컬렉션 인스턴스는 해당 인스턴스 하나로 이루어진 스트림이 생성되는 차이점이 있다.

기본 자료형을 위해 IntStream, DoubleStream, LongStream을 반환하는 of 메서드도 존재한다. 아래와 같이 범위 내에 있는 값들로 스트림을 생성할 수 있다. double 형이 없는 이유는 실수 사이에 존재하는 값은 셀 수 없기 때문이다.

static IntStream range(int startInclusive, int endExclusive) { ... }
static IntStream rangeClosed(int startInclusive, int endInclusive) { ... }

static LongStream range(Long startInclusive, Long endExclusive) { ... }
static LongStream rangeClosed(Long startInclusive, Long endInclusive) { ... }

 

병렬 스트림(Parallel Stream)

default Stream<E> parallelStream() { ... }
Stream<T> parallel() { ... }

병렬처리를 지원하는 병렬 스트림을 생성할 수 있다. 컬렉션의 경우, Collection<E> 인터페이스에 디폴트 메서드로 정의되어 있는 parallelStream 메서드를 사용한다. 이미 생성된 스트림의 경우, parallel 메서드를 사용한다.

병렬 스트림을 사용하면 CPU 코어 수를 고려하여 병렬 처리가 가능해진다.  둘 이상의 연산을 동시에 진행하므로, 연산의 단계를 줄일 수 있다(연산의 횟수가 줄어드는 것은 아니다).

기본 자료형을 위해 IntStream, DoubleStream, LongStream을 반환하는 parallel 메서드도 존재한다. 

 

스트림 연결

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) { ... }

Stream<String> stream1 = Stream.of("One", "Two");
Stream<String> stream2 = Stream.of("Three", "Four");

Stream.concat(stream1, stream2)
        .forEach(s -> System.out.print(s + " "));	// One Two Three Four

concat 메서드는 두개의 스트림을 연결하여 하나의 스트림을 생성한다. 기본 자료형을 위해 IntStream, DoubleStream, LongStream을 반환하는 concat 메서드도 존재한다. 

 

 

중간 연산자(Intermediated Operation)

중간 연산자를 사용하면 스트림을 가공할 수 있다.

Filtering

Stream<T> filter(Predicate<? super T> predicate) { ... }

filter 메서드는 스트림을 구성하는 데이터 중 일부를 조건에 따라 걸러내는 작업을 한다. 매개변수 형으로 Predicate<T>를 받으며 predicate를 만족하는 스트림을 반환한다. filter 메서드는 내부적으로 스트림 데이터를 하나씩 인자로 전달하며 test를 호출한다. 그 결과 true가 반환되면 스트림에 남기고, false가 반환되면 해당 데이터는 버린다.

int[] arr = {1, 2, 3, 4, 5}
Arrays.stream(ar)
        .filter(n -> n%2 == 1)
        .forEach(n -> System.out.print(n + " "));	// 1 3 5

위의 코드를 예로 들면 predicate에 n -> n%2 == 1이 들어가 있다. 이 식은 홀수일 때 true를 반환하는 람다식이므로, filter 메서드는 홀수인 데이터만 남긴다.

Mapping

<R> Stream<R> map(Function<? super T, ? extends R> mapper) { ... }

map 메서드는 스트림을 구성한는 데이터를 가공하여 다른 형태의 데이터로 만드는 작업을 한다. 이때 데이터의 값 뿐만 아니라 타입도 바뀔 수 있다. 매개변수 형으로 Function<T, R> 받으며 mapper를 사용하여 기존 스트림을 변형하여 만들어진 새로운 스트림을 반환한다. map 메서드는 내부적으로 스트림 데이터를 하나씩 인자로 전달하여 apply  메서드를 호출한다. 그 결과 반환되는 값을 모아 새로운 스트림을 생성한다.

List<String> ls = Arrays.asList("Box", "Robot", "Simple");
ls.stream()
    .map(s -> s.length())
    .forEach(n -> System.out.print(n + " "));	// 3 5 6

위의 코드를 보면 mapper에 s -> s.length()가 들어있다. 이 식은 String s 를 인자로 받아 s.length 를 반환하는 식으로, 데이터의 길이로 이루어진 스트림을 생성한다.

기본 자료형 값의 반환을 고려하여 아래와 같은 매핑 관련 메서드들도 제공한다. 아래 메서드를 사용하면 오토박싱이 진행되지 않는 장점이 있다.

IntStream mapToInt(ToIntFunction<? super T> mapper);
LongStream mapToInt(ToLongFunction<? super T> mapper);
DoubleStream mapToInt(ToDoubleFunction<? super T> mapper);

 

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper) { ... }

<R> Stream<R> flatMapToInt(Function<? super T, ? extends IntStream> mapper) { ... }
<R> Stream<R> flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper) { ... }
<R> Stream<R> flatMapToLong(Function<? super T, ? extends LongStream> mapper) { ... }

flatMap 메서드는 map 메서드와 비슷하지만 전달하는 람다 식에서 스트림을 생성하고 반환해야 한다는 차이가 있다. 매개변수 형으로  Function<T, Stream<R>> 을 받는다.  기본 자료형 값의 반환을 고려한 flatMap 메서드들도 제공한다.

class ReportCard {
    private int kor;
    private int eng;
    private int math;
    ...
}

ReportCard[] cards = {
        new ReportCard(70, 80, 90),
        new ReportCard(90, 80, 70),
        new ReportCard(80, 80, 80)
};

Arrays.stream(cards)
        .flatMapToInt(r -> IntStream.of(r.getKor(), r.getEng(), r.getMath()))
        .forEach(System.out::println);	// 70 80 90 90 80 70 80 80 80

위의 코드를 보면 mapper에 r -> IntStream(r.getKor(), r.getEng(), r.getMath()) 가 들어있다. 이 식은 ReportCard r 을 인자로 받아 r의 kor, eng, math를 가진 새로운 스트림을 생성한다. 한 데이터를 사용해 여러 데이터를 가진 스트림을 생성할 때 유용해 보인다.

 

Sorting

Stream<T> sorted() { ... }
Stream<T> sorted(Comparator<? super T> comparator) { ... }

정렬을 위해서는 스트림을 구성하는 인스턴스가 Comparable<T> 인터페이스를 구현해야 한다.

인자가 없는 sorted 메서드는 데이터를 오름차순으로 정렬한다. 인자가 있는 경우 매개변수 형으로 Comparator<T> 을 받는다. sorted 메서드는 내부적으로 compare(T o1, T o2) 를 호출하여 데이터를 정렬한다. 

기본 자료형 값의 반환을 고려해, IntStream, DoubleStream, LongStream에서도  sorted() 메서드를 제공한다.

 

Looping

Stream<T> peek(Consumer<? super T> action) { ... }

peek 메서드는 모든 데이터를 대상으로 특정 연산을 진행한다. forEach와 기능은 같지만, forEach는 최종 연산자이고 peek 은 중간연산자라는 차이가 있다. 지연 처리로 인해 최종 연산자를 호출하지 않으면 peek 메서드가 실행되지 않는다.

기본 자료형 값의 반환을 고려해, IntStream, DoubleStream, LongStream에서도  peek 메서드를 제공한다.

 

 

최종 연산자(Terminal Operation)

Looping

void forEach(Consumer<? super T> action) { ... }

forEach 메서드는 모든 데이터를 대상으로 특정 연산을 진행한다. 매개변수 형으로 Consumer<T>를 받는다. 내부적으로 스트림 데이터를 하나씩 인자로 전달하면서 accept 메서드를 호출한다. 

 

Calculating

int sum()
double sum()
long sum()

OptionalDouble average()
long count()
Optional<T> min(Comparator<? super T> comparator)
Optional<T> max(Comparator<? super T> comparator)

데이터의 합, 평균, 최대, 최소, 갯수 등을 계산한다. sum() 과 average() 는 수와 관련된 연산이므로 IntStream, DoubleStream, LongStream 을 참조하는 스트림을 대상으로만 사용할 수 있다.

 IntStream, DoubleStream, LongStream 이 아닌 다른 형을 참조하는 스트림에서는 min(), max() 의 인자로 Comparator<T> 를 사용하여 값을 얻을 수 있다.

 

Reduction

T reduce(T identity, BinaryOperator<T> accumulator) { ... }

reduce 메서드는 입력받은 연산을 사용하여 데이터를 축소하는 연산을 한다. 매개변수 형으로 BinaryOperator<T>를 받는다. 스트림에 data1, data2, data3, data4가 있다고 가정하자. reduce 는 내부적으로 data1과 data2를 apply 하여 resul1을 얻고, result1과 data3을 apply 하여 result2를 얻고, result2와 data4를 apply 하여 result3을 얻는다. 최종적으로 연산된 result3이 reduce가 반환하는 값이다. identify는 기본값으로 간주되어 스트림이 빈 경우 없을 때 반환되거나, 스트림이 비어있지 않으면 첫번째 데이터로 간주된다. 

List<String> list = Arrays.asList("Box", "Simple", "Complex", "Robot");

BinaryOperator<String> lc = (s1, s2) -> {
    if(s1.length() > s2.length())
        return s1;
    else
        return s2;
};
String str = list.stream().reduce("", lc);	// Complex

위으 코드를 보면 accumulator에 참조변수 lc가 들어간다. lc는 s1 과 s2의 길이를 비교하여 더 긴 값을 반환하는 식이므로,  스트림 데이터를 비교하며 가장 길이가 긴 데이터를 반환한다. 스트림이 비어있지 않으므로 "" 값이 스트림의 첫 번째 데이터로 간주된다. 

 

Matching

boolean anyMatch(Predicate<? super T> predicate) { ... }
boolean allMatch(Predicate<? super T> predicate) { ... }
boolean noneMatch(Predicate<? super T> predicate) { ... }

중간 연산을 통해 생성된 스트림에 대해 어떠한 조건을 만족하는 지 확인하는 기능을 한다. anyMatch 메서드는 데이터가 모두 조건을 만족하는지, allMatch 메서드는 데이터가 조건을 하나라도 만족하는지, noneMatch 메서드는 데이터가 모두 조건을 만족하지 않는지 확인한다. 매개변수 형으로 Predicate<T> 를 받는다.

기본 자료형 값의 반환을 고려해, IntStream, DoubleStream, LongStream에서도  위의 메서드들을 제공한다.

 

Finding

Optional<T> findFirst() { ... }
Optional<T> findAny() { ... }

스트림에서 데이터를 찾아온다. findFirst 메서드는 스트림에서 첫 번째 데이터를 반환하고, findAny 메서드는 임의의 데이터를 반환한다. 만약 스트림이 비어있다면 Optional.empty 를 반환한다.

 

Collecting

<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner) { ... }

collect 메서드는 스트림에 대하여 mutable reduction 을 수행한다. mutable reduction 이란 ArrayList 같은 mutable result container 를 생성하여 데이터를 통합하는 action 이다.

supplier에는 mutable result container 를 생성하는 람다식이 들어간다. accumulator에는 데이터를 mutable result container로 넣는 람다식이 들어간다. combiner 는 순차 스트림의 경우 사용되지 않지만, 병렬 스트림인 경우 나뉘어진 result container를 합치는데 사용되는 람다식이 들어간다. 

String[] words = { "Hello", "Box", "Robot", "Toy" }

List<String> list = Arrays.stream(words).collect(() -> new ArrayList<>(),
                                                 (c, s) -> c.add(s)
                                                 (lst1, lst2) -> lst1.addAll(lst2));

위의 코드를 보면, list 의 경우 () -> new ArrayList<>() 로 container 를 생성한 후, (c, s) -> c.add(s) 를 통해 데이터를 container에 저장한다. 순차 스트림(Sequential Stream)이므로, (lst1, lst2) -> lst1.addAll(lst2) 은 사용되지 않는다. 만약 병렬 스트림이였다면 () -> new ArrayList<>() 로 여러 container가 생긴 후, (c, s) -> c.add(s) 를 통해 데이터가 container 에 나뉘어 저장된 후, 컨테이너를 하나로 합칠 때 (lst1, lst2) -> lst1.addAll(lst2) 이 사용된다.

위와 같은 collect는 구현해야 할 인자가 많아 번거롭다. Collector 형을 인자로 받는 collect 메서드를 사용하면 좀 더 편리하게 사용할 수 있다.

 

<R,A> R collect(Collector<? super T,A,R> collector) { ... }

Collector 형 매개변수를 받아 처리하는 collect 메서드이다. 자주 사용하는 작업은 Collections 클래스에서 정적 메서드로 제공한다. Collectors 클래스 내의 메서드들은 데이터들을 모아 collection으로 만들어주거나, 특정 기준을 통해 데이터를 요약하는 등의 mutable reduction 을 수행한다.

 

// List<Student> students
List<String> list = students.stream().map(Student::getName).collect(Collectors.toList);
Set<String> set = students.stream().map(Student::getName).collect(Collectors.toSet);
Map<String, Student> map = students.stream()
                                   .collect(Collectors.toMap(Studnet::getName, Function.identity())));
LinkedList<Student> linkedlist = students.stream().collect(Collectors.toCollection(LinkedList::new));

toList, toSet, toMap, toCollection 은 스트림의 데이터를 사용하여 컬렉션을 만든다. toList 와 toSet 은 매개변수가 필요 없다. toMap 은 매개변수로 key 매핑 함수와 value 매핑 함수를 가지고,  toCollection 은 매개변수로 Collection 생성 함수를 가진다.

참고로 Function.identity() 메서드는 자기 자신을 반환하는 메서드이다. 위의 코드에서는 Student 를 반환한다.

 

// List<Student> students
int sum = students.stream().collect(Collectors.summingInt(Student::getAge));
dobule avg = students.stream().collect(Collectors.averagingInt(Student::getAge));
int count = students.stream().collect(Collectors.counting());
Optional<Student> topStudent = students.stream().collect(Collectors.maxBy(studentComparator));
IntSummaryStatistics statistic = students.stream().collect(Collectors.summarizingInt(Student::getAge));

summingInt, summingDouble, summingLong 메서드는 mapper 를 사용해 데이터로부터 값을 뽑아 그 합을 구한다.

averagingInt, averagingDouble, averagingLong 메서드는  mapper 를 사용해 데이터로부터 값을 뽑아 그 평균을 구한다.

counting 메서드는 스트림 내의 데이터 갯수를 구한다.

maxBy, minBy 메서드는 comparator 를 사용해 스트림 내의 최대/최소 데이터를 구한다.

summarizingInt, summarizingDouble, summarizingLong 메서드는 mapper를 사용해 데이터로부터 값을 뽑아 int/double/longSummaryStatics 객체를 반환한다. int/double/longSummaryStatics 객체에는 갯수, 합계, 평균, 최소, 최대 같은 통계 값들을 가지므로 이러한 값을 모두 사용할 때 유용하다.

 

String join1 = students.stream().map(Student::getName)
                                .collect(Collectors.joining()));	         // jamelisajohn
String join1 = students.stream().map(Student::getName)
                                .collect(Collectors.joining(", ", "<", ">")));	// <james, lisa, john>

joining 메서드는 문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환한다. 매개변수를 받는 경우 구분자, 접두사, 점미사 순서로 매개변수를 받는다. 

 

Map<Integer, List<Student>> map = students.stream().collect(Collectors.groupingBy(Student::getAge));
// {17: [...], 18: [...], 19 : [...]}

groupingBy 메서드는 특정 조건으로 데이터를 그룹짓는다. 매개변수 형으로 Function<T, K> 를 받는다. 

위의 코드의 경우 Student 의 age를 기준으로 그룹화했다. 같은 age를 가진 Student 끼리 List 로 묶인 Map 을반환한다.

 

Map<Integer, List<Student>> map = students.stream().collect(Collectors.partitioningBy(s -> s.getAge() > 18));
// {true : [...], false : [...]}

partitioningBy 메서드는 특정 조건으로 데이터를 그룹짓는다. groupBy 와 달리 매개변수 형으로 Predicate<T> 를 받는다.

위의 코드의 경우 Student의 age 가 18 초과인지를 기준으로 그룹화했다. 해당 조건에 대해 true와 false를 반환하여, 같은 boolean 값을 가진 Student 끼리 List 로 묶인 Map 을 반환한다.

 

참조
윤성우의 열혈 JAVA 프로그래밍
https://docs.oracle.com/javase/9/docs/api/index.html?overview-summary.html
https://futurecreator.github.io/2018/08/26/java-8-streams/
https://jamie95.tistory.com/111

 

'JAVA > Java' 카테고리의 다른 글

[Java] I/O 스트림  (0) 2022.09.29
[Java] 시각과 날짜의 처리  (1) 2022.09.24
[Java] Optional 클래스  (0) 2022.09.15
[Java] 메서드 참조(Method Reference)  (0) 2022.09.15
[Java] 기본 함수형 인터페이스(Functional Interface)  (0) 2022.09.13

댓글