람다와 스트림 4
Java의 정석 의 책을 읽고 정리한 내용입니다.
📖 F. collect()
collect()
는 스트림의 요소를 수집하는 최종 연산으로 앞서 배운 리듀싱(reducing
)과 유사하다.
collect()
가 스트림의 요소를 수집하려면, 어떻게 수집할 것인가에 대한 방법이 정의되어 있어야 하는데, 이 방법을 정의한 것인 바로 컬렉터(collector
)이다.
컬렉터는 Collector인터페이스
를 구현한 것으로, 직접 구현할 수도 있고 미리 작성된 것을 사용할 수도 있다.
Collectors클래스
는 미리 작성된 다양한 종류의 컬렉터를 반환하는 static메서드
를 가지고 있으며, 이 클래스를 통해 제공되는 컬렉터만으로도 많은 일들을 할 수 있다.
collect() 스트림의 최종연산, 매개변수로 컬렉터를 필요로 한다.
Collector 인터페이스, 컬렉터는 이 인터페이스를 구현해야한다.
Collectors 클래스, static메서드로 미리 작성된 컬렉터를 제공한다.
collect()
는 매개변수의 타입이 Collector
인데, 매개변수가 Collector
를 구현한 클래스의 객체이어야 한다는 뜻이다.
그리고 collect()
는 이 객체에 구현된 방법대로 스트림의 요소를 수집한다.
💡 참고
sort()
할 때,Comparator
가 필요한 것처럼collect()
할 때는Collector
가 필요하다.
Object collect(Collector collector) // Collector를 구현한 클래스의 객체를 매개변수로
Object collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)
그리고 매개변수가 3개나 정의된 collect()
는 잘 사용되지는 않지만, Collector인터페이스
를 구현하지 않고 간단히 람다식으로 수집할 때 사용하면 편리하다.
✔️ 스트림을 컬렉션과 배열로 변환 - toList(), toSet(), toMap(), toCollection(), toArray()
스트림의 모든 요소를 컬렉션에 수집하려면, Collectors클래스
의 toList()
와 같은 메서드를 사용하면 된다.
List
나 Set
이 아닌 특정 컬렉션을 지정하려면, toCollection()
에 해당 컬렉션의 생성자 참조를 매개변수로 넣어주면 된다.
List<String> names = stuStream.map(Student::getName)
.collect(Collectors.toList());
ArrayList<String> list = names.stream()
.collect(Collectors.toCollection(ArrayList::new));
Map
은 키와 값의 쌍으로 저장해야하므로 객체의 어떤 필드를 키로 사용할지와 값으로 사용할지를 지정해줘야 한다.
Map<String, Person> map = personStream
.collect(Collectors.toMap(p->p.getRegId(), p->p));
위 문장은 요소의 타입이 Person
인 스트림에서 사람의 주민번호(regId
)를 키로 하고, 값으로 Person객체
를 그대로 저장한다.
스트림에 저장된 요소들을 T[]
타입의 배열로 변환하려면, toArray()
를 사용하면 된다.
단, 해당 타입의 생성자 참조를 매개변수로 지정해줘야 한다.
만일 매개변수를 지정하지 않으면 반환되는 배열의 타입은 Object[]
이다.
Student[] stuNames = studentStream.toArray(Student[]::new); // OK
Student[] stuNames = studentStream.toArray(); // 에러
Object[] stuNames = studentStream.toArray(); // OK
✔️ 통계 - counting(), summingInt(), averageInt(), maxBy(), minBy()
보다 간결한 코드를 위해서 Collectors
의 static메서드
를 호출할 때는 Collectors.
를 생략하였다. static import
되어 있다고 가정하자.
💡 참고
summingint()외에도 summingLong(), summingDouble()이 있다. averagingInt()도 마찬가지다.
long count = stuStream.count();
long count = stuStream.collect(counting()); // Collectors.counting()
long totalScore = stuStream.mapToInt(Student::getTotalScore).sum();
long totalScore = stuStream.collect(summingInt(Student::getTotalScore);
OptionalInt topScore = studentStream.mapToInt(Student:getTotalScore).max();
Optional<Student> topStudent = stuStream.max(Comparator.comparingInt(Student::getTotalScore)));
Optional<Student> topStudent = stuStream.collect(maxby(Comparator.comparingint(Student::getTotalScore)));
IntSummaryStatistic stat = stuStream.mapToInt(Student::getTotalScore).summaryStatistics();
IntSummaryStatistic stat = stuStream.collect(summarizingInt(Student::getTotalScore));
summingInt()
와 summarizingInt()
를 혼동하지 않도록 주의하자!
✔️ 리듀싱 - reducing()
리듀싱 역시 collect()
로 가능하다. IntStream
에는 매개변수 3개짜리 collect()
만 정의되어 있으므로 boxed()
를 통해 IntStream
을 Stream<Integer>
로 변환해야 매개변수 1개짜리 collect()
를 쓸 수 있다.
IntStream intStream = new Random().ints(1,46).distinct().limit(6);
OptionalInt max = intStream.reduce(Integer::max);
Optional<Integer> amx = 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::getTotalSocre).reduce(0, Integer::sum);
int grandTotal = stuStream.collect(reducing(0,Student::getTotalScore,Integer::sum));
Collectors.reducing()
에는 3가지 종류가 있다.
세 번째 메서드만 제외하고 reduce()
와 같다.
세 번째 것은 위의 예에서 알 수 있듯이 map()
과 reduce()
를 하나로 합쳐놓은 것이다.
Collector reducing(BinaryOpertor<T> op)
Collector reducing(T identity, BinaryOperator<T> op)
Collector reducing(U identity, Frunction<T,U> mapper, BinaryOperator<U> op)
✔️ 문자열 결합 - joining()
문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환한다. 구분자를 지정해줄 수도 있고, 접두사와 접미사도 지정가능하다.
스트림의 요소가 String
이나 StringBuffer
처럼 CharSequence
의 자손인 경우에만 결합이 가능하므로
스트림의 요소가 문자열이 아닌 경우에는 먼저 map()
을 이용해서 스트림의 요소를 문자열로 변환해야 한다.
String studentNames = stuStream.map(Student::getName).collect(joining());
String studentNames = stuStream.map(Student::getName).collect(joining(","));
String studentNames = stuStream.map(Student::getName).collect(joining(",","[","]"));
만일 map()
없이 스트림에 바로 joining()
하면, 스트림의 요소에 toString()
을 호출한 결과를 결합한다.
// Student의 toString()으로 결합
String studentInfo = stuStream.collect(joining(","));
import java.util.*;
import java.util.stream.*;
public class StreamEx6 {
public static void main(String[] args) {
Student3[] stuArr = {
new Student3("이자바", 3, 300),
new Student3("김자바", 1, 200),
new Student3("안자바", 2, 100),
new Student3("박자바", 2, 150),
new Student3("소자바", 1, 200),
new Student3("나자바", 3, 290),
new Student3("감자바", 3, 180)
};
// 학생 이름만 뽑아서 List<String>에 저장
List<String> names = Stream.of(stuArr).map(Student3::getName)
.collect(Collectors.toList());
System.out.println("names = " + names);
// 스트림을 배열로 변환
Student3[] stuArr2 = Stream.of(stuArr).toArray(Student3[]::new);
for (Student3 s : stuArr2) {
System.out.println(s);
}
// 스트림을 Map<String, String>로 변환. 학생 이름이 key
Map<String, Student3> stuMap = Stream.of(stuArr)
.collect(Collectors.toMap(s -> s.getName(), p -> p));
for (String s : stuMap.keySet()) {
System.out.println("name = " + stuMap.get(s));
}
long count = Stream.of(stuArr).collect(Collectors.counting());
long totalScore = Stream.of(stuArr)
.collect(Collectors.summingInt(Student3::getTotalScore));
System.out.println("count = " + count);
System.out.println("totalScore = " + totalScore);
totalScore = Stream.of(stuArr)
.collect(Collectors.reducing(0, Student3::getTotalScore, Integer::sum));
System.out.println("totalScore = " + totalScore);
Optional<Student3> topStudent = Stream.of(stuArr)
.collect(Collectors.maxBy(Comparator.comparingInt(Student3::getTotalScore)));
System.out.println("topStudent = " + topStudent);
IntSummaryStatistics stat = Stream.of(stuArr)
.collect(Collectors.summarizingInt(Student3::getTotalScore));
System.out.println("stat = " + stat);
String stuNames = Stream.of(stuArr).map(Student3::getName)
.collect(Collectors.joining(",", "{", "}"));
System.out.println(stuNames);
}
}
class Student3 implements Comparable<Student3> {
String name;
int ban;
int totalScore;
Student3(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).toString();
}
String getName() {
return name;
}
int getBan() {
return ban;
}
int getTotalScore() {
return totalScore;
}
// 총점 내림차순을 기본 정렬로 한다.
public int compareTo(Student3 s) {
return s.totalScore - this.totalScore;
}
}
✔️ 그룹화와 분할 - groupingBy(), partitioningBy()
이제부터는 본격적으로 collect()
의 유용함을 알게 될 것이다.
그룹화는 스트림의 요소를 특정 기준으로 그룹화하는 것을 의미하고,
분할은 스트림의 요소를 두 가지, 지정된 조건에 일치하는 그룹과 일치하지 않는 그룹으로의 분할을 의미한다.
아래의 메서드 정의에서 알 수 있듯이, groupingBy()
는 스트림의 요소를 Function
으로, partitioningBy()
는 Predicate
로 분류한다.
Collector groupingBy(Function classifier)
Collector groupingBy(Function classifier, Collector downstream)
Collector groupingBy(Function classifier, Supplier mapFactor, Collector downstream)
Collector partitioningBy(Predicate predicate)
Collector partitioningBy(Predicate predicate, Collector downstream)
메서드의 정의를 보면 groupingbBy()
와 partitioningBy()
가 분류를 Fubnction
으로 하느냐
Predicate
로 하느냐의 차이만 있을 뿐 동일하다는 것을 알 수 있다.
스트림을 두 개의 그룹으로 나눠야 한다면, 당연히 partitioningBy()
로 분할하는 것이 더 빠르다.
그 외에는 groupingBy()
를 쓰면 된다. 그리고 그룹화와 분할의 결과는 Map
에 담겨 반환된다.
밑은 예시에서 사용될 Student클래스
와 Stream
정보이다.
class Student {
String name;
boolean isMale;
int hak;
int ban;
int score;
public Student(String name, boolean isMale, int hak, int ban, int score) {
this.name = name;
this.isMale = isMale;
this.hak = hak;
this.ban = ban;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", 성별=" + (isMale ? "남" : "여") +
", hak=" + hak +
", ban=" + ban +
", score=" + score +
'}';
}
public String getName() {
return name;
}
public boolean isMale() {
return isMale;
}
public int getHak() {
return hak;
}
public int getBan() {
return ban;
}
public int getScore() {
return score;
}
// groupingBy()에서 사용
enum Level {HIGH, MID, LOW}
Student[] stuArr = {
new Student("나자바", true, 1, 1, 300),
new Student("김지미", false, 1, 1, 250),
new Student("김자바", true, 1, 1, 200),
new Student("이지미", false, 1, 2, 150),
new Student("남자바", true, 1, 2, 100),
new Student("안지미", false, 1, 2, 50),
new Student("황지미", false, 1, 3, 100),
new Student("강지미", false, 1, 3, 150),
new Student("이자바", true, 1, 3, 200),
new Student("나자바", true, 2, 1, 300),
new Student("김지미", false, 2, 1, 250),
new Student("김자바", true, 2, 1, 200),
new Student("이지미", false, 2, 2, 150),
new Student("남자바", true, 2, 2, 100),
new Student("안지미", false, 2, 2, 50),
new Student("황지미", false, 2, 3, 100),
new Student("강지미", false, 2, 3, 150),
new Student("이자바", true, 2, 3, 200)
};
}
✔️ partitioningBy()에 의한 분류
먼저 상대적으로 간단한 partitiongBy()
부터 시작하자. 가장 기본적인 분할은 학생들을 성별로 나누어 List
에 담는 것이다.
// 1. 기본 분할
Map<Boolean, List<Student>> stuBySex = stuStream.collect(partitioningBy(Student::isMale)); // 성별로 분할
List<Student> maleStu = stuBySex.get(true); // map에서 남학생 목록 얻음
List<Student> femaleStu = stuBySex.get(true);// map에서 여학생 목록 얻음
이번에는 counting()
을 추가해 남학생과 여학생의 수를 구해보자!
// 2. 기본 분할 + 통계 정보
Map<Boolean, Long> stuNumBySex = stuStream.collect(partitioningBy(Student::isMale,counting()));
System.out.println("남학생 수 : " + stuNumBySex.get(true)); // 남학생 수 : 8
System.out.println("여학생 수 : " + stuNumBySex.get(false)); // 여학생 수 : 10
counting()대신 summingLong()을 사용하면, 남학생과 여학생의 총점을 구할 수 있다.
그러면, 남학생 1등과 여학생 1등은 어떻게 구할 수 있을까?
Map<Boolean,Optional<Student>> topScoreBySex = stuStream.collect(
partitioningBy(Student::isMale,
maxBy(comparingInt(Student::getScore))
)
);
System.out.println("남학생 1등" : OptioanlcoreBySex.get(true)); // 남학생 1등
System.out.println("여학생 1등 : " + topScoreBySex.get(false)); // 여학생 1등
// 남학생 1등 : Optioanl[[나자바, 남, 1, 1, 300]]
// 여학생 1등 : Optioanl[[김지미, 여, 1, 1, 250]]
maxBy()
는 반환타입이 Optional<Student>
라서 위와 같은 결과가 나왔다. Optional<Student>
가 아닌 Student
를 반환 결과로 얻으려면, 아래와 같이 collectingAndThen()
과 Optional::get
을 함께 사용하면 된다.
Map<Boolean,Student> topScoreBySex = stuStream.collect(
partitioningBy(Student::isMale,
collectingAndThen(
maxBy(comparingInt(Student::getScore)), Optional::get
)
)
);
System.out.println("남학생 1등" : OptioanlcoreBySex.get(true)); // 남학생 1등
System.out.println("여학생 1등 : " + topScoreBySex.get(false)); // 여학생 1등
// 남학생 1등 : [나자바, 남, 1, 1, 300]
// 여학생 1등 : [김지미, 여, 1, 1, 250]
성적이 150점 아래인 학생들은 불합격처리하고 싶다. 불학겹자를 성별로 분류하여 얻어내려면 어떻게 해야 할까? partitioningBy()
를 한 번 더 사용해서 이중 분할을 하면 된다. (다중 분할)
Map<Boolean, Map<Boolean, List<Student>>> failedStuBySex = stuStream.collect(
partitioningBy(Student::isMale),
partitioningBy(s -> s.getScore() < 150)
)
);
List<Student> failedMaleStu = failedStuBySex.get(true).get(true);
List<Student> failedFemaleStu = failedStuBySex.get(false).get(true);
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
class Student {
String name;
boolean isMale;
int hak;
int ban;
int score;
public Student(String name, boolean isMale, int hak, int ban, int score) {
this.name = name;
this.isMale = isMale;
this.hak = hak;
this.ban = ban;
this.score = score;
}
public String toString() {
return String.format("[%s, %s, %d학년 %d반, %3d점", name, isMale ? "남" : "여", hak, ban, score);
}
public String getName() {
return name;
}
public boolean isMale() {
return isMale;
}
public int getHak() {
return hak;
}
public int getBan() {
return ban;
}
public int getScore() {
return score;
}
// groupingBy()에서 사용
enum Level {HIGH, MID, LOW}
}
public class MainJava {
public static void main(String[] args) {
Student[] stuArr = {
new Student("나자바", true, 1, 1, 300),
new Student("김지미", false, 1, 1, 250),
new Student("김자바", true, 1, 1, 200),
new Student("이지미", false, 1, 2, 150),
new Student("남자바", true, 1, 2, 100),
new Student("안지미", false, 1, 2, 50),
new Student("황지미", false, 1, 3, 100),
new Student("강지미", false, 1, 3, 150),
new Student("이자바", true, 1, 3, 200),
new Student("나자바", true, 2, 1, 300),
new Student("김지미", false, 2, 1, 250),
new Student("김자바", true, 2, 1, 200),
new Student("이지미", false, 2, 2, 150),
new Student("남자바", true, 2, 2, 100),
new Student("안지미", false, 2, 2, 50),
new Student("황지미", false, 2, 3, 100),
new Student("강지미", false, 2, 3, 150),
new Student("이자바", true, 2, 3, 200)
};
System.out.println("1. 단순분할(성별로 분할)");
Map<Boolean, List<Student>> stuBySex = Stream.of(stuArr)
.collect(Collectors.partitioningBy(Student::isMale));
List<Student> maleStudent = stuBySex.get(true);
List<Student> femaleStudent = stuBySex.get(false);
for (Student s : maleStudent) System.out.println(s);
for (Student s : femaleStudent) System.out.println(s);
System.out.println("\n2. 단순분할 + 통계(성별 학생수)");
Map<Boolean, Long> stuNumBySex = Stream.of(stuArr)
.collect(Collectors.partitioningBy(Student::isMale, Collectors.counting()));
System.out.println("남학생 수 = " + stuNumBySex.get(true));
System.out.println("여학생 수 = " + stuNumBySex.get(false));
System.out.println("\n3. 단순분할 + 통계(성별 1등)");
Map<Boolean, Optional<Student>> topScoreBySex = Stream.of(stuArr)
.collect(Collectors.partitioningBy(Student::isMale, Collectors.maxBy(
Comparator.comparingInt(Student::getScore)
)));
System.out.println("남학생 1등 = " + topScoreBySex.get(true));
System.out.println("여학생 1등 = " + topScoreBySex.get(false));
Map<Boolean, Student> topScoreBySex2 = Stream.of(stuArr)
.collect(Collectors.partitioningBy(Student::isMale,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparingInt(Student::getScore)), Optional::get
)
));
System.out.println("남학생 1등 = " + topScoreBySex2.get(true));
System.out.println("여학생 1등 = " + topScoreBySex2.get(false));
System.out.println("\n4. 다중분할(성별 불합격자, 100점 이하)");
Map<Boolean, Map<Boolean, List<Student>>> failedStuBySex =
Stream.of(stuArr).collect(
Collectors.partitioningBy(Student::isMale,
Collectors.partitioningBy(s->s.getScore() <= 100)
)
);
List<Student> failedMaleStu = failedStuBySex.get(true).get(true);
List<Student> failedFemaleStu = failedStuBySex.get(false).get(true);
for (Student s : failedMaleStu) System.out.println(s);
for (Student s : failedFemaleStu) System.out.println(s);
}
}
> Task :MainJava.main()
1. 단순분할(성별로 분할)
[나자바, 남, 1학년 1반, 300점
[김자바, 남, 1학년 1반, 200점
[남자바, 남, 1학년 2반, 100점
[이자바, 남, 1학년 3반, 200점
[나자바, 남, 2학년 1반, 300점
[김자바, 남, 2학년 1반, 200점
[남자바, 남, 2학년 2반, 100점
[이자바, 남, 2학년 3반, 200점
[김지미, 여, 1학년 1반, 250점
[이지미, 여, 1학년 2반, 150점
[안지미, 여, 1학년 2반, 50점
[황지미, 여, 1학년 3반, 100점
[강지미, 여, 1학년 3반, 150점
[김지미, 여, 2학년 1반, 250점
[이지미, 여, 2학년 2반, 150점
[안지미, 여, 2학년 2반, 50점
[황지미, 여, 2학년 3반, 100점
[강지미, 여, 2학년 3반, 150점
2. 단순분할 + 통계(성별 학생수)
남학생 수 = 8
여학생 수 = 10
3. 단순분할 + 통계(성별 1등)
남학생 1등 = Optional[[나자바, 남, 1학년 1반, 300점]
여학생 1등 = Optional[[김지미, 여, 1학년 1반, 250점]
남학생 1등 = [나자바, 남, 1학년 1반, 300점
여학생 1등 = [김지미, 여, 1학년 1반, 250점
4. 다중분할(성별 불합격자, 100점 이하)
[남자바, 남, 1학년 2반, 100점
[남자바, 남, 2학년 2반, 100점
[안지미, 여, 1학년 2반, 50점
[황지미, 여, 1학년 3반, 100점
[안지미, 여, 2학년 2반, 50점
[황지미, 여, 2학년 3반, 100점
Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0.
✔️ groupingBy()에 의한 분류
일단 가장 간단한 그룹화를 해보자. stuStream
을 반 별로 그룹지어 Map
에 저장하는 방법은 다음과 같다.
Map<Integer,List<Student>> stuByBan = stuStream.collect(groupingBy(Student::getBan)); // toList()가 생략됨
groupingBy()
로 그룹화를 하면 기본적으로 List<T>
에 담는다. 그래서 위의 문장은 아래문장의 생략된 형태이다. 만일 원한다면, toList()
대신 toSet()
이나 toCollection(HashSet::new)
을 사용할 수도 있다. 단, Map
의 제네릭 타입도 적절히 변경해줘야 한다는 것을 잊지 말자.
Map<Integer,List<Student>> stuByBan = stuStream.collect(groupingBy(Student::getBan, toList())); // toList() 생략가능
Map<Integer,HashSet<Student>> stuByHak = stuStream.collect(groupingBy(Student::getHak, toCollection(HashSet::new)));
이번엔 조금 복잡하게 stuStream
을 성적의 등급(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 return Student.Level.LOW;
}, counting()
); // [MID] - 8명, [HIGH] - 8명, [LOW] - 2명
groupingBy()
를 여러 번 사용하면, 다수준 그룹화가 가능하다. 만일 학년별로 그룹화 한 후에 다시 반별로 그룹화하고 싶으면 다음과 같이 한다.
Map<Integer,Map<Integer,List<Student>>> stuByHakAndBan =
stuStream.collect(groupingBy(Student::getHak, groupingBy(Student::getBan)));
위의 코드를 발전시켜서 각 반의 1등을 출력하고 싶다면, collectingAndThen()
과 maxBy()
를 써서 다음과 같이 하면 된다.
Map<Integer, Map<Integer, Student>> topStuByHakAndBan =
stuStream.collect(groupingBy(Student::getHak,
groupingBy(Student::getBan,
collectingAndThen(
maxBy(comparingInt(Student::getScore)), Optional::get
)
)
));
아래의 코드는 학년별과 반별로 그룹화한 다음에, 성적그룹으로 변환(mapping
)하여 Set
에 저장한다.
Map<String, 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())
)
)
);
Student
는 위에있는 class
이므로 생략
public class MainJava {
public static void main(String[] args) {
Student[] stuArr = {
new Student("나자바", true, 1, 1, 300),
new Student("김지미", false, 1, 1, 250),
new Student("김자바", true, 1, 1, 200),
new Student("이지미", false, 1, 2, 150),
new Student("남자바", true, 1, 2, 100),
new Student("안지미", false, 1, 2, 50),
new Student("황지미", false, 1, 3, 100),
new Student("강지미", false, 1, 3, 150),
new Student("이자바", true, 1, 3, 200),
new Student("나자바", true, 2, 1, 300),
new Student("김지미", false, 2, 1, 250),
new Student("김자바", true, 2, 1, 200),
new Student("이지미", false, 2, 2, 150),
new Student("남자바", true, 2, 2, 100),
new Student("안지미", false, 2, 2, 50),
new Student("황지미", false, 2, 3, 100),
new Student("강지미", false, 2, 3, 150),
new Student("이자바", true, 2, 3, 200)
};
System.out.println("1. 단순그룹화(반별로 그룹화)");
Map<Integer, List<Student>> stuByBan = Stream.of(stuArr)
.collect(Collectors.groupingBy(Student::getBan));
for (List<Student> ban : stuByBan.values()) {
for (Student s : ban) {
System.out.println(s);
}
}
System.out.println("\n2. 단순그룹화(성적별로 그룹화)");
Map<Student.Level, List<Student>> stuByLevel = Stream.of(stuArr)
.collect(Collectors.groupingBy(s -> {
if (s.getScore() >= 200) return Student.Level.HIGH;
else if (s.getScore() >= 100) return Student.Level.MID;
else return Student.Level.LOW;
}));
TreeSet<Student.Level> keySet = new TreeSet<>(stuByLevel.keySet());
for (Student.Level key : keySet) {
System.out.println("[" + key + "]");
for (Student s : stuByLevel.get(key)) {
System.out.println(s);
}
System.out.println();
}
System.out.println("\n3. 단순그룹화 + 통계(성적별 학생수)");
Map<Student.Level, Long> stuCntByLevel = Stream.of(stuArr)
.collect(Collectors.groupingBy(s -> {
if (s.getScore() >= 200) return Student.Level.HIGH;
else if (s.getScore() >= 100) return Student.Level.MID;
else return Student.Level.LOW;
}, Collectors.counting()));
for (Student.Level key : stuCntByLevel.keySet()) {
System.out.printf("[%s] - %d명, ", key, stuCntByLevel.get(key));
}
System.out.println();
/*
for(List<Student> level : stuByLevel.values()){
System.out.println();
for(Student s : level){
System.out.println(s);
}
}
*/
// Map<Hak, Map<Ban, List>>
System.out.println("\n4. 다중그룹화(학년별, 반별)");
Map<Integer, Map<Integer, List<Student>>> stuByHakAndBan = Stream.of(stuArr)
.collect(Collectors.groupingBy(Student::getHak,
Collectors.groupingBy(Student::getBan)
));
for (Map<Integer, List<Student>> hak : stuByHakAndBan.values()) {
for (List<Student> ban : hak.values()) {
for (Student s : ban) {
System.out.println(s);
}
System.out.println();
}
}
System.out.println("\n5. 다중그룹화 + 통계(학년별, 반별 1등)");
Map<Integer, Map<Integer, Student>> topStuByHakAndBan = Stream.of(stuArr)
.collect(Collectors.groupingBy(Student::getHak,
Collectors.groupingBy(Student::getBan,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparingInt(Student::getScore))
, Optional::get
))
));
for (Map<Integer, Student> ban : topStuByHakAndBan.values()) {
for (Student s : ban.values())
System.out.println(s);
}
System.out.println("\n6. 다중그룹화 + 통계(학년별, 반별 성적그룹)");
Map<String, Set<Student.Level>> stuByScoreGroup = Stream.of(stuArr)
.collect(Collectors.groupingBy(s -> s.getHak() + "-" + s.getBan(),
Collectors.mapping(s -> {
if (s.getScore() >= 200) return Student.Level.HIGH;
else if (s.getScore() >= 100) return Student.Level.MID;
else return Student.Level.LOW;
}, Collectors.toSet())
));
Set<String> keySet2 = stuByScoreGroup.keySet();
for (String key : keySet2) {
System.out.println("[" + key + "]" + stuByScoreGroup.get(key));
}
}
}
> Task :MainJava.main()
1. 단순그룹화(반별로 그룹화)
[나자바, 남, 1학년 1반, 300점
[김지미, 여, 1학년 1반, 250점
[김자바, 남, 1학년 1반, 200점
[나자바, 남, 2학년 1반, 300점
[김지미, 여, 2학년 1반, 250점
[김자바, 남, 2학년 1반, 200점
[이지미, 여, 1학년 2반, 150점
[남자바, 남, 1학년 2반, 100점
[안지미, 여, 1학년 2반, 50점
[이지미, 여, 2학년 2반, 150점
[남자바, 남, 2학년 2반, 100점
[안지미, 여, 2학년 2반, 50점
[황지미, 여, 1학년 3반, 100점
[강지미, 여, 1학년 3반, 150점
[이자바, 남, 1학년 3반, 200점
[황지미, 여, 2학년 3반, 100점
[강지미, 여, 2학년 3반, 150점
[이자바, 남, 2학년 3반, 200점
2. 단순그룹화(성적별로 그룹화)
[HIGH]
[나자바, 남, 1학년 1반, 300점
[김지미, 여, 1학년 1반, 250점
[김자바, 남, 1학년 1반, 200점
[이자바, 남, 1학년 3반, 200점
[나자바, 남, 2학년 1반, 300점
[김지미, 여, 2학년 1반, 250점
[김자바, 남, 2학년 1반, 200점
[이자바, 남, 2학년 3반, 200점
[MID]
[이지미, 여, 1학년 2반, 150점
[남자바, 남, 1학년 2반, 100점
[황지미, 여, 1학년 3반, 100점
[강지미, 여, 1학년 3반, 150점
[이지미, 여, 2학년 2반, 150점
[남자바, 남, 2학년 2반, 100점
[황지미, 여, 2학년 3반, 100점
[강지미, 여, 2학년 3반, 150점
[LOW]
[안지미, 여, 1학년 2반, 50점
[안지미, 여, 2학년 2반, 50점
3. 단순그룹화 + 통계(성적별 학생수)
[MID] - 8명, [HIGH] - 8명, [LOW] - 2명,
4. 다중그룹화(학년별, 반별)
[나자바, 남, 1학년 1반, 300점
[김지미, 여, 1학년 1반, 250점
[김자바, 남, 1학년 1반, 200점
[이지미, 여, 1학년 2반, 150점
[남자바, 남, 1학년 2반, 100점
[안지미, 여, 1학년 2반, 50점
[황지미, 여, 1학년 3반, 100점
[강지미, 여, 1학년 3반, 150점
[이자바, 남, 1학년 3반, 200점
[나자바, 남, 2학년 1반, 300점
[김지미, 여, 2학년 1반, 250점
[김자바, 남, 2학년 1반, 200점
[이지미, 여, 2학년 2반, 150점
[남자바, 남, 2학년 2반, 100점
[안지미, 여, 2학년 2반, 50점
[황지미, 여, 2학년 3반, 100점
[강지미, 여, 2학년 3반, 150점
[이자바, 남, 2학년 3반, 200점
5. 다중그룹화 + 통계(학년별, 반별 1등)
[나자바, 남, 1학년 1반, 300점
[이지미, 여, 1학년 2반, 150점
[이자바, 남, 1학년 3반, 200점
[나자바, 남, 2학년 1반, 300점
[이지미, 여, 2학년 2반, 150점
[이자바, 남, 2학년 3반, 200점
6. 다중그룹화 + 통계(학년별, 반별 성적그룹)
[1-1][HIGH]
[2-1][HIGH]
[1-2][MID, LOW]
[2-2][MID, LOW]
[1-3][MID, HIGH]
[2-3][MID, HIGH]
Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0.
📖 G. Collector 구현하기
이제 직접 컬렉터를 작성해 보자.
컬렉터를 작성한다는 것은 Collector인터페이스
를 구현한다는 것을 의미하는데, Collector인터페이스
는 다음과 같이 정의되어 있다.
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Charcteristics> characteristics(); // 컬렉터의 특성이 담긴 Set을 반환
...
}
직접 구해야하는 것은 위의 5개의 메서드인데, characteristics()
를 제외하면 모두 반환 타입이 함수형 인터페이스이다. 즉, 4개의 람다식을 작성하면 된다.
supplier() 작업 결과를 제공할 공간을 제공
accumulator() 스트림의 요소를 수집(collect)할 방법을 제공
combiner() 두 저장공간을 병합할 방법을 제공(병렬 스트림)
finisher() 결과를 최종적으로 변환할 방법을 제공
supplier()
는 수집 결과를 저장할 공간을 제공하기 위한 것이고, accumulator()
는 스트림의 요소를 어떻게 supplier()
가 제공한 공간에 누적할 것인지를 정의한다.
combiner()
는 병렬 스트림인 경우, 여러 쓰레드에 의해 처리된 결과를 어떻게 합칠 것인가를 정의한다.
그리고, finisher()
는 작업결과를 변환하는 일을 하는데 변환이 필요없다면, 항등 함수인 Function.identity()
를 반환하면 된다.
public Function finisher() {
return Function.identity(); // 항등 함수를 반환. return x -> x;와 동일
}
마지막으로 characteristics()
는 컬렉터가 수행하는 작업의 속성에 대한 정보를 제공하기 위한 것이다.
Characteristics.CONCURRENT 병렬로 처리할 수 있는 작업
Characteristics.UNORDERED 스트림의 요소의 순서가 유지될 필요가 없는 작업
Characteristics.IDENTITY_FINISH finisher()가 항등 함수인 작업
위 3가지 속성 중에서 해당하는 것을 다음과 같이 Set
에 담아서 반환하도록 구현하면 된다.
💡 참고
열거형Characteristics
는Collector
내에 정의되어 있다.
public SEt<Charcteristics> Characteristics() {
return Collections.unmodifiableSet(EnumSet.of(
Collector.Characteristics.CONCURRENT,
Collector.Characteristics.UNORDERED
));
}
만약 아무런 속성도 지정하고 싶지 않으면, 아래 코드와 같이 하면 된다.
Set<Characteristics> characteristics() {
return Collections.emptySet(); // 지정할 특성이 없는 경우 비어있는 Set을 반환한다.
}
finisher()
를 제외하고 supplier()
, accumulator()
, combiner()
는 모두 앞서 리듀싱에 대해서 배울 때 등장한 개념들이다.
결국 Collector
도 내부적으로 처리하는 과정이 리듀싱과 같다는 것을 의미한다.
IntStream
의 count()
, sum()
, max()
, min()
등이 reduce()
로 구현되어 있다는 것을 기억할 것이다.
그리고 collect()
로도 count()
등의 메서드와 같은 일을 할 수 있었다.
long count = stuStream.count();
long count = stuStream.collect(counting());
reduce()
와 collect()
는 근본적으로 하는 일이 같다.
collect()
는 앞서 본 것처럼, 그룹화와 분할, 집계 등에 유용하게 쓰이고, 병렬화에 있어서 reduce()
보다 collect()
가 더 유리하다.
결론적으로 reduce()
에 대해 잘 이해했다면, Collector
를 구현하는 일이 어렵지 않을 것이다.
만약 String배열
의 모든 문자열을 하나의 문자열로 합치려면 어떻게 해야 할까? 아마 아래와 같을 것이다.
String[] strArr = {"aaa", "bbb", "ccc"};
StringBuffer sb = new StringBuffer(); // supplier(), 저장할 공간을 생성
for(String tmp : strArr)
sb.append(tmp); // accumulator(), sb에 요소를 저장
String result = sb.toString(); // finisher(), StringBuffer를 String으로 변환
위의 코드만으로도 컬렉터를 어떻게 구현해야하는지 감이 올 것이다.
위의 코드를 바탕으로 Stream<String>
의 모든 문자열을 하나로 결합해서 String
으로 반환하는 ConcatCollector
를 작성해 보자!
public class MainJava {
public static void main(String[] args) {
String[] strArr = {"aaa","bbb","ccc"};
Stream<String> stringStream = Stream.of(strArr);
String result = stringStream.collect(new ConcatCollector());
System.out.println("Arrays.toString(strArr) = " + Arrays.toString(strArr));
System.out.println("result = " + result);
}
}
class ConcatCollector implements Collector<String, StringBuilder, String> {
@Override
public Supplier<StringBuilder> supplier() {
return StringBuilder::new;
}
@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();
}
}
📖 H. 스트림의 변환
스트림의 변환에 사용되는 메서드
Author And Source
이 문제에 관하여(람다와 스트림 4), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@chang626/람다와-스트림-4저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)