프로그래밍 언어/Java

[Stream] 수집, collect() 메소드, Collectors

코딩하는흑구 2019. 10. 11. 18:49
수집

- 요소들을 필터링/매핑한 후 요소들을 수집하는 최종처리 메소드를 제공

- 필요한 요소만 컬렉션으로 담을 수 있고, 요소들을 그룹핑한 후 집계할 수 있음

 

예제를 위한 Student 클래스

public class Student implements Comparable<Student>{
	
	public enum Sex {MALE, FEMALE}
	public enum City {Seoul, Pusan}
	
	private String name;
	private int score;
	private Sex sex;
	private City city;
	
	
	
	public Student(String name, int score) {
		super();
		this.name = name;
		this.score = score;
	}

	public Student(String name, int score, Sex sex) {
		super();
		this.name = name;
		this.score = score;
		this.sex = sex;
	}
	
	public Student(String name, int score, Sex sex, City city) {
		super();
		this.name = name;
		this.score = score;
		this.sex = sex;
		this.city = city;
	}
	
	public String getName() {
		return name;
	}

	public int getScore() {
		return score;
	}

	public Sex getSex() {
		return sex;
	}

	public City getCity() {
		return city;
	}

	@Override
	public String toString() {
		return "Student [name=" + name + ", score=" + score + ", sex=" + sex + ", city=" + city + "]";
	}

	@Override
	public int compareTo(Student o) {
		return Integer.compare(score,o.score);
	}
}

 

필터링한 요소 수집

- collect(Collector<T, A, R> collector)

- 필터링 또는 매핑된 요소들을 새로운 컬렉션에 수집하고 이를 리턴한다.

- T 요소를 A 누적기가 R에 저장한다는 의미

public class ToListExample {
	public static void main(String[] args) {
		List<Student> totalList = Arrays.asList(
			new Student("타곤",10, Student.Sex.MALE),
			new Student("태알하",20, Student.Sex.FEMALE),
			new Student("은섬",30, Student.Sex.MALE),
			new Student("탄야",40, Student.Sex.FEMALE)
		);
		
		//남학생들만 묶어서 List 생성
		List<Student> maleList = totalList.stream()
				.filter(s -> s.getSex()==Student.Sex.MALE)
				.collect(Collectors.toList());
		maleList.stream().forEach(s -> System.out.println(s.getName()));
//타곤
//은섬
		System.out.println();
		
		//여학생들만 묶어서 HashSet 생성
		Set<Student> femaleSet = totalList.stream().filter(s->s.getSex() == Student.Sex.FEMALE)
				.collect(Collectors.toSet());
                //.collect(Collectors.toCollection(HashSet::new)); 이런 형태도 가능
		femaleSet.stream().forEach(s -> System.out.println(s.getName()));
//탄야
//태알하
	}
}

- 다시한번 정리하자면 collect 메소드로 스트림을 묶어서 수집기인 Collector 객체의 메소드를 활용해서 특정 타입(List, Map, ConcurrentMap, Set)으로 반환할 수 있다.

- 여기서 ConcurrentMap은 멀티스레드 환경에서 사용하는 Map 계열이다. 스레드에 안전하다. 사용법은 기본 Map과 비슷하다.

 

사용자 정의 컨테이너에 수집

- List, Map, Set 같은 객체가 아닌 사용자가 직접 정의한 컨테이너 객체에 수집하는 방법.

- 직접 클래스로 만든다.

 

사용자 정의 컨테이너(남학생 클래스)

public class MaleStudent {
	private List<Student> list;
	
	public MaleStudent() {
		list = new ArrayList<Student>();
		System.out.println("["+Thread.currentThread().getName()+"] MaleStudent()");
	}
	
	public void accumulate(Student student) { //요소를 수집하는 메소드
		list.add(student);
		System.out.println("["+Thread.currentThread().getName()+"] accumulate()");
	}
	
	public void combine(MaleStudent other) { //두 MaleStudent를 결합하는 메소드(병렬  처리시에만 사용)
		list.addAll(other.getList());
		System.out.println("["+Thread.currentThread().getName()+"] combine()");
	}
	
	public List<Student> getList(){ //요소가 저장된 컬렉션을 리턴
		return this.list;
	}

	@Override
	public String toString() {
		return "MaleStudent [list=" + list + ", getList()=" + getList() + ", getClass()=" + getClass() + ", hashCode()="
				+ hashCode() + ", toString()=" + super.toString() + "]";
	}
}

- 위 MaleStudent는 list 변수에 Student객체들을 담을 것임.(남학생들만)

- accumulate 메소드는 매개변수로 들어온 Student 객체를 list에 저장.

- combine 메소드는 다른 MaleStudent 객체가 들어오면 list에 합치는 과정.

 

Stream<Student> totalStream = totalList.stream(); //컬렉션을 스트림으로

//남자인 학생들의 스트림 생성.
Stream<Student> maleStream = totalStream.filter(s -> s.getSex() == Student.Sex.MALE);

