본문 바로가기
JAVA/기본

스트림(Stream)

by 히포파타마스 2021. 12. 8.

스트림(Steam)

보통 많은 수의 데이터를 다룰 때는 컬렉션이나 배열 그리고 for문과 Iterator를 이용해서 코드를 작성한다.

그러나 이러한 방식으로 작성된 코드는 너무 길고 알아보기 어려우며 재사용성도 떨어진다.

 

또 다른 문제는 데이터 소스마다 다른 방식으로 다뤄야 한다는 것이다.

Collection이나 Iterator과 같은 인터페이스를 이용해서 컬렉션을 다루는 방식을 표준화하기는 했지만, 각 컬렉션 클래스에는 같은 기능의 메서드들이 중복해서 정의되어 있다.

 

이런 문제점들을 해결하기 위해서 나온 것이 '스트림(Stream)' 이다.

스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았다.

 

스트림을 사용하면, 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.

 

[스트림 사용 예]

//strList = List,  strArr = Array
strList.stream().sorted().forEach(System.out::println);
Arrays.stream(strArr).sorted().forEach(System.out::println);

위 예시는 각각 리스트와 배열의 요소를 출력하는 예제이다.

 

리스트와 배열 모두 stream()을 통해 스트림으로 변환된 뒤 같은 메서드가 사용되었다.

이처럼 다른 두 데이터 소스로 같은 작업을 할 때, 스트림을 사용하면 통일된 방식을 적용할 수 있다.

 

 

스트림은 다음과 같은 특징을 갖고 있다.

● 스트림은 데이터 소스를 변경하지 않는다.

◎ 스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐, 데이터 소스를 변경하지 않는다.

● 스트림은 일회용이다.

◎ 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없다.

●스트림은 작업을 내부 반복으로 처리한다.

 

 

 

■ 스트림의 연산

스트림은 다양한 연산을 이용해서 복잡한 작업들을 간단히 처리할 수 있다.

 

스트림이 제공하는 연산은 중간 연산과 최종 연산으로 분류할 수 있는데, 중간 연산은 연산 결과를 스트림으로 반환하기 때문에 계속해서 연결할 수 있다.

반면 최종 연산은 스트림의 요소를 모두 소모하면서 연산을 수행하므로 단 한 번만 연산이 가능하다.

 

스트림에 정의된 연산은 다음과 같다.

 

[스트림 연산_중간 연산]

 

 

[스트림 연산_최종 연산]

 

 

□ 지연된 연산

스트림은 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다.

중간 연산은 호출하는 것은 단지 어떤 작업이 수행되어야 하는지를 지정해주는 것일 뿐이다.

최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다.

 

 

□ Stream<Integer>와 IntStream

요소의 타입이 T인 스트림은 기본적으로  Stream<T>이기 때문에 상황에 따라, 오토박싱&언박싱으로 인한 비효율이 발생한다.

이런 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림, IntStream, LongStream, DoubleStream이 제공된다.

이 스트림들은 기본형 값으로 작업할 때 더 효율적이며, 기본형 값을 다루는데 유용한 메서드들이 포함되어있다.

 

 

■ 병렬 스트림

스트림에 parallel()이라는 메서드를 호출해서 병렬로 연산을 수행할 수 있다.

 

[parallel() 사용 예]

int sum = strStream.parallelStream()	//strStream을 병렬 스트림으로 전환
				.mapToInt(String::length())
				.sum();

parallelStream()을 사용하면 데이터 소스를 병렬 스트림으로 전환해준다.

 

 

 

 

1. 스트림 생성

□ 컬렉션

컬렉션의 최고 조상인 Collection에 stream()이 정의되어 있다.

때문에 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 stream()으로 스트림을 생성할 수 있다.

 

[스트림 생성_Collection]

Stream<T> Collection.stream()

 

 

□ 배열

