관리 메뉴

나구리의 개발공부기록

자바의 정석 기초편 ch14 - 45 ~ 55 [collect(), Collectors, 스트림의 그룹화와 분할, 스트림의 변환] 본문

유튜브 공부/JAVA의 정석 기초편(유튜브)

자바의 정석 기초편 ch14 - 45 ~ 55 [collect(), Collectors, 스트림의 그룹화와 분할, 스트림의 변환]

소소한나구리 2023. 12. 22. 10:52

1) collect() - 최종연산

  • Collector를 매개변수로 하는 스트림의 최종연산 (Collector는 인터페이스)
  • Collector를 구현한 Collectors클래스의 메서드를 인수로 사용할 수 있음
Object collect(Collector collector) // Collector를 구현한 클래스의 객체를 매개변수로
Object collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner) // 잘 안쓰임

2) Collector - 인터페이스

  • 수집(collect)에 필요한 메서드를 정의해 놓은 인터페이스
  • supplier(), accumulator()가 핵심
 public interface Collector<T, A, R> {      // T(요소)를 A에 누적한 다음, 결과를 R로 변환해서 반환 
    Supplier<A>          supplier();        // StringBuilder::new            누적할 곳(초기화)
    BiConsumer<A, T>     accumulator();     // (sb, s) -> sb.append(s)       누적방법(누적연산)
    BinaryOperator<A>    combiner();        // (sb1, sb2) -> sb1.append(sb2) 결합방법(병렬)
    Function<A, R>       finisher();        // sb -> sb.toString()           최종변환
    Set<Characteristics> characteristics(); // 컬렉터의 특성이 담긴 Set을 반환
     ...
 }

3) Collectors - 클래스

  • Collector 인터페이스를 구현하여 다양한 기능을 제공함
  • 변환 – mapping(), toList(), toSet(), toMap(), toCollection(), …
  • 통계 – counting(), summingInt(), averagingInt(), maxBy(), minBy(), summarizingInt(), …
  • 문자열 결합 – joining()
  • 리듀싱 – reducing()
  • 그룹화와 분할 – groupingBy(), partitioningBy(), collectingAndThen()
  • 스트림을 컬렉션으로 변환 - tolist(), toSet(),toMap, toCollection()
// Student의 이름을 List로 저장
List<String> names = stuStream.map(Student::getName)  // Stream<Student>→Stream<String>
                       .collect(Collectors.toList()); // Stream<String>→List<String>

// Student의 이름을 ArrayList로 저장
ArrayList<String> list = names.stream()
  .collect(Collectors.toCollection(ArrayList::new)); // Stream<String>→ArrayList<String>

// person을 map으로 저장(맵은 key와 value값이 필요)
Map<String,Person> map = personStream
  .collect(Collectors.toMap(p->p.getRegId(), p->p));// Stream<Person>→Map<String,Person>

 

(1) 스트림을 배열로 변환 - toArray()

// 많이 쓰임
Student[] stuNames = studentStream.toArray(Student[]::new); // OK. x-> new Student[x]

Student[] stuNames = studentStream.toArray(); // 에러. Student[]로 자동 형변환 불가
Object[]  stuNames = studentStream.toArray(); // OK. Object[]배열로 반환

 

(2) 스트림의 통계정보 제공 - counting(), summingInt(), maxBy(), minBy(),....

  • 스트림의 전체 요소에 적용할 때는 스트림의 메서드를 적용하고 그룹별로 적용할 때는 Collerctors의 메서드들을 사용하여 수량세기, 합계, 최대값, 최소값 등을 계산할 수 있음
// 전체 요소에 대해 적용
long count = stuStream.count();
long totalScore = stuStream.mapToInt(Student::getTotalScore).sum();  // IntStream의 sum()
OptionalInt topScore = studentStream.mapToInt(Student::getTotalScore).max();
Optional<Student> topStudent = stuStream
                        .max(Comparator.comparingInt(Student::getTotalScore));

// 그룹별로 적용 가능
long count = stuStream.collect(counting()); // Collectors.counting()
long totalScore = stuStream.collect(summingInt(Student::getTotalScore));
Optional<Student> topStudent = stuStream
             .collect(maxBy(Comparator.comparingInt(Student::getTotalScore)));

 

(3) 스트림을 리듀싱 - reducing()

  • 그룹별로 리듀싱이 가능
  • 기능은 reduce()와 동일(누적연산)
