본문 바로가기
JAVA

[JAVA] 자바8 스트림 Map.Entry를 활용해 키(key) 또는 값(value)을 기준으로 Map(맵)정렬하기 2

by 딘딘은딘딘 2023. 9. 13.
반응형

이번 포스팅은 자바 8 스트림을 이용해 Map 정렬하기 두번째 이다.

지난 번에는 Map<String, Integer> 타입을 정렬하고, Collectors.toMap에 대한 사용법을 작성했다.

 

[JAVA] 자바8 스트림 Map.Entry를 활용해 키(key) 또는 값(value)을 기준으로 Map(맵)정렬하기 1

 

[JAVA] 자바8 스트림 Map.Entry를 활용해 키(key) 또는 값(value)을 기준으로 Map(맵)정렬하기 1

이번 포스팅은 자바 8 스트림(Stream)을 이용해 HashMap의 키 또는 Value를 기준으로 내림차순, 오름차순 정렬을 해본다. 테스트를 위해 Map의 key,value 타입은 Map 타입으로 설정 했으며, Key는 순번, value 0

anianidindin.tistory.com

 

이번에는 이어서 Map<String, VO> 를 Value 또는 key를 기준으로 오름차순, 내림차순 정렬하도록 한다.

 


* Map<String, VO(객체)> 정렬

객체를 정렬하기 위해서는 먼저 테스트를 위한 객체를 작성해야 한다.

 

Dish 라는 클래스를 작성하고 4개의 필드를 생성했다.

생성자로 한번에 객체를 생성할 예정이기 때문에 편의상 setter은 생성하지 않았다.

 

 

테스트를 위해 임의로 3개의 객체 생성했으며

dishMap.entrySet()으로 생성된 Map을 Set 인터페이스에 담아줬다.

 

그리고 LinkedHashMap<String, Dish>를 생성한 이유는

이전 글에서 설명한 toMap의 4번째 파라미터에 들어가는 default Map 을 넣기 위해 생성해놨다.

이전에는 생성자 참조 LinkedHashMap::new 를 통해 객체를 생성하고 바로 소진했는데

 

이번에는 toMap을 실행할때  이미 존재하는 map 변수를 대입하면 어떻게 되는지 궁금해 확인해보기 위해 작성했다.

 

- 객체의 오름차순 정렬

Comparator<Map.Entry<String, Dish>> compDish = 
        Map.Entry.comparingByValue(Comparator.comparing(Dish::getType));

sorted메서드에 전달할 함수형 인터페이스 분리

Map<String, Dish> dishSortMap = dishEntries.stream()
                .sorted(compDish)
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
                        (oldValue, newValue) -> oldValue, () -> emptyMap));
                        
for(String s : dishSortMap.keySet()) {
    System.out.println(dishSortMap.get(s));
}

 

dish의 Type를 기준으로 오름차순 정렬을 진행했다.

( 각각 메서드에 대한 설명은 이전 글에서 확인할 수 있다. )

 

sorted 메서드 안에 파라미터는 아래와 같이 바로 대입 할 수도 있지만,

정렬 조건이 다양해 질수록 코드가 복잡해져 따로 분리를 했다.

.sorted(Map.Entry.comparingByValue(Comparator.comparing(Dish::getType)))

( Key를 기준으로 정렬하고 싶다면 Map.Entry.comparingByKey() 를 사용하면 된다. )

 

 

실행 결과를 보면 Type를 기준으로 정렬이 된 것을 확인할 수 있다.

 

그리고 테스트를 위해 LinkedHashMap에 미리 put을 하고 

toMap의 4번째 인자에 대입하니 기존의 LinkedHashMap에 정렬된 Map 3개가 추가로 더해진 것을 확인 할 수 있었다.

 

궁금증은 해결 되었으니 다시 LinkedHashMap은 주석처리를 한다.

/*dish = new Dish("LinkedMapName!!!", true, 600, Dish.Type.OTHER);
emptyMap.put("LinkedMapKey!!!", dish);*/

 

- 객체의 내림차순 정렬

내림차순 정렬은 reversed()만 추가해주면 된다.

Comparator<Map.Entry<String, Dish>> compDish =
        Map.Entry.comparingByValue(Comparator.comparing(Dish::getType).reversed());

스트림을 사용해 dishEntries 를 Map으로 반환하는 로직은 오름차순 정렬과 같다. 

Comparator 함수형 인터페이스에 어떻게 정의하느냐에 따라