배열을 소스로 하는 스트림을 생성하는 메서드는 다음과 같이 Stream과 Arrays에 static 메서드로 정의되어 있다.

 

[스트림 생성_배열]

// Stream<T> Stream.of(T...values), 가변 인자
Stream<String> strStream = Stream.of("a", "b", "c");

// Stream<T> Stream.of(T[])
Stream<String> strStream = Stream.of(new String[]{"a", "b", "c"});

// Stream<T> Arrays.stream(T[])
Stream<String> strStream = Arrys.stream(new String[]{"a", "b", "c"});

// Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)
Stream<String> strStream = Arrys.stream(new String[]{"a", "b", "c"}, 0, 3);

 

int, long, double과 같은 기본형 배열을 소스로 하는 스트림을 생성하는 메서드도 있다.

 

[기본형 스트림 생성_배열]

IntStream IntStream.of(int... values)
IntStream IntStream.of(int[])
IntStream Arrays.stream(int[])
IntStream Arrays.stream(int[] array, int startInclusive, int endExclusive)

long과 double타입의 배열들도 위의 예시와 같은 패턴으로 스트림을 생성할 수 있다.

 

 

□ 특정 범위의 정수

IntStream과 LongStream은 다음과 같이 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()와 rangeClosed()를 가지고 있다.

 

[range(), rangeClosed()]

IntStream intStream.range(int begin, int end)
IntStream intStream.rangeClosed(int begin, int end)

range()의 경우 경계의 끝인 end가 범위에 포함되지 않고, rangeClosed()는 포함된다(두 메서드 모두 초기값은 포함한다).

 

 

□ 임의의 수

난수를 생성하는 데 사용되는 Random 클래스의 ints(), longs(), doubles() 메서드들은 해당 타입의 난수들로 이루어진 스트림을 반환한다.

 

이 메서드들이 반환하는 스트림은 크기가 정해지지 않은 '무한 스트림' 이므로 limit()도 같이 사용해서 스트림의 크기를 제한해 주어야 한다.

 

[스트림 생성_임의의 수]

IntStream intStream = new Random().ints() // 무한 스트림
IntStream intStream = new Random().ints().limits(5) // 요소를 5개로 제한한다.
IntStream intStream = new Random().ints(5) // 요소를 5개로 제한한다.

limits()를 사용하지 않고 매개변수를 주어 스트림의 크기를 지정해줄 수 있다.

 

이 메서드들로 생성되는 난수는 다음과 같은 범위를 갖는다.

 

[난수 범위]

 

매개변수로 난수의 범위를 지정해 줄 수 있으며 동시에 요소의 개수도 제한할 수 있다.

 

[스트림 생성_임의의 수 2]

IntStream ints(int begin, int end)
LongStream longs(long begin, long end)
DoubleStream double(double begin, double end)

IntStream ints(long streamSize, int begin, int end)
LongStream longs(long streamSize, long begin, long end)
DoubleStream double(long streamSize, double begin, double end)

 

 

□ 람다식 - iterate(), generate()

Stream 클래스의 iterate()와 generate()는 람다식을 매개변수로 받아서, 이 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.

 

[iterate(), generate()]

// static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
Stream<Integer> evenStream = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6 ... (무한 스트림)


// static <T> Stream<T> generate(Supplier<T> s)
Stream<Double> randomStream = Stream.generate(Math::random);

iterate()는 seed로 지정된 값부터 시작해서 람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복한다.

 

generate()는 iterate()와 달리 이전 결과를 이용해서 다음 요소를 계산하지 않는다.

 

 

iterate()와 generate()에 의해 생성된 스트림은 기본형 스트림 타입의 참조 변수(IntStream 등)로 다룰 수 없다.

굳이 필요하다면 mapToInt()와 같은 메서드로 변환을 해야 한다.

 

 

□ 파일

java.nio.file.Files의 list()는 지정된 디렉터리에 있는 파일의 목록을 소스로 하는 스트림을 생성해서 반환한다.

 