Collector reducing(BinaryOperator<T> op)
Collector reducing(T identity, BinaryOperator<T> op) // 누적작업
Collector reducing(U identity, Function<T,U> mapper, BinaryOperator<U> op) // map+reduce, 변환작업

// 스트림 생성
IntStream intStream = new Random().ints(1,46).distinct().limit(6);

// 전체 리듀싱
OptionalInt max = intStream.reduce(Integer::max);
long sum = intStream.reduce(0, (a,b) -> a + b);
int grandTotal = stuStream.map(Student::getTotalScore).reduce(0, Integer::sum);

// 그룹별 리듀싱 가능
Optional<Integer> max = intStream.boxed().collect(reducing(Integer::max));
long sum = intStream.boxed().collect(reducing(0, (a,b)-> a + b));
int grandTotal = stuStream.collect(reducing(0, Student::getTotalScore, Integer::sum));

 

(4) 문자열 스트림의 요소를 모두 연결 - joining()

  • 인수로 구분자를 입력하면 구분자로 요소들을 연결함
String studentNames = stuStream.map(Student::getName).collect(joining());
String studentNames = stuStream.map(Student::getName).collect(joining(",")); // 구분자
String studentNames = stuStream.map(Student::getName).collect(joining(",", "[", "]"));
String studentInfo  = stuStream.collect(joining(",")); // Student의 toString()으로 결합

4) 스트림의 그룹화와 분할

  • collect()는 partitioningBy와 groupingBy를 같이 사용함
  • partitioningBy() : 인수의 값과 일치한 요소와 아닌 요소로 스트림을 2분할, 전체를 사용하는 거보다 성능이 좋음
Collector partitioningBy(Predicate predicate) 
Collector partitioningBy(Predicate predicate, Collector downstream)

Map<Boolean, List<Student>> stuBySex = stuStream               
                    .collect(partitioningBy(Student::isMale)); // 학생들을 성별로 분할
                    
List<Student> maleStudent   = stuBySex.get(true);  // Map에서 남학생 목록을 얻음
List<Student> femaleStudent = stuBySex.get(false); // Map에서 여학생 목록을 얻음

Map<Boolean, Long> stuNumBySex = stuStream
                    .collect(partitioningBy(Student::isMale, counting())); // 분할 + 통계
System.out.println("남학생 수 :"+ stuNumBySex.get(true));  // 남학생 수 :8
System.out.println("여학생 수 :"+ stuNumBySex.get(false)); // 여학생 수 :10

Map<Boolean, Optional<Student>> topScoreBySex = stuStream                  // 분할 + 통계
  .collect(partitioningBy(Student::isMale, maxBy(comparingInt(Student::getScore))));
System.out.println("남학생 1등 :"+ topScoreBySex.get(true)); // 남학생 1등 :Optional[[나자바,남, 1, 1,300]]
System.out.println("여학생 1등 :"+ topScoreBySex.get(false)); //여학생 1등 :Optional[[김지미,여, 1, 1,250]]

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);

 

(1) 예제

  • 단일 분할, 다중 분할(추가 그룹 조건을 두번째 인수로 지정)로 그룹화를 하고 그룹화한 요소를 가지고 연산을 적용한 예제
import java.util.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;
import static java.util.Comparator.*;

class Student2 {
	String name;
	boolean isMale; // 성별
	int hak;        // 학년
	int ban;        // 반
	int score;

	Student2(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;
	}
	String	getName()     { return name;	}
	boolean  isMale()     { return isMale;	}
	int      getHak()     { return hak;	}
	int      getBan()     { return ban;	}
	int      getScore()   { return score;	}

	public String toString() { 
		return String.format("[%s, %s, %d학년 %d반, %3d점]",
			name, isMale ? "남":"여", hak, ban, score); 
	}
   // groupingBy()에서 사용
	enum Level { HIGH, MID, LOW }  // 성적을 상, 중, 하 세 단계로 분류
}

