이번 글에서는 Collector의 그룹화에 대해서 알아보겠습니다.

먼저 그동안 사용했던 games 객체 리스트를 조금 수정해보겠습니다.

 

type 속성을 추가하고, getter/setter 메서드를 추가했습니다.

그리고 생성자는 기존 것 외에 type을 추가하여 3가지 매개변수를 받는 생성자를 추가했습니다.,

package demo;

public class Game {
	private String name;
	private int price;
	private String type;
	
	public Game(String name, int price) {
		this.name = name;
		this.price = price;
	}
	
	public Game(String name, int price, String type) {
		this.name = name;
		this.price = price;
		this.type = type;
	}
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getPrice() {
		return price;
	}
	public void setPrice(int price) {
		this.price = price;
	}

	public String getType() {
		return type;
	}

	public void setType(String type) {
		this.type = type;
	}
}

 

groupingBy

데이터를 하나 이상의 특성으로 분류하여 그룹화는 연산을 지원합니다.

아라 예제는 groupingBy를 활용하여 game 타입 별 그룹 연산을 하고 출력하여 결과를 확인해봅니다.

Map<String, List<Game>> gamesByType = games.stream().collect(groupingBy(Game::getType));
gamesByType.get("RPG").stream().forEach(game -> System.out.println(game.getName()));
gamesByType.get("Shooting").stream().forEach(game -> System.out.println(game.getName()));

결과값

DIA 3
DIA 2
DIA 1
CALL 1

 

인수로 메서드 참조 대신 람다 표현식으로 구현도 가능합니다. 

결과 값은 동일합니다.

Map<String, List<Game>> rpgGame = games.stream().collect(groupingBy(game -> {
	if(game.getType().equals("RPG")) return "RPG"; else return "Shooting";
}));

rpgGame.get("RPG").stream().forEach(game -> System.out.println(game.getName()));
gamesByType.get("Shooting").stream().forEach(game -> System.out.println(game.getName()));

 

그룹화 이후 연산이 하고 싶다면 filtering 등을 이용할 수 있지만 groupingBy 전에 사용하게 되면

특정 결과만 도출하고 반대되는 그룹 값들은 없어지게 된다.

예를 들면 RPG를 filtering 하고 groupingBy 하게 되면 RPG 결과값만 남고 Shooting 값은 사라지게 됩니다.

다음 예제와 같이 groupingBy에 인수로 filtering을 사용하면 비어있는 그룹의 값으로 결과 값이 반환됩니다.

40,000 초과하는 가격을 가진 게임 목록에 대해서 filtering 하고 그룹화합니다.

Map<String, List<Game>> priceGameByType = games.stream()
	collect(groupingBy(Game::getType, filtering(game -> game.getPrice() > 40000, toList())));
    
priceGameByType.forEach((k,v) -> {
	System.out.print(k + " : ");
	v.forEach(game -> System.out.println(game.getName()));
	System.out.println();
});

결과값

Shooting 목록은 해당되는 요소가 없지만 그렇다고 해당 그룹 자체 없이 결과 값이 반환되지는 않는 것이 포인트입니다.

Shooting : 
RPG : DIA 1

 

filtering 외에 mapping 등 다양한 응용이 가능합니다.

다음 예제는 앞에서 정리했던 joining 메서드를 사용하여 각 요소들을  "," 구분자로 합쳐서 결과 값을 그룹화하여

출력해보겠습니다.

Map<String, String> gameNameByType = games.stream()
	.collect(groupingBy(Game::getType, mapping(Game::getName, joining(","))));
    
gameNameByType.forEach((k,v) -> {
	System.out.print(k + " : " + v);
	System.out.println();
});

결과값

Shooting : CALL 1
RPG : DIA 3,DIA 2,DIA 1

 

다음 예제는 counting 메서드를 사용하여 그룹화된 값들에 대해 합계를 구하여 결과를 반환합니다.

Map<String, Long> typesCount = games.stream().collect(groupingBy(Game::getType, counting()));

typesCount.forEach((k,v) -> {
	System.out.print(k + " : " + v);
	System.out.println();
});

결과값

Shooting : 1
RPG : 3

 

다음은 2번 groupingBy를 사용하여 여러 번 그룹화를 적용해보겠습니다.

Map<String, Map<String, List<Game>>> gameMultiGrouping = games.stream().collect(groupingBy(Game::getType,
		groupingBy(game -> {
			if(game.getPrice() > 20000) {
				return "RPG";
			} else {
				return "Shooting";
			}
		})
	)
);
		
gameMultiGrouping.forEach((k,v) -> {
	System.out.println("<"+k+">");
	v.get(k).forEach(game -> System.out.println(game.getName()));
	System.out.println();
});

결과값

<Shooting>
CALL 1

<RPG>
DIA 3
DIA 2
DIA 1

 

collectingAndThen

다음 예제는 maxBy를 사용하여  각각 그룹에 가장 비싼 게임을 결과값을 반환하겠습니다.

여기서 결과 값은 collectingAndThen을 사용하여 다른 형식으로 변환합니다.

참고할 점은 리듀싱 컬렉터의 경우에는 Optional.empty()를 반환하지 않으므로 안전합니다.

Map<String, Game> mostPriceBytype = games.stream().collect(groupingBy(Game::getType,
collectingAndThen(maxBy(Comparator.comparingInt(Game::getPrice)), Optional::get)));

결과값

Shooting : CALL 1
RPG : DIA 1

 

'Java > Stream' 카테고리의 다른 글

Collector 분할  (0) 2022.11.08
Collector 요약  (0) 2022.10.28
스트림 생성  (0) 2022.10.27
기본형 특화 스트림  (0) 2022.10.27
리듀싱  (0) 2022.10.26

+ Recent posts