[Files.list()]

Stream<Path> Files.list(Path dir)

 

BufferedReader 클래스의 lines() 메서드는 파일의 한 행(line)을 요소로 스트림을 생성한다.

 

[BufferedReader_lines()]

Stream<String> lines()

 

 

 

□ 기타

요소가 하나도 없는 비어있는 스트림을 생성할 수 도 있다.

 

[빈 스트림 생성]

Stream emptyStream = Stream.empty(); // 빈 스트림을 생성해서 반환

 

 

Stream의 static 메서드인 concat()은 요소의 타입이 같은 뚜 스트림을 하나로 연결할 수 있다.

 

[concat()]

Stream<String> strs3 = Stream.concat(strs1, str2s) // 요소가 String인 두 스트림을 연결

 

 

 

 

2. 스트림의 중간 연산

□ 스트림 자르기_skip(), limit()

skip()은 매개변수로 받은 값만큼 요소를 건너뛰고, limit()는 매개변수로 받은 값만큼 요소를 제한한다.

 

[skip(), limit()]

Stream<T> skip(long n)
Stream<T> limit(long maxSize)

// 기본형
IntStream skip(long n)
IntStream limit(long maxSize)

기본형 스트림의 skip()과 limit()는 반환 타입이 기본형 스트림이다.

 

 

□ 스트림 요소 걸러내기_filter(), distict()

distict()는 스트림에서 중복된 요소들을 제거하고, filter()는 주어지 조건(Predicate)에 맞지 않는 요소를 걸러낸다.

 

[filter(), distict()]

// Stream<T> filter(Predicate<? super T> predicate)
IntStream intStream1 = Intstream.rangeClosed(1, 10)
intStream.filter(i -> i % 2 == 0).forEach(System.out::print); //246810

// Stream<T> distinct()
IntStream intStream2 = IntStream.of(1, 2, 2, 3, 3, 3, 4, 5, 6)
intStream2.distinct().forEach(System.out::print); // 123456

 필요하다면 filter()를 다른 조건으로 여러 번 사용할 수도 있다.

 

 

□ 정렬_sorted()

스트림을 정렬할 때는 sorted()를 사용하면 된다.

 

[sorted()]

Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)

sorted()에 Comparator를 지정하지 않으면 스트림 요소의 기본 정렬 기준(Comparable)으로 정렬한다.

단, 스트림의 요소가 Comparable을 구현한 클래스여야 한다.

 

다음은 String을 요소로 갖는 스트림을 다양한 방법으로 정렬한 것이다.

 

[sorted() 사용 예]

위 예시처럼 Comparator 인터페이스에 static 메서드와 디폴트 메서드를 사용하면 정렬의 보다 쉬워진다.

 

정렬에 사용되는 메서드의 개수는 많지만, 가장 기본적인 메서드는 comparing()이다.

 

[comparing()]

comparing(Function<T, U> keyExtractor)
comaparing(Function<T, U> keyExtractor, Comparator<U> keyComparator)

스트림의 요소가 Comparable을 구현한 경우, 매개변수 하나짜리를 사용하면 되고 그렇지 않다면, 추가적인 매개변수로 정렬기준(Comparator)을 따로 지정해 줘야 한다.

 

 

비교대상이 기본형인 경우, comparing() 대신 위 예시의 comparingInt()와 같음 메서드를 사용하면 오토박싱과 언박싱 과정이 없어서 더 효율적이다.

 

 

정렬 조건을 추가할 때는 thenComaring()을 사용하면 된다.

예를 들어 학생 스트림(studentStream)을 반(ban) 별, 성적(totalScore) 순, 그리고 이름(name) 순으로 정렬하여 출력하려면 다음과 같이 하면 된다.

 

[thenComparing() 예]