class Ex14_10 {
	public static void main(String[] args) {
		Student2[] stuArr = {
			new Student2("나자바", true,  1, 1, 300),	
			new Student2("김지미", false, 1, 1, 250),	
			new Student2("김자바", true,  1, 1, 200),	
			new Student2("이지미", false, 1, 2, 150),	
			new Student2("남자바", true,  1, 2, 100),	
			new Student2("안지미", false, 1, 2,  50),	
			new Student2("황지미", false, 1, 3, 100),	
			new Student2("강지미", false, 1, 3, 150),	
			new Student2("이자바", true,  1, 3, 200),	
			new Student2("나자바", true,  2, 1, 300),	
			new Student2("김지미", false, 2, 1, 250),	
			new Student2("김자바", true,  2, 1, 200),	
			new Student2("이지미", false, 2, 2, 150),	
			new Student2("남자바", true,  2, 2, 100),	
			new Student2("안지미", false, 2, 2,  50),	
			new Student2("황지미", false, 2, 3, 100),	
			new Student2("강지미", false, 2, 3, 150),	
			new Student2("이자바", true,  2, 3, 200)	
		};

		System.out.printf("1. 단순분할(성별로 분할)%n");
		Map<Boolean, List<Student2>> stuBySex =  Stream.of(stuArr)
				.collect(partitioningBy(Student2::isMale));

		List<Student2> maleStudent   = stuBySex.get(true);
		List<Student2> femaleStudent = stuBySex.get(false);

		for(Student2 s : maleStudent)   System.out.println(s);
		for(Student2 s : femaleStudent) System.out.println(s);

		System.out.printf("%n2. 단순분할 + 통계(성별 학생수)%n");
		Map<Boolean, Long> stuNumBySex = Stream.of(stuArr)
				.collect(partitioningBy(Student2::isMale, counting()));

		System.out.println("남학생 수 :"+ stuNumBySex.get(true));
		System.out.println("여학생 수 :"+ stuNumBySex.get(false));

		System.out.printf("%n3. 단순분할 + 통계(성별 1등)%n");
		Map<Boolean, Optional<Student2>> topScoreBySex = Stream.of(stuArr)
				.collect(partitioningBy(Student2::isMale, 
					maxBy(comparingInt(Student2::getScore))
				));
		System.out.println("남학생 1등 :"+ topScoreBySex.get(true));
		System.out.println("여학생 1등 :"+ topScoreBySex.get(false));

		// Optional::get -> Optional의 값을 꺼내서 반환
		Map<Boolean, Student2> topScoreBySex2 = Stream.of(stuArr)
			.collect(partitioningBy(Student2::isMale, 
				collectingAndThen(
					maxBy(comparingInt(Student2::getScore)), Optional::get
				)));

		System.out.println("남학생 1등 :"+ topScoreBySex2.get(true));
		System.out.println("여학생 1등 :"+ topScoreBySex2.get(false));
		
		System.out.printf("%n4. 다중분할(성별 불합격자, 100점 이하)%n");

		Map<Boolean, Map<Boolean, List<Student2>>> failedStuBySex = 
			Stream.of(stuArr).collect(partitioningBy(Student2::isMale, 
				partitioningBy(s -> s.getScore() <= 100))
			); 
            
		List<Student2> failedMaleStu   = failedStuBySex.get(true).get(true);
		List<Student2> failedFemaleStu = failedStuBySex.get(false).get(true);

		for(Student2 s : failedMaleStu)   System.out.println(s);
		for(Student2 s : failedFemaleStu) System.out.println(s);
	}
}

 

(2) groupingBy()

  • 스트림을 그룹화
  • n분할 할 수 있음
Collector groupingBy(Function classifier) 
Collector groupingBy(Function classifier, Collector downstream) 
Collector groupingBy(Function classifier, Supplier mapFactory, Collector downstream)

Map<Integer, List<Student>> stuByBan = stuStream               // 학생을 반별로 그룹화
              .collect(groupingBy(Student::getBan, toList())); // toList() 생략가능
              
Map<Integer, Map<Integer, List<Student>>> stuByHakAndBan = stuStream  // 다중 그룹화
              .collect(groupingBy(Student::getHak,                    // 1. 학년별 그룹화
                   groupingBy(Student::getBan)                        // 2. 반별 그룹화
));

// 조건별로 성적을 나눔
Map<Integer, Map<Integer, Set<Student.Level>>> stuByHakAndBan = stuStream
    .collect(
         groupingBy(Student::getHak, groupingBy(Student::getBan,   // 다중 그룹화(학년별, 반별)
              mapping(s-> {   // 성적등급(Level)으로 변환.  List<Student> → Set<Student.Level>
                   if     (s.getScore() >= 200) return Student.Level.HIGH;
                   else if(s.getScore() >= 100) return Student.Level.MID;
                   else                         return Student.Level.LOW;
              } , toSet())  // mapping()             // enum Level { HIGH, MID, LOW }
         ))  // groupingBy()
    ); // collect()

 

