람다와 스트림 4

195338 단어 JavaJava

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()와 같은 메서드를 사용하면 된다.
ListSet이 아닌 특정 컬렉션을 지정하려면, 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()

보다 간결한 코드를 위해서 Collectorsstatic메서드를 호출할 때는 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()를 통해 IntStreamStream<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에 담아서 반환하도록 구현하면 된다.

 

💡 참고
열거형 CharacteristicsCollector내에 정의되어 있다.

 

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도 내부적으로 처리하는 과정이 리듀싱과 같다는 것을 의미한다.
IntStreamcount(), 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. 스트림의 변환

스트림의 변환에 사용되는 메서드

 

좋은 웹페이지 즐겨찾기