studentStream.sorted(Comparator.comparing(Student::getBan)
				.thenComparing(Student::getTotalScore)
				.thenCompareing(Student::getName))
				.forEach(System.out::println);

 

 

□ 변환_map()

스트림의 요소를 특정 형태로 변환해야 할 때는 map()을 사용하면 된다.

 

[map()]

Stream<R> map(Function<? super T, ? extend R> mapper)

매개변수로 T타입을 R타입으로 변환해서 반환하는 함수를 매개변수로 받는다.

 

예를 들어 File의 스트림에서 파일의 확장자만을 뽑은 다음 중복을 제거해서 출력하려면 다음과 같이 map() 사용하면 된다.

 

[map() 사용 예]

fileStream.map(File::getNAme)
		.filter(s -> s.indexOf('.') != -1) // 확장자 없으면 제외
		.map(s -> s.substring(s.indexOf('.') + 1)) // 확장자 추출
		.map(String::toUpperCase)
		.distinct()
		.forEach(System.out::print);

 

 

map()은 Stream<T> 타입의 스트림을 반환하는데, 스트림의 요소를 숫자로 변환하는 경우 IntStream과 같은 기본형 스트림으로 변환하는 것이 더 유용할 수 있다.

이 경우 기존 스트림을 다음과 같은 메서드들을 사용해서 기본형 스트림으로 변환할 수 있다.

 

[기본형 변환 map()]

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

count()만 지원하는 Stream<T>와 달리 IntStream과 같은 기본형 스트림은 sum, average, max, min과 같은 숫자를 다루는데 편리한 메서드들을 제공한다.

단, 이런 통계 메서드들은 최종 연산이기 때문에 연속해서 사용할 수 없다.

예를 들어, sum()과 average()등을 동시에 호출해야 할 때 스트림을 또 생성해야 된다.

 

이러한 불편함을 해결하기 위해 기본형 스트림은  summaryStatistics() 라는 메서드를 따로 제공한다.

 

[summaryStatics() 사용 예]

IntSummaryStatics stat = scoreStream.summaryStatistics();
long totalCount = stat.getCount();
long totalScore = stat.getSum();
double avgScore = stat.getAverage();
int minScore = stat.getMin();
int maxScore = stat.getMax();

summaryStatistics()으로 생성된 IntSummaryStatistics 타입의 stat은 위와 같이 다양한 종류의 메서드를 제공한다.

 

 

반대로 기본형 스트림을 Stream<T>로 변환할 때는 mapToObj()를, Stream<Interger>와 같은 wrapper 클래스를 요소로 하는 스트림으로 변환할 때는  boxed()를 사용한다.

 

 

□ 조회_peek()

연산이 올바르게 처리되었는지 연산 중간에 확인하고 싶다면, peek()를 사용하면 된다.

peek()는 forEach()와 비슷하게 모든 요소를 조회하지만 매개변수로 Consumer<T>를 받기 때문에 요소를 변경할 수는 없다.

 

대신 요소를 소모해서 연산을 끝내는 forEach()와는 달리 요소를 소모하지 않기 때문에 연산 사이에 여러 번 끼워 넣어도 문제가 되지 않는다.

 

 

□ faltMap()

스트림의 요소가 배열이거나 map()의 연산 결과가 배열인 경우, 즉 스트림의 타입이 Stream<T[]>인 경우, Stream<T>로 다루는 것이 더 편리할 때가 있다. 이럴 때는  map() 대신 flatMap()을 사용하면 된다.

 

예를 들어, 요소가 문자열 배열인 스트림(Stream<String[]>)이 있을 때, 각 문자열이 요소인 스트림, 즉 Stream<String>으로 만들려면 각 배열을 합쳐서 추출하고 다시 스트림으로 만들고 아주 번거로운 작업이 될 것이다.

 

바로 이럴 때는 다음과 같이 flatMap()을 써서 문제를 해결할 수 있다.

 

[flatMap() 사용 예]