(3) 예제

import java.util.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;
import static java.util.Comparator.*;

class Student3 {
	String name;
	boolean isMale; // 성별
	int hak;        // 학년
	int ban;        // 반
	int score;

	Student3(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;
	}

	String	getName() 	 { return name;    }
	boolean isMale()  	 { return isMale;  }
	int	getHak()   	 { return hak;	   }
	int	getBan()  	 { return ban;	   }
	int	getScore()	 { return score;   }

	public String toString() {
		return String.format("[%s, %s, %d학년 %d반, %3d점]"
				, name, isMale ? "남" : "여", hak, ban, score);
	}

	enum Level {
		HIGH, MID, LOW
	}
}

class Ex14_11 {
	public static void main(String[] args) {
		Student3[] stuArr = {
			new Student3("나자바", true,  1, 1, 300),	
			new Student3("김지미", false, 1, 1, 250),	
			new Student3("김자바", true,  1, 1, 200),	
			new Student3("이지미", false, 1, 2, 150),	
			new Student3("남자바", true,  1, 2, 100),	
			new Student3("안지미", false, 1, 2,  50),	
			new Student3("황지미", false, 1, 3, 100),	
			new Student3("강지미", false, 1, 3, 150),	
			new Student3("이자바", true,  1, 3, 200),	
			new Student3("나자바", true,  2, 1, 300),	
			new Student3("김지미", false, 2, 1, 250),	
			new Student3("김자바", true,  2, 1, 200),	
			new Student3("이지미", false, 2, 2, 150),	
			new Student3("남자바", true,  2, 2, 100),	
			new Student3("안지미", false, 2, 2,  50),	
			new Student3("황지미", false, 2, 3, 100),	
			new Student3("강지미", false, 2, 3, 150),	
			new Student3("이자바", true,  2, 3, 200)	
		};

		System.out.printf("1. 단순그룹화(반별로 그룹화)%n");
		Map<Integer, List<Student3>> stuByBan = Stream.of(stuArr)
				.collect(groupingBy(Student3::getBan));

		for(List<Student3> ban : stuByBan.values()) {
			for(Student3 s : ban) {
				System.out.println(s);
			}
		}

		System.out.printf("%n2. 단순그룹화(성적별로 그룹화)%n");
		Map<Student3.Level, List<Student3>> stuByLevel = Stream.of(stuArr)
			.collect(groupingBy(s-> {
				if(s.getScore() >= 200) return Student3.Level.HIGH;
				else if(s.getScore() >= 100) return Student3.Level.MID;
				else                         return Student3.Level.LOW;
               	}));
	
		TreeSet<Student3.Level> keySet = new TreeSet<>(stuByLevel.keySet());

		for(Student3.Level key : keySet) {
			System.out.println("["+key+"]");

			for(Student3 s : stuByLevel.get(key))
				System.out.println(s);
				System.out.println();
		}

		System.out.printf("%n3. 단순그룹화 + 통계(성적별 학생수)%n");
		Map<Student3.Level, Long> stuCntByLevel = Stream.of(stuArr)
			.collect(groupingBy(s-> {
				if(s.getScore() >= 200) return Student3.Level.HIGH;
				else if(s.getScore() >= 100) return Student3.Level.MID;
				else                         return Student3.Level.LOW;
               	}, counting()));
                
		for(Student3.Level key : stuCntByLevel.keySet())
			System.out.printf("[%s] - %d명, ", key, stuCntByLevel.get(key));
			System.out.println();
/*
		for(List<Student3> level : stuByLevel.values()) {
			System.out.println();
			for(Student3 s : level) {
				System.out.println(s);
			}
		}
*/
		System.out.printf("%n4. 다중그룹화(학년별, 반별)");
		Map<Integer, Map<Integer, List<Student3>>> stuByHakAndBan =
			Stream.of(stuArr)
				.collect(groupingBy(Student3::getHak,
					 groupingBy(Student3::getBan)
				));

		for(Map<Integer, List<Student3>> hak : stuByHakAndBan.values()) {
			for(List<Student3> ban : hak.values()) {
				System.out.println();
				for(Student3 s : ban)
					System.out.println(s);
			}
		}

		System.out.printf("%n5. 다중그룹화 + 통계(학년별, 반별 1등)%n");
		Map<Integer, Map<Integer, Student3>> topStuByHakAndBan =
			Stream.of(stuArr)
				.collect(groupingBy(Student3::getHak,
					groupingBy(Student3::getBan,
					collectingAndThen(
					maxBy(comparingInt(Student3::getScore))
					, Optional::get
					))));

		for(Map<Integer, Student3> ban : topStuByHakAndBan.values())
			for(Student3 s : ban.values())
				System.out.println(s);

		System.out.printf("%n6. 다중그룹화 + 통계(학년별, 반별 성적그룹)%n");
		Map<String, Set<Student3.Level>> stuByScoreGroup = Stream.of(stuArr)
			.collect(groupingBy(s-> s.getHak() + "-" + s.getBan(),
				mapping(s-> {
					if(s.getScore() >= 200) return Student3.Level.HIGH;
					else if(s.getScore() >= 100) return Student3.Level.MID;
					else                        return Student3.Level.LOW;
					} , toSet())));

		Set<String> keySet2 = stuByScoreGroup.keySet();

		for(String key : keySet2) {
			System.out.println("["+key+"]" + stuByScoreGroup.get(key));
		}
	}  // main의 끝
}

 


