스트림이 제공하는 다양한 연산을 이용하면 복잡한 작업들을 간단히 처리할 수 있다.
마치 데이터베이스에 SELECT문으로 쿼리문을 작성하는 것과 같은 느낌이다.
※ 스트림에 정의된 메서드 중에서 데이터 소스를 다루는 작업을 수행하는 것을 연산(operation)이라고 한다.
스트림이 제공하는 연산은 중간 연산, 최종 연산 2가지이다.
중간 연산
연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산할 수 있음
최종 연산
연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능
stream.distinct().limit(5).sorted().forEach(System.out::println)
// 중간연산 중간연산 중간연산 최종연산
모든 중간 연산의 결과는 스트림이지만, 연산 전의 스트림과 같은 것은 아니다.
중간 연산은 map()과 flatMap()이 핵심이다. 나머지는 이해하기 쉽고 사용법도 간단하다.
skip()과 limit()은 스트림의 일부를 잘라낼 때 사용한다. skip(i)는 처음 i개의 요소를 건너뛰고 limit(i)는 스트림의 요소를 i개로 제한한다.
Stream<T> skip(long n)
Stream<T> limit(long maxSize)
기본형 스트림에도 skip()과 limit()이 있다. 반환 타입이 기본형 스트림이라는 점만 다를 뿐이다.
IntStream skip(long n)
IntStream limit(long maxSize)
아래와 같은 코드를 작성하면 1 ~ 10을 연속적으로 가진 intStream의 첫 3개 요소는 건너뛰고 그 뒤로 5개만 출력한다.
IntStream intStream = IntStream.rangeClosed(1, 10); // 1 ~ 10 연속으로 가진 스트림
intStream.skip(3).limit(5).forEach(System.out::print); // 45678
distinct()는 스트림에서 중복된 요소를 제거하고, filter()는 주어진 조건(Predicate)에 맞지 않는 요소를 걸러낸다
Stream<T> filter(Predicate<? super T> predicate)
Stream<T> distinct()
distinct()의 사용법은 간단하다.
IntStream intStream = IntStream.of(1, 2, 2, 3, 3, 4, 4, 5, 5, 6);
intStream.distinct().forEach(System.out::println);
filter()는 매개변수로 Predicate를 활용한다. 아래와 같이 연산결과가 boolean인 람다식을 사용해도 된다.
IntStream intStream = IntStream.rangeClosed(1, 10); // 1~10
intStream.filter(i -> i % 2 == 0).forEach(System.out::print); // 246810
filter는 중간 연산이기에 필요하다면 아래와 같이 두 번 이상 사용해도 된다.
// 아래의 두 문장은 동일한 결과를 얻는다.
intStream.filter(i -> i%2!=0 && i%3!=0).forEach(System.out::print); // 157
intStream.filter(i -> i%2!=0).filter(i -> i%3!=0).forEach(System.out::print);
스트림을 정렬할 때는 sorted()를 사용하면 된다.
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)
sorted()는 지정된 Comparator로 스트림을 정렬하는데, Comparator대신 int값을 반환하는 람다식을 사용하는 것도 가능하다. Comparator를 지정하지 않는다면 스트림 요소의 기본 정렬 기준(Comparable)으로 정렬한다. 단, 스트림의 요소가 Comparable을 구현한 클래스가 아니라면 예외가 발생한다.
Stream<String> strStream = Stream.of("dd", "aaa", "CC", "cc", "b");
strStream.sorted().forEach(System.out::print); // CCaaabccdd
위의 코드는 따로 Comparator를 지정하지 않아 String의 Comparable을 통해 사전순으로 자동 정렬했다. Comparator를 지정하여 역순으로도 정렬 가능하다.
※ String.CASE_INSENSITIVE_ORDER는 String클래스에 정의된 Comparator이다.
JDK1.8이상부터 Comparator인터페이스에 static메서드와 디폴트 메서드가 많이 추가 되었는데, 이 메서드들을 이용하면 쉬운 정렬이 가능하다. 이 메서드들 모두 Comparator를 반환하고, 기본 메서드는 comparing()이다.
comparing(Function<T, U> keyExtractor)
comparing(Function<T, U> keyExtractor, Comparator<U> keyComparator)
스트림의 요소가 Comparable을 구현한 요소라면, 매개변수 하나짜리를 사용하면 되고 그렇지 않은 경우, 추가적인 매개변수로 정렬기준(Comparator)를 따로 지정해 주어야 한다.
KeyExtractor를 통해 U타입의 비교대상을 지정하는데, 이 때 U타입에 Comparable이 있다면 keyComparator를 지정할 필요가 없으나 Comparable구현이 안된 새로운 참조형등의 타입이면 Comparator를 만들거나 지정해 주어야 한다.
comparingInt(ToIntFunction<T> keyExtractor)
comparingLong(ToLongFunction<T> keyExtractor)
comparingDouble(ToDoubleFunction<T> keyExtractor)
비교 대상이 기본형이라면, comparing()대신에 위의 메서드를 사용하여 오토박싱과 언박싱 과정을 없애 효율적으로 사용할 수 있다. 정렬 조건을 추가하고 싶다면 thenComparing()을 사용하면된다.
thenComparing(Comparator<T> order)
thenComparing(Function<T, U> keyExtractor)
thenComparing(Function<T, U> keyExtractor, Comparator<U> keyComp)
import java.util.Comparator;
import java.util.stream.Stream;
public class StreamExample {
public static void main(String[] args) {
Stream<Student> studentStream = Stream.of(
new Student("이자바", 3, 300),
new Student("김자바", 1, 200),
new Student("안자바", 2, 100),
new Student("박자바", 2, 150),
new Student("소자바", 1, 200),
new Student("나자바", 3, 290),
new Student("감자바", 3, 180)
);
studentStream.sorted(Comparator.comparing(Student::getBan) // 반별 정렬
.thenComparing(Comparator.naturalOrder())) // 기본 정렬
.forEach(System.out::println);
}
}
class Student implements Comparable<Student> {
String name;
int ban;
int totalScore;
Student(String name, int ban, int totalScore) {
this.name = name;
this.ban = ban;
this.totalScore = totalScore;
}
public String toString() {
return String.format("[%s, %d, %d]", name, ban, totalScore);
}
String getName() {return name;}
int getBan() {return ban;}
int getTotalScore() {return totalScore;}
// 총점 내림차순을 기본 정렬로 한다.
public int compareTo(Student s) {
return s.totalScore - this.totalScore;
}
}
위 예제는 반 별 정렬할때 int값을 KeyExtractor로 가져온다. 정수형은 Comparable이 기본 구현되어 있기에, Comparator를 지정할 필요가 없고, 그 다음 기본 정렬할때 스트림의 요소가 Student인데 Student클래스를 보면 Comparable의 compareTo()를 오버라이딩 했기에 기본정렬이 가능했다. 만약 Student에 Comparable을 구현하지 않았다면 오류가 발생했을 것이다.
스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환해야 하는 경우 map()을 사용한다. 아래 선언부의 뜻은, 매개변수로 T타입을 R타입으로 반환하는 함수를 지정해야 한다.
Stream<R> map(Function<? super T,? extends R> mapper)
File스트림에서 파일의 이름만 뽑아서 출력할때 아래와 같이 map()을 사용한다.
Stream<File> fileStream = Stream.of(
new File("Ex1.java"),
new File("Ex1"), new File("Ex1.bak"),
new File("Ex1.txt"));
// Stream.of를 이용해 File객체 4개를 추가했다. 이 File 인스턴스들은 인스턴스 메서드인 getName()을
// 가지고 있는데, 이는 파일 이름을 String으로 반환하기에 String스트림이 만들어 진다.
Stream<String> filenameStream = fileStream.map(File::getName);
filenameStream.forEach(System.out::println); // 파일이름 모두 출력
map()은 중간연산자 이므로 여러번 사용해서 아래와 같이 File 객체들의 확장자만 뽑을 수도 있다.
filenameStream.filter(s -> s.indexof('.') != -1) // 파일 이름들 중 확장자가 없으면(확장자는 Ex1.txt처럼 .뒤에 적혀있다)
// 뽑아 낼 확장자가 없어 미리 제외시킴
.map(s -> s.substring(s.indexof('.') + 1)) // Stream<String> -> Stream<String>
.map(String::toUpperCase)
.distinct() // 중복 제거
.forEach(System.out::println); // 스트림안의 String요소들 한개씩 출력
연산과 연산 사이에 올바르게 처리됐는지 확인하려면, peek()를 사용한다.
forEach()와는 다르게 스트림의 요소를 소모하지 않기에 연산 사이에 여러 번 끼워 넣어도 문제가 되지 않는다.
fileStream.map(File::getName) // Stream<File> -> Stream<String>
.filter(s -> s.indexOf('.') != -1)
.peek(s -> System.out.printf("filename = %s%n", s)) // 파일명 출력해보기
.map(s -> s.substring(s.indexOf('.') + 1))
.peek(s -> System.out.printf("extension = %s%n", s)) // 확장자 출력해보기
.forEach(System.out::println)
Stream<String[]> strArrStrm = Stream.of(
new String[]{"abc", "def", "ghi"},
new String[]{"ABC", "DEF", "GHI"}
);
// flatMap()은 스트림의 타입이 Stream<T[]>인 경우, Stream<T>로 변환해서 작업할 떄 사용한다.
각 요소의 문자열들을 합쳐서 문자열이 요소인 스트림, 즉 Stream<String.>으로 만들려면 어떻게 해야 할까?
Stream<Stream<String>> strStrStrm = strArrStrm.map(Arrays::stream);
보통은 이렇게 생각하는데, 원하는 것과 달리 스트림 안에 스트림이 생성되고 만다.
이 때 간단히 map()을 아래와 같이 flatMap()으로 바꾸기만 하면 우리가 원하는 결과를 얻을 수 있다.
Stream<String> strStrm = strArrStrm.flatMap(Arrays::stream);
이렇게 flatMap()을 사용하면 각 요소의 문자열들이 합쳐지게 된다.
import java.util.Arrays;
import java.util.stream.Stream;
public class FlatMapExample {
public static void main(String[] args) {
Stream<String[]> strArrStrm = Stream.of(
new String[]{"abc", "def", "ghi"},
new String[]{"ABC", "DEF", "GHI"});
// Stream<Stream<String>> strStrmStrm = strArrStrm.map(Arrays::stream);
Stream<String> strStrm = strArrStrm.flatMap(Arrays::stream);
strStrm.map(String::toLowerCase)
.distinct()
.sorted()
.forEach(System.out::println);
System.out.println();
String[] lineArr = {
"Believe or not It is true",
"Do or do not There is no try",
};
Stream<String> lineStream = Arrays.stream(lineArr);
lineStream.flatMap(line -> Stream.of(line.split(" +")))
.map(String::toLowerCase)
.distinct()
.sorted()
.forEach(System.out::println);
//아래는 풀어서 써 본것. 더 어려워 보이긴 한다..
// Stream<String[]> strArrStrm2 = lineStream.map(s -> s.split(" +"));
// Stream<String> strStrm2 = strArrStrm2.flatMap(Arrays::stream);
// strStrm2.map(String::toLowerCase)
// .distinct()
// .sorted()
// .forEach(System.out::println);
}
}
" +"는 정규식으로 하나 이상의 공백을 의미하며, s.split(" +")의 결과는 문자열 s를 공백을 구분자로 자른 문자열 배열이 된다.
line.split()을 하면 lineArr에 있던 두 문장이 공백을 기준으로 각각의 String[]에 잘려서 담긴다. 따라서 String[] strArr1, String[] strArr2이렇게 두개로 나뉘어져 저장돼야 하지만 flatMap()을 사용했기에 바로 문자열타입의 Stream이 생성돼 그 안에 두 문장을 자른 값이 한번에 들어가게 된다.
최종 연산은 스트림의 요소를 소모해서 결과를 만들어 낸다. 그래서 최종 연산후에 스트림은 닫히게 되고 더 이상 사용할 수 없게된다.
최종 연산은 reduce()와 collect()가 핵심이다. 나머지는 이해하기 쉽고 사용법도 간단하다.
forEach()는 중간 연산의 peek()와 달리 스트림의 요소를 소모하는 최종 연산이며, 반환 타입이 void이므로 스트림의 요소를 출력하는 용도로 많이 사용한다.
void forEach(Consumer<? super T> action)
forEachOrdered()는 직렬일때는 아무런 의미 없다.
아래와 같이 스트림을 처리한다고 했을 때, 병렬로 배열등을 출력하면 순서를 보장할 수 없게된다. 이 때 순서를 보장하는게 위 메서드이다.
IntStream.range(1, 10).parallel().forEach(System.out::println); // 순서 보장 안됨.
IntStream.range(1, 10).parallel().forEachOrdered(System.out::println); // 순서 보장 됨
스트림의 요소에 대해 지정된 조건에 요소 모두, 요소 하나, 요소 모두불일치하는지 확인할 수 있는 메서드들이며 Predicate를 필요로한다.
boolean allMatch (Predicate<? super T> p) // 모든 요소가 일치하면 참
boolean anyMatch (Predicate<? super T> p) // 하나의 요소라도 일치하면 참
boolean noneMatch (Predicate<? super T> p) // 모든 요소가 불일치하면 참
이 외에도 스트림의 요소 중에서 조건에 일치하는 첫 번째 것을 반환하는 findFirst()가 있고,
병렬 스트림을 이용해서 조건에 일치하는 아무 요소 하나를 반환하는 findAny()가 있다.
Optional<T> findFirst() // 첫 번째 요소를 반환. 순차 스트림에 사용
Optional<T> findAny() // 아무거나 하나를 반환. 병렬 스트림에 사용
아래와 같이 filter(Predicate p)와 같이 사용한다.
Optional<Student> result = stuStream.filter(s -> s.getTotalScore() <= 100).findFirst();
Optional<Student> result = parellelStream.filter(s -> s.getTotalScore() <= 100).findAny();
스트림의 요소를 하나씩 줄여가며 누적연산 수행 - reduce()
※ accumulate : 누적하다
reduce()는 이름에서 짐작하듯, 스트림의 요소를 줄여나가며 연산을 수행하고 최종결과를 반환한다. 그래서 매개변수의 타입이 BinaryOperator인 것이다. 처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다.
이 과정에서 스트림의 요소를 하나씩 소모하며, 스트림의 모든 요소를 소모하게 되면 그 결과를 반환하게 된다.
Optional<T> reduce(BinaryOperator<T> accumulator)
이 외에도 연산결과의 초기값(identity)를 갖는 reduce()도 있다. 이 메서드들은 초기값과 스트림의 첫 번째 요소로 연산을 시작
하게 된다.
스트림의 요소가 하나도 없는 경우, 초기값이 반환되므로 반환타입은 Optional이아닌 그냥 T이다.
T reduce(T identity, BinaryOperator<T> accumulator)
U reduce(U identity, BiFunction<T, T, U> accumulator, BinaryOperator<U> combiner)
위 두번째 마세드의 마지막 매개변수이 combiner는 병렬 스트림에 의해 처리된 결과를 합칠 때 사용하기 위해 사용한다.
앞서 사용한 count()와 sum()은 모두 내부적으로 reduce()를 사용한다.
int count = intStream.reduce(0, (a,b) -> a + 1); // count()
int sum = intStream.reduce(0, (a,b) -> a + b); // sum()
int max = intStream.reduce(Integer.MIN_VALUE, (a,b) -> a>b ? a:b); // max()
int min = intStream.reduce(Integer.MAX_VALUE, (a,b) -> a<b ? a:b); // min()
reduce()가 어렵다면 아래처럼 동작한다고 생각하면 된다.
int a = identity; // 초기값을 a에 저장한다.
for(int b : stream)
a = a + b; // 모든 요소의 값을 a에 누적한다.
이 for문을 이해하지 못하는 사람은 없을 것이다. 이 구조를 reduce()메서드에 대입하면 아래와 같다.
T redcue(T identity, BinaryOperator<T> accumulator) {
T a = identity;
for(T b : stream)
a = accumulator.apply(a, b);
}
redcue()를 사용하는 방법은 간단하다. 그저 초기값과 어떤 연산으로 스틀미의 요소를 줄여나갈 것인지만 결정하면 된다.
public class ReduceExample {
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);
// Stream<String>을 IntStream으로 변환
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, (a, b) -> a + b);
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.getAsInt());
System.out.println("min = " + min.getAsInt());
}
}
스트림의 최종 연산 중에서 가장 복잡하면서도 유용하게 활용될 수 있는 것이 collect()이다. 이 메서드는 스트림의 요소를 수집하는 최종 연산으로 reduce()와 비슷하다. collect()가 스트림의 요소를 수집할 때 어떤 방식으로 수집할 것인가에 대한 방법이 정의돼 있어야 하는데 이 방법을 정의한 것이 바로 collector이다.
reduce()는 전체 스트림을 대상으로 리듀싱을 하지만, collect()는 그룹별로 묶어서 리듀싱을 진행할 수 있다.
컬렉터(collector)는 Collector인터페이스를 구현한 것으로, 직접 구현할 수도 있지만 미리 작성된 구현체를 사용할 수도 있다. Collectors클래스는 미리 작성된 다양한 종류의 collector를 반환하는 static메서드들을 가지고 있다. 이 클래스를 통해 제공되는 컬렉터만으로도 많은 일들을 할 수 있다.
collect() // 스트림의 최종 연산, 매개변수로 컬렉터를 필요로 한다.
Collector // 인터페이스, 컬렉터는 이 인터페이스를 구현해야 한다.
Collectors // 클래스, static메서드로 미리 작성된 컬렉터들을 제공한다.
collect()는 매개변수의 타입이 Collector이므로 Collector인터페이스를 구현한 객체를 제공하면 된다. 그러면 collect()가 해당 Collector구현 객체를 토대로 스트림의 요소를 수집한다.
※sort()를 사용할 때 Comparator가 필요한 것처럼 말이다.
Object collect(Collector collector) // Collector를 구현한 클래스의 객체를 매개변수로.
Object collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)
아래 매개변수 3개짜리 collect()는 잘 사용되지는 않지만, Collector인터페이스를 구현하지 않고 간단하게 람다식으로 수집할 때 사용한다.
또한, 직접 Collector를 구현한 클래스를 만들 수 있지만 거의 그럴 일은 없기에 Collectors가 제공하는 구현 클래스들을 잘 사용하는것만으로도 충분하다.
스트림의 모든 요소를 컬렉션에 수집하려면, Collectors클래스의 toList()와 같은 메서드를 사용한다. List나 Set이 아닌 특정 컬렉션을 사용하려면, toCollection()에 원하는 컬렉션의 생성자 참조를 매개변수로 넣어준다.
List<String> names = stuStream.map(Student::getName).collect(Collectors.toList());
// 물론 Collectors.toList()도 ArrayList를 반환하긴 하지만 아래처럼 지정도 가능함.
ArrayList<String> list = names.stream().collect(Collectors.toCollection(ArrayList::new));
// Collectors.toCollection(LinkedList::new)처럼 특정할 수도 있음.
맵에도 담을수 있지만 맵은 key, value가 있기에 스트림에 있는 값을 아래와 같이 map에 옮겨담을수도 있다.
Map<int, Person> map = personStream.collect(Collectors.toMap(p -> p.id, p->p));
람다식 2개를 사용하여 앞쪽 람다식의 결과는 key에, 뒤쪽 람다식의 결과는 value에 저장한다
//map 반환
스트림의 요소를 T[]로 변환하는 방법은 아래와 같다.
T[] tArr = Stream객체.toArray(T[]::new); // T타입의 배열로 반환하고 싶다면 생성자 주입
Object[] oArr = Stream객체.toArray(); // OK. toArray()는 Object[]반환
Student[] sArr = Stream객체.toArray(); // 에러. 자동 형변환 안됨.
Student[] sArr = (Student[])Stream객체.toArray(); // 형변환 하면 OK.
앞서 살펴봤던 최종 연산들이 제공하는 통계 정보를 collect()로 똑같이 얻을 수 있다.
※ summingInt()외에 summingLong(), summingDouble()이 있다. averagingInt()도 마찬가지.
long count = stuStream.count();
long count = stuStream.collect(Collectors.counting());
long totalScore = stuStream.mapToInt(Student::getTotalScore).sum();
long totalScore = stuStream.collect(Collectors.summingInt(Student::getTotalScore));
Optional<Student> topStudent = stuStream
.max(Comparator.comparingInt(Student::getTotalScore));
Optional<Stduent> topStduent = stuStream
.collect(Collectors.maxBy(Comparator.comparingInt(Stduent::getTotalScore)));
IntSummaryStatistics stat = stuStream
.mapToInt(Student::getTotalScore).summaryStatistics();
IntSummaryStatistics stat = stuStream
.collect(Collectors.summarizingInt(Student::getTotalScore));
리듀싱 역시 collect()로 가능하다. IntStream에는 매개변수 3개짜리 collect()만 정의되어 있으므로 boxed()를 통해 intStream을 Integer타입의 Stream으로 변환해야 collect(Collector c)메서드를 사용할 수 있다.
static import java.util.stream.Collectors;
IntStream intStream = new Random().ints(1, 46).distinct().limit(6);
OptionalInt max = intStream.reduce(Integer::max);
Optional<Integer> max = intStream.boxed().collect(reducing(Integer::max));
long sum = intStream.reduce(0, (a, b) -> a + b);
long sum = intStream.boxed().collect(reducing(0, (a, b) -> a + b));
int grandTotal = stuStream.map(Student::getTotalScore).reduce(0, Integer::sum);
int grandTotal = stuStream
.collect(reducing(0, Student::getTotalScore, Integer::sum));
Collectors.reducing()에는 아래와 같이 3가지 종류가 있는데 3번째 빼고는 reduce()와 같다. 세 번째 메서드는 map()과 reduce()를 하나로 합쳐놓은 것 뿐이다.
Collector reducing(BinaryOperator<T> op)
Collector reducing(T identity, BinaryOperator<T> op)
Collector reducing(U identity, Function<T, U> mapper, BinaryOperator<U> op)
joining()은 문자열 스트림의 모든 요소를 하나의 문자열로 연겨ㅐ헛 변환한다. 구분자를 지정할 수도 있고, 접두사와 점미사도 지정 가능하다. 스트림의 요소가 String이거나 StringBuffer등의 CharSequence자손인 경우에만 가능하다.
String studentNames = stuStream.map(Student::getName).collect(joining());
// 구분자 없음 exp = 김자바박자바최자바감자바
String studentNames = stuStream.map(Student::getName).collect(joining(","));
// 구분자 exp = 김자바,박자바,최자바,감자바
String studentNames = stuStream.map(Student::getName).collect(joining(",","[","]"));
// 구분자, 접두사, 접미사 exp = [김자바,박자바,최자바,감자바]
String이 아닌데 joining()을 실행하면 스트림 요소의 toString()을 호출해 결합한다.
collect()의 핵심인 그룹화이다. 그룹화를 하지 않을거라면 굳이 collect()를 사용하지 않고 다른 연산으로 대체가능한 경우가 많았다.
그룹화는 스트림의 요소를 특정 기준으로 그룹화 하는것을 말하고, 분할은 스트림의 요소를 두 가지, 지정된 조건에 일치하는 그룹과 그렇지 않은 그룹으로의 분할을 의미한다.
groupingBy()는 스트림의 요소를 Function으로 나누고, partitioningBy()는 Predicate로 분류한다.
Collector partitioningBy(Predicate predicate)
Collector partitioningBy(Predicate predicate, Collector downstream)
Collector groupingBy(Function classifier)
Collector groupingBy(Fucntion classifier, Collector downstream)
Collector groupingBy(Function classifier, Supplier mapFactory, Collector downstream)
스트림을 두 개의 그룹으로 나눠야 한다면, 당연히 partitioningBy()로 분할하는 것이 빠르고, 그 외에는 groupingBy()를 사용한다.
그룹화와 분할의 결과는 Map에 담겨 리턴된다.
분할은 Predicate를 사용하기때문에 해당 조건에 맞는지 맞지않는지 2그룹으로 분할된다.
// 1. 기본 분할
Map<Boolean, List<Student>> stuBySex = stuStream.collect(partitioningBy(Student::isMale)); // 성별로 분할
List<Student> maleStudent = stuBySex.get(true); // Map에서 남학생 목록 얻기
List<Student> femaleStudent = stuBySex.get(false); // Map에서 여학생 목록 얻기
위 코드는 간단한 분할이다. isMale()이라는 메서드를 이용해 스트림의 요소마다 true, false를 비교해서 분할하고 이를 Map<Boolean, List>로 담는다. 그럼 해당 Map에는 true가 key인 List 한 개와, false가 key인 List 한 개로 총 두 개의 List를 가지게 되는 코드이다.
// 2. 기본 분할 + 통계 정보
Map<Boolean, Long> stuNumBySex = stuStream.collect(partitioningBy(Student::isMale, Collectors.counting()));
System.out.println("남학생 수 :" + stuNumBySex.get(true));
System.out.println("여학생 수 :" + stuNumBySex.get(false));
위 코드는 partitioningBy()의 매개변수 두개짜리를 사용한 예제이다. Collector를 지정해 주어, isMale()의 결과에 따른 남학생, 여학생 수를 각각 true, false의 key를 가지는 Map에 value로 저장했다.
Map<Boolean, Optional<Student>> topScoreBySex = stuStream
.collect(
partitioningBy(Student::isMale,
Collecotrs.maxBy(comparingInt(Student::getScore))
);
System.out.println("남학생 1등 :" + topScoreBySex.get(true));
System.out.println("여학생 1등 :" + topScoreBySex.get(false));
partitioningBy()를 2번씩사용하여 다중 분할할 수도 있다.
Map<Boolean, Map<Boolean, List<Student>> failedStuBySex = stuStream
.collect(partitioningBy(Student::isMale, // 1. 성별로 분할
partitioningBy(s -> s.getScore() < 150))); // 2. 성적으로 분할
List<Student> failedMaleStu = failedStuBySex.get(true).get(true);
List<Student> failedFemaleStu = failedStuBySex.get(false).get(true);
groupingBy()는 분할과 다르게 n분할가능하다. 가장 간단한 그룹화 방법을 사용해서 학생들을 반 기준으로 나눠보자
Map<Integer, List<Student>> stuByBan = stuStream
.collect(groupingBy(Stduent::getBan)); // toList() 생략
groupingBy()로 그룹화를 진행하면 기본적으로 List에 담긴다. 그래서 아래 문장을 위와 같이 쓸 수 있다. toList()대신 toSet(), toCollection(...::new)도 가능하다.
Map<Integer, List<Student>> stuByBan = stuStream
.collect(groupingBy(Student::getBan, toList())); // toList() 생략 가능
Map<Integer, Set<Student>> stuByHak = stuStream
.collect(groupingBy(Studnet::getHak, toCollection(HashSet::new)));
아래와 같이 다중 그룹화도 가능하다.
Map<Integer, Map<Integer, List<Student>>> stuByHakAndBan = stuStream
.collect(groupingBy(Student::getHak, // 1. 학년별 그룹화
groupingBy(Student::getBan) // 2. 반별 그룹화
));
조금 더 복잡하게 Student.Level이라는 열거형 등급을 준비하고, 이 등급(HIGH, MID, LOW)를 기준으로 분류한다.
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 reutrn Student.Level.LOW;
}, counting())
}; // [MID] - 8명, [HIGH] - 8명, [LOW] - 2명
더더 복잡하게, 아래의 코드는 학년별과 반별로 그룹화한 다음에, 성적그룹으로 변환(mapping)하여 Set에 저장한다.
Map<Integer, Map<Integer, Set<Student.Level>>> stuByHakAndBan = stuStream
.collect(
groupingBy(Student::getHak,
groupingBy(Student::getBan,
mapping(s-> {
if(s.getScore() >= 200) return Student.Level.HIGH;
else if(s.getScore() >= 100) return Student.Level.MID;
else return Student.Level.LOW;
} , toSet())));
)