Stream<String> strStream = strArrStrm.flatMap(Arrays::stream);

flatMap()을 사용하면 변환된 요소들을 요소로 하는 스트림을 반환한다.

 

 

[flatMap()]

Stream<R> flatMap(Function<? super T, ? extend Stream<R>)

flatMap()은 Stream<R>을 반환하는 람다식을 매개변수로 받는다.

때문에 flatMap() 들어가는 람다식은 반드시 Stream을 반환해야 한다.

 

 

 

 

3. 스트림의 최종 연산

최종 연산은 스트림의 요소를 소모해서 결과를 만들어내기 때문에 최종 연산 후에는 스트림을 더 이상 사용할 수 없다.

최종 연산의 결과는 스트림 요소의 합과 같은 단일 값이거나, 스트림의 요소가 담긴 배열 또는 컬렉션일 수 있다.

 

 

□ forEach()

 

[forEach()]

void forEach(Consumer<? super T> action)

forEach()는 각 스트림의 요소에 매개변수로 받은 람다식을 적용한다.

반환 타입이 void이므로 스트림의 요소를 출력하는 용도로 많이 사용된다.

 

 

□ 조건 검사 - allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()

스트림의 요소에 대해 지정된 조건에 모든 요소가 일치하는지, 일부가 일치하는지 등을 확인하는 데 사용할 수 있는 메서드들이다.

 

[allMatch(), anyMatch(), noneMatch 사용]

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

위 예의 메서드들은 모두 매개변수로 Predicate를 요구하며, 연산 결과로 boolean을 반환한다.

 

 

[anyMatch() 사용 예]

boolean noFailed = stuStream.anyMatch(s -> s.getTotalScore() <= 100)

stuStream이 학생 성적 정보 스트림이라고 할 때 총점 100점 이하의 학생이 '있는지' 확인하려면 위의 예제처럼 anyMatch()를 사용하면 된다.

 

 

findFirst()는 스트림의 요소 중 조건에 일치하는 첫 번째 값을 반환한다.

병렬 스트림인 경우에는 findAny()를 사용하면 된다.

 

 

□ 통계 - count(), sum(), average(), max(), min()

기본형 스트림이 아닌 일반 스트림의 경우 통계와 관련된 메서드는 다음과 같다.

 

[통계 메서드]

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

대부분의 경우 위의 메서드를 사용하기보다 기본형 스트림으로 변환하거나, 아니면 reduce()와 collect()를 사용해서 통계 정보를 얻는다.

 

기본형 스트림의 경우 위 예시의 메서드 외에 sum(), average() 등의 추가적인 통계 메서드들이 있다.

 

 

□ reduce()

reduce()는 스트림의 요소를 줄여나가면서 연산을 수행하고 최종 결과를 반환한다.

처음 두 요소를 가지고 연산한 결과를 가지고 그다음 요소와 연산하기 때문에 매개변수의 타입이 BinaryOperator<T>이다.

 

 [reduce()]

Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
U reduce(U identity, BiFunction<U,T,U> accumulator, BinaryOperator<U> combiner)

reduce()는 초기값(identity)를 받을 수 있으며, 이 경우 초기값과 스트림의 첫 번째 요소로 연산을 시작한다.

스트림의 요소가 하나도 없는 경우, 초기값이 반환되므로 반환 타입은 Optional<T>가 아니라 T이다.

 

위 예시의 세 번째 메서드의 마지막 매개변수인 combiner은 병렬 스트림에 의해 처리된 결과를 합칠 때 사용하기 위한 것이다.

 

스트림의 통계 메서드인 count()와 sum() 등은 내부적으로 모두 reduce()를 이용해서 작성되었다.

 

[count(), sum(), max() - reduce()로 구현]

int count = intStream.reduce(0, (a,b) -> a + 1);
int sum = intStream.reduce(0, (a,b) -> a + b);
int max = intStream.reduce((a,b) -> a > b ? a : b);

 

 

 

 