4) 스트림의 변환 메서드들 정리

(1) 스트림 -> 기본형 스트림

from to 변환 메서드
Stream<T> IntStream mapToInt(ToIntFunction<T> mapper)
LongStream mapToLong(ToLongFunction<T> mapper)
DoubleStream mapToDouble(ToDoubleFunction<T> mapper)

(2) 기본형 스트림 -> 스트림

from to 변환 메서드
IntStream
LongStream
DoubleStream
Stream<Integer> boxed()
Stream<Long>
Stream<Double>
Stream<U> mapToObj(DoubleFunction<U> mapper)

(3) 기본형 스트림 -> 기본형 스트림

from to 변환 메서드
IntStream
LongStream
DoubleStream
LongStream
DoubleStream
asLongStream()
asDoubleStream()

(4) 스트림 -> 부분 스트림

from to 변환 메서드
Stream<T>
IntStream
Stream<T>
IntStream
skip(long n)
limit(long maxSize)

(5) 두개의 스트림 -> 스트림

from to 변환 메서드
Stream<T>, Stream<T> Stream<T> concat(Stream<T> a, Stream<T> b)
IntStream, IntStream IntStream concat(IntStream a, IntStream b)
LongStream, LongStream LongStream concat(LongStream a, LongStream b)
DoubleStream, DoubleStream DoubleStream concat(DoubleStream a, DoubleStream b)

(6) 스트림의 스트림

from to 변환 메서드
Stream<Stream<T>> Stream<T> flatMap(Function mapper)
Stream<IntStream> IntStream flatMapToInt(Function mapper)
Stream<LongStream> LongStream flatMapToLong(Function mapper)
Stream<DoubleStream> DoubleStream flatMapToDouble(Function mapper)

(7) 스트림 <-> 병렬 스트림

from to 변환 메서드
Stream<T>
IntStream
IntStream
DoubleStream
Stream<T>
IntStream
LongStream
DoubleStream
parallel(), 스트림 → 병렬 스트림
sequential(),병렬 스트림 → 스트림

(8) 스트림 -> 컬렉션

from to 변환 메서드
Stream<T>
IntStream
LongStream
DoubleStream
Collection<T> collect(Collectors.toCollection(Supplier factory))
List<T> collect(Collectors.toList())
Set<T> collect(Collectors.toSet())

(9) 컬렉션 -> 스트림

from to 변환 메서드
Collection<T>
List<T>
Set<T>
Stream<T> stream()

(10) 스트림 -> Map 

from to 변환 메서드
Stream<T>
IntStream
LongStream
DoubleStream
Map<K, V> collect(Collectors.toMap(Function key, Function value))
collect(Colectors.toMap(Function, Function, BinaryOperator))
collect(Collectors.toMap(Function, Function, BinaryOpertor, merge,
                                                             Supplier mapSupplier))

(11) 스트림 -> 배열

from to 변환 메서드
Stream<T> Object[] toArray()
T[] toArray(IntFunction<A[]> generator)
IntStream
LongStream
DoubleStream
int[]
long[]
double[]
toArray()

 

 

** 출처 : 남궁성의 정석코딩_자바의정석_기초편 유튜브