정렬 방식이 달라지기 때문에

나머지 스트림으로 Map을 반환하는 소스는 동일하다.

 

 

- 객체의 2가지 이상 기준 정렬

1. Dish의 Type 오름차순, 칼로리 오름차순 정렬

 

Comparator<Map.Entry<String, Dish>> compDish = 
        Map.Entry.comparingByValue(Comparator.comparing(Dish::getType)
                                            .thenComparing(Dish::getCalories));

Comparator.comparing 에 추가로 thenComparing()을 작성하면 된다.

더 많은 조건을 추가하고 싶다면 thenComparing을 추가하면 된다.

 

참고로 comparing 메서드의 경우 Function 함수형 인터페이스를 파라미터로 받는다.

위 코드의 메서드 참조는 Dish의 getType 이라는 함수(동작)를 리턴해준다.

메서드 참조는 람다의 축약형이기 때문에 람다로 작성하면 아래와 같다.

Comparator.comparing((Dish dish4) ->  dish4.getType())

 

실행 하면 Type의 오름차순 정렬 뒤 칼로리의 오름차순으로 정렬된다.

 

2. Dish의 Type 오름차순, 칼로리 내림차순 정렬

 

 

한 가지 기준으로 내림차순 정렬을 할 때는 .reversed() 메서드만 붙이면 간단히 해결되었었다.

 

그러나 두 가지 이상의 조건 + 두 번째 기준의 내림차순으로 정렬하면 

 

Comparator<Map.Entry<String, Dish>> compDish =
        Map.Entry.comparingByValue(Comparator.comparing(Dish::getType)
                .thenComparing((Dish::getCalories))
                .reversed());

아래와 같이 reversed() 만 추가 해주면 된다 라고 생각할 수도 있다.

 

하지만 reversed() 메서드를 사용할 경우

앞의 첫번째 정렬 기준 (타입의 오름차순)은 무시하고 두번째 정렬기준인 칼로리의 내림차순 정렬만 수행하게 된다.

// 위 코드의 결과
Dish{name='kimchi', vegetarian=true, calories=300, type=OTHER}
Dish{name='season', vegetarian=false, calories=120, type=OTHER}
Dish{name='pork', vegetarian=false, calories=800, type=MEAT}

 

 

따라서 아래와 같이

thenComparing에 첫번째 인자로 Function 추상 메서드를 주고,

두번째 인자에 Comparator로 정렬 기준을 인자로 전달해줘야 한다.

Comparator<Map.Entry<String, Dish>> compDish =
        Map.Entry.comparingByValue(Comparator.comparing(Dish::getType)
                                            .thenComparing((Dish::getCalories), Comparator.reverseOrder()));

 

결과는 아래와 같다.

// Dish Type의 오름차순 / Calories의 내림차순 정렬
Dish{name='pork', vegetarian=false, calories=800, type=MEAT}
Dish{name='kimchi', vegetarian=true, calories=300, type=OTHER}
Dish{name='season', vegetarian=false, calories=120, type=OTHER}

 

comparingByValue() 메서드의 경우 파라미터로 Comparator<? super V> 타입을 받는다.

그리고 Comparator의 thenComparing() 메서드는 Comparator<T>를 반환한다.

 

따라서 아래와 같이 내림차순 정렬하는 Comparator 인터페이스를 새로 정의 해서 사용하는게 가독성이 좋다.

Comparator<Dish> dishTpAscCalDescComp = Comparator.comparing(Dish::getType)
        .thenComparing((Dish::getCalories), Comparator.reverseOrder());

Comparator<Map.Entry<String, Dish>> compDish =
        Map.Entry.comparingByValue(dishTpAscCalDescComp);

 


- 이전에 작성한 1탄에서는 Map<String, Integer> 의 정렬과

스트림을 Map으로 변환하는 toMap 그리고 Map.Entry에 대해 공부했다.

 

- 2탄에서는 1탄의 내용에 이어서 Map<String, VO> 객체의 정렬에 대해 공부했다.

함수형 인터페이스와 람다에 대한 사용법을 조금씩 알게 되어가고 있는 느낌이다.

특히 스트림을 Map으로 반환하는 Collectors(컬렉터) 를 잘 활용하면 실무에서도 다양하게 활용 할 수 있을 것 같다.

 

다음은 Collectors.partitioningBy() 에 대해 다뤄 볼 예정이다.

반응형