4. collect()

collect()는 스트림의 요소를 수집하는 최종 연산이다.

collect()는 주어진 매개변수로 스트림의 요소를 수집하는데 이 스트림을 수집하는 방법을 정의한 것이 바로 컬렉터(collector)이다.

 

Collectors는 Collector인터페이스를 구현한 것으로, 직접 구현할 수도 있고 미리 작성된 것을 이용할 수도 있다.

 

[collect(), Collector, Collectors]

 

collect()는 매개변수의 타입이 Collector인데, 매개변수가 Collector를 구현한 클래스의 객체여야 한다는 뜻이다.

collect()는 매개변수로 받은 Collector를 구현한 객체에 정의된 방법대로 스트림의 요소를 수집한다.

 

[collect()]

Object collect(Collector collector)
Object collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)

매개변수가 3개인 collect()는 Collector의 인터페이스를 구현하지 않고 간단히 람다식으로 수집할 때 사용된다.

 

 

□ 스트림을 컬렉션과 배열로 반환 - toList(), toSet(), toMap(), toCollection(), toArray()

스트림의 모든 요소를 컬렉션에 수집하려면, Collectors클래스의 toList()와 같은 메서드를 사용하면 된다.

List나 Set이 아닌 특정 컬렉션을 지정하려면, toCollection()에 해당 컬렉션의 생성자 참조를 매개변수로 넣어주면 된다.

 

[toList(), toCollection()]

List<String names = stuStream.map(Student::getName)
					.collect(Collectors.toList())

ArrayList<String> list = names.stream()
					.collect(COllectors.toCollection(ArrayList::new));

학생 정보 스트림인 stuStream을 이용해서 학생의 이름을 List와 ArrayList로 수집하려면 위의 예시처럼 toList()와 toCollection()을 사용하면 된다.

 

 

[toMap()]

Map<String, Stirng> map = personStream
					.collect(Collectors.toMap(p -> p.getRegId(), p -> p));

Map은 키와 값의 쌍으로 저장해야 하므로 객체의 어떤 필드를 키로 사용할지와 값으로 사용할지를 지정해줘야 한다.

 

위 예시는 개인 정보의 스트림인 personStream으로 키값이 주민번호(regId), 개인 객체를 값으로 하는 Map으로 스트림을 수집한다.

 

 

스트림에 저장된 요소들을 'T[]' 타입의 배열로 변환하려면, toArray()를 사용하면 된다.

 

[toArray()]

student[] stuNames = studentStream.toArray(Student[]::new);
Object[] stuNames = studentStream.toArray();

해당 타입의 생성자 참조를 매개변수로 지정해주어야 하며, 매개변수를 지정하지 않으면 반환되는 배열의 타입은 'Object[]' 이다.

 

 

□ 통계 - counting(), summinInt(), averaginInt(), maxBy(), minBy()

스트림의 통계 메서드들이 제공하는 통계 정보를 collect()로 똑같이 얻을 수 있다.

 

[통계 메서드 - collcet()]

long count = stuStream.collect(counting());
long totalScore = stuStream.collect(summingInt(Student::getTotalScore));
Optional<Student> topStudent = stuStream
				.collect(maxBy(Comparator.comparingInt(Student::getTotalScore)));

Collectors의 static메서드를 호출할 때는 'Collectors.'를 생략하였다.

 

 

□ 리듀싱 - reducing()

리듀싱 역시 collect()로 가능하다.

IntStream에는 매개변수 3개짜리 collect()만 정의되어 있으므로 boxed()를 통해 IntStream을 Stream<Integer>로 변환해야 매개변수 1개짜리 collect()를 쓸 수 있다.

 

[reducing()]

Collector reducing(BinaryOperator<T> op)
Collector reducing(T identity, BinaryOperator<T> op)
Collector reducing(U identity, Function<T, U> mapper, BinaryOperator<U> op)