Suplier<MaleStudent> supplier = () -> new MaleStudent();
BiConsumer<MaleStudent, Student> accumulator = (ms, s) -> ms.accumulate(s);
BiConsumer<MaleStudent, MaleStudent> combiner = (ms1, ms2) -> ms1.combine(ms2);

MaleStudent maleStudent = maleStream.collect(supplier, accumulator, combiner);

 

>> MaleStudent maleStudent = totalList.stream().filter(s -> s.getSex() == Student.Sex.MALE).collect(MaleStudent::new, MaleStudent::accumulate, MaleStudent::combine);

으로 메소드 참조 형태로 표현해서 사용할 수도 있다.

 

요소를 그룹핑해서 수집

- collect 메소드는 요소수집 기능 외에 컬렉션의 요소들을 그룹핑해서 Map 객체를 생성하는 기능도 제공.

- collect를 호출할 때, Collectors의 groupingBy() 또는 groupingByConcurrent()가 리턴하는 Collector를 매개값으로 대입하면 된다.

- 전자는 스레드에 안전하지 않은 Map을 후자는 스레드에 안전한 concurrentMap을 생성한다.

 

public class GroupingByExample {
	public static void main(String[] args) {
		List<Student> totalList = Arrays.asList(
				new Student("타곤",10, Student.Sex.MALE, Student.City.Seoul),
				new Student("태알하",20, Student.Sex.FEMALE, Student.City.Pusan),
				new Student("은섬",30, Student.Sex.MALE, Student.City.Pusan),
				new Student("탄야",40, Student.Sex.FEMALE, Student.City.Seoul)
		);
		
		Map<Student.Sex, List<Student>> mapBySex = totalList.stream()
				.collect(Collectors.groupingBy(Student::getSex));
		
		System.out.println("[남학생] ");
		mapBySex.get(Student.Sex.MALE).stream().forEach(s->System.out.print(s.getName()+" "));
		
		System.out.println("\n[여학생] ");
		mapBySex.get(Student.Sex.FEMALE).stream().forEach(s-> System.out.print(s.getName()+" "));
		
		System.out.println();
		
		Map<Student.City, List<String>> mapByCity = totalList.stream()
				.collect(
					Collectors.groupingBy(
							Student::getCity, //객체에서 그룹핑할 요소
							TreeMap::new,
							Collectors.mapping(Student::getName, Collectors.toList()) 
							//City로 그룹핑하였을 때 Map에 value로 가지는 List의 String으로 들어갈 값., 콜렉터
						)
					);
		
		System.out.println("\n[서울] ");
		mapByCity.get(Student.City.Seoul).stream().forEach(s->System.out.print(s+" "));
		
		System.out.println("\n[부산] ");
		mapByCity.get(Student.City.Pusan).stream().forEach(s->System.out.print(s+" "));
		
	}
}

결과

[남학생]  
타곤 은섬  
[여학생]  
태알하 탄야  
[서울]  
타곤 탄야  
[부산]  
태알하 은섬

 

그룹핑 후 매핑 및 집계

- Collectors.groupingBy 메소드는 그룹핑 후, 매핑이나 집계(평균, 카운팅, 연결, 최대, 최소, 합계)를 집계할 수 있도록 두번째 매개값으로 Collector를 가질 수 있다.

 

예제 : 학생들을 성별로 그룹핑한 후, 점수를 평균내고 성별별로 이름을 반환한다.

public class GroupingAndReduction {
	public static void main(String[] args) {
		List<Student> totalList = Arrays.asList(
				new Student("타곤",10, Student.Sex.MALE),
				new Student("태알하",20, Student.Sex.FEMALE),
				new Student("은섬",30, Student.Sex.MALE),
				new Student("탄야",40, Student.Sex.FEMALE)
		);
		
		//성별로 점수를 저장하는 Map 얻기
		Map<Sex, Double> mapBySex = totalList.stream()
				.collect(
					Collectors.groupingBy(
						Student::getSex, //성별별로
						Collectors.averagingDouble(Student::getScore) //평균 집계내기
					)
				);
		System.out.println("남학생 평균 점수 : "+mapBySex.get(Student.Sex.MALE));
		System.out.println("여학생 평균 점수 : "+mapBySex.get(Student.Sex.FEMALE));
		
		//성별을 쉼표로 구분한 이름을 저장하는 Map 얻기
		Map<Student.Sex, String> mapByName = totalList.stream()
				.collect(
					Collectors.groupingBy(
						Student::getSex,
						Collectors.mapping(Student::getName, Collectors.joining(","))
						//"," 로 조인하여 요소들 구분하기.
						
					)
				);
		System.out.println("남학생 전체 이름 : "+mapByName.get(Student.Sex.MALE));
		System.out.println("여학생 전체 이름 : "+mapByName.get(Student.Sex.FEMALE));
	}
}