위 예시에서  세 번째 메서드만 제외하고 reduce()와 쓰임새가 같다.

세 번째 것은 map() reduce()를 하나로 합쳐놓은 것이다.

 

 

□ 문자 결합 - joining()

문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환한다.

구분자를 지정해줄 수 있고, 접두사와 접미사도 지정 가능하다.

 

[joining()]

String studentNames = stuStream.map(Student::getName).collect(joining(",", "[", "]");

위 예시는 학생들의 이름을 구분자 "," 로 합치고 접두사로 "[", 접미사로 "]" 를 붙여준다.

 

 

□ 그룹화와 분할 - groupingBy(), partitioningBy()

그룹화는 스트림의 요소를 특정 기준으로 그룹화하는 것을 의미하고, 분할은 스트림의 요소를 두 가지, 지정된 조건에 일치하는 그룹과 일치하지 않는 그룹으로 분할하는 것을 의미한다.

 

[groupingBy(), partitionBy()]

Collector groupingBy(Fuction classifier)
Collector groupingBy(Fuction classifier, Collector downstream)
Collector groupingBy(Fuction classifier, Supplier mapFactory, Collector downstream)

Collector partitioningBy(Predicate predicate)
Collector partitioningBy(Predicate predicate, Collector downstream)

groupingBy()는 스트림의 요소를 Fuction으로, partitioningBy()는 Predicate로 분류한다.

 

 

Student라는 클래스가 학생들의 이름, 성별, 학년, 반, 점수라는 필드를 갖고 있다고 하자, 이 경우 groupingBy()와 paritionBy()를 이용해서 다양한 조건으로 스트림을 분할할 수 있다.

 

[partitioning() 사용 예]

//성별로 분할
Map<Boolean, List<Student>> stuBySex = stuStream
				.collect(partitioningBy(Student::isMale));

//성별로 분할한 뒤, 카운팅 적용
Map<Boolean, Long> stuNumBySex = stuNumStream
				.collect(partitioningBy(Student::isMail, counting()));


//성별로 분리, 성적 1등 추출
Map<Boolean, Optional<Student>> topScoreBySex = stuStream
				.collect(
					partitioningBy(student::isMale,
						maxBy(comparingInt(Student::getScore))
					)
				);

//collectionAndThen으로 Optional 제거
Map<Boolean, Optional<Student>> topScoreBySex = stuStream
				.collect(
					partitioningBy(student::isMale,
						collectionAndThen(
							maxBy(comparingInt(Student::getScore)), Optional::get
						)
					)
				);

maxBy()의 결괏값은 Optional인데 바로 Optional을 풀어주려면 collectionAndThen()을 사용하면 된다.

 

 

[groupingBy() 예]

//반으로 분류
Map<Integer, List<Student>> stuByBan = stuStream
				.collect(groupingBy(Student::getBan, toList())); //toList() 생략 가능
                

//성적 등급으로 분류 후, 카운팅 적용
Map<Student.Level, Long> stuByLevel = stuStream
				.collect(groupingBy(s -> {
					if(s.getScore() >= 200 return Student.Level.HIGH;
					else if(s.getScore() >= 100) return Student.Level.MID;
					else return Student.Level.Low;
					}, counting())
				);

 

groupingBy()를 여러 번 사용하면, 다수 준 그룹화도 가능하다.

 

 

 

 

5. 스트림의 변환

스트림 간의 변환은 다음과 같이 정리할 수 있다.

 

[스트림의 변환 1]

 

 

[스트림의 변환 2]

'JAVA > 기본' 카테고리의 다른 글

Comparator 구현  (0) 2022.08.11
자료구조 초기화, List-Array 변환  (0) 2022.07.15
Optional  (0) 2021.12.03
람다(Lambda)  (0) 2021.11.28
예외 처리(exception handling)  (0) 2021.08.02

댓글