HashMap 문법에 대한 정리는 아래 포스트를 참조
https://velog.io/@seha01130/HashMap이란-메소드-종류-정리
기본적으로 HashMap은 순서를 보장하지 않기때문에 정렬이 필요하다면 별도의 조치가 필요하다.
아래처럼 <String, Integer> 해시맵이라고 가정하고 진행
Map<String, Integer> map = new HashMap<>();
map.put("banana", 3);
map.put("apple", 5);
map.put("cherry", 1);
ArrayList 형태로 변경
List<Map.Entry<String, Integer>> list = new ArrayList<>(map.entrySet());
key/value 기준에 따라 .getKey() ↔ .getValue()만 바꾸면 됨
list.sort(Comparator.comparing(Map.Entry::getKey));list.sort(Comparator.comparing(Map.Entry::getKey, String.CASE_INSENSITIVE_ORDER)); list.sort(Map.Entry.comparingByKey());
list.sort(Map.Entry.comparingByValue());
list.sort((a, b) -> a.getKey().compareTo(b.getKey())); list.sort((a, b) -> a.getKey().compareToIgnoreCase(b.getKey()));for (Map.Entry<String, Integer> entry : list) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
-----출력결과-----
apple = 5
banana = 3
cherry = 1
list.sort(Map.Entry.comparingByKey(Comparator.reverseOrder()));
list.sort(Map.Entry.comparingByValue(Comparator.reverseOrder()));
또는
🌟 list.sort((a, b) -> b.getKey().compareTo(a.getKey()));
🌟 list.sort((a, b) -> b.getKey().compareToIgnoreCase(a.getKey()));
💡 b - a 방식은 int 오버플로우 위험이 있어 Comparator.comparingInt() 또는 Integer.compare() 사용이 안전하다.
b - a 방식 :
// 내림차순 (큰 값 먼저) list.sort((a, b) -> b.getValue() - a.getValue()); // 오름차순 (작은 값 먼저) list.sort((a, b) -> a.getValue() - b.getValue());
list.sort(Comparator.comparingInt((Map.Entry<String, Integer> e) -> e.getValue()));
list.sort((a, b) -> Integer.compare(a.getValue(), b.getValue()));
list.sort(Comparator.comparingInt((Map.Entry<String, Integer> e) -> e.getValue()).reversed());
list.sort((a, b) -> Integer.compare(b.getValue(), a.getValue()));
근데 사실 현재 예시에서는 getValue()가 Comparable을 구현한 Integer 객체를 반환하기 때문에, 그 객체의 인스턴스 메서드인 .compareTo()를 사용해도 되긴 한다.
Map<String, Integer> sortedMap = new LinkedHashMap<>();
for (Map.Entry<String, Integer> entry : list) {
sortedMap.put(entry.getKey(), entry.getValue());
}
✔ LinkedHashMap은 삽입 순서 유지
✔ 정렬된 순서로 put() 하면 그 정렬 순서가 유지된다.
이렇게 하면 sortedMap은 정렬된 순서를 유지하는 맵이 된다.
방법2 를 사용하는걸 추천한다.
방법1은 그냥 방법2을 설명하기 위한 부가요소정도의 느낌으로 작성했다.
여러 개의 조건을 순서대로 적용해서 정렬할 때는 List로 변환한 뒤, Comparator를 연쇄적으로 연결(chaining) 하는 방식이 있다.
그러나 primitive 타입, 객체 타입을 신경써야하고 reversed()의 함정 등 신경쓸게 많다. 아래 내용에서 다 소개하고있다. 그럴떄는 2번째 방법인 if문을 사용하여 명시적으로 람다식을 써주는 방식을 사용하자.
예시
1순위: value 기준 숫자 내림차순
2순위: key 기준 문자열 오름차순
즉, value 기준 내림차순 후 값이 같을 경우 key 알파벳 순서대로 정렬
list.sort(Comparator
.comparingInt((Map.Entry<String, Integer> e) -> e.getValue()).reversed() // 1순위: value 내림차순
.thenComparing(Map.Entry::getKey) // 2순위: key 오름차순
);
Comparator.comparingInt(Map.Entry::getValue) // ❌ 오류
.comparingInt()은 int 값을 리턴하는 함수를 요구함. (ToIntFunction<T>)
그런데 Map.Entry::getValue는 Integer 객체를 리턴한다. (int 아님!)
Java는 Integer → int로 바꿔야 하는데, 메서드 참조에서는 타입이 명확하지 않으면 이걸 못 함.
그래서 "타입을 추론할 수 없다"고 오류가 난다.
따라서
Comparator.comparingInt((Map.Entry<String, Integer> e) -> e.getValue())
이렇게 명시해줘야함!
이건 Java에게 "나는 Map.Entry<String, Integer> 타입의 e에서 int 값 하나 꺼낼 거야!"라고 명확하게 알려주는 것.
그럼 Java가 "이건 int로 쓰이는구나!" 라고 제대로 인식할 수 있다.
Comparator.comparingInt(...)가 실행된 결과는 Comparator<Map.Entry<String, Integer>> 라는 타입이 명확한 객체(인스턴스) 이다.
thenComparing은 바로 이 '타입이 확정된 객체'에 대해 호출되는 인스턴스 메서드이다. 따라서 컴파일러는 당연히 다음에 올 메서드 참조(Map.Entry::getKey)도 Map.Entry에서 가져와야 한다는 것을 아주 쉽게 추론할 수 있으므로 즉, 제너릭 타입이 확정됐기 때문에 따로 명시할 필요가 없다.
* Map.Entry::getKey를 넣었을 때 getKey()가 String을 리턴하는 메서드인 걸 자동으로 추론함
comparing()은 Integer 같은 객체를 직접 다룰 수 있으므로, 컴파일러가 타입을 추론하는 데 아무런 문제가 없다.
즉, 메서드 참조를 그대로 사용할 수 있다!
list.sort(Comparator
// 1. comparing()으로 Integer 객체를 직접 비교
.comparing(Map.Entry::getValue, Comparator.reverseOrder()) // value 내림차순
.thenComparing(Map.Entry::getKey) // key 오름차순
);
Comparator.comparing(Map.Entry::getValue): getValue()가 반환하는 Integer 객체를 기준으로 오름차순 정렬하는 Comparator를 만든다.Comparator.reverseOrder(): 위에서 만든 오름차순을 뒤집어 내림차순으로 만든다.comparingInt와 람다 조합 대신 comparing과 메서드 참조 조합을 사용하는 것이 코드가 더 간결하고 직관적이게 된다.
예시를 보면 더 잘 이해가 될 것이다.
https://www.acmicpc.net/problem/20920
위의 백준 문제를 보면
1. 자주 나오는 단어일수록 앞에 배치한다.
2. 해당 단어의 길이가 길수록 앞에 배치한다.
3. 알파벳 사전 순으로 앞에 있는 단어일수록 앞에 배치한다
이렇게 3가지의 조건을 순서대로 우선순위를 적용하여 정렬해야한다.
단어와 빈도수를 HashMap hm에다가 넣어준 상태라고 가정하자.
그럼 위의 정렬을 위해 아래처럼 코드를 짤 수 있다.
List<String> words = new ArrayList<>(hm.keySet());
words.sort(Comparator
// 조건 1: 빈도수 내림차순
.comparingInt((String word) -> hm.get(word))
// 조건 2: 단어 길이 내림차순
.thenComparingInt(String::length).reversed()
// 조건 3: 알파벳 오름차순
.thenComparing(word -> word)
);
comparingInt, thenComparingInt, thenComparing의 인수는 "이걸 기준으로 비교해 줘!" 하고 비교할 대상을 꺼내오는 방법(함수) 을 넣어주는 것
각 메서드에 "어떤 키를 뽑아낼지"에 대한 방법(함수)을 인수로 전달하면, Java가 알아서 그 키들을 순서대로 비교해 전체 리스트를 정렬해 준다~
.comparingInt((String word) -> hm.get(word))hm.get(word)를 통해 각 단어의 빈도수(int)를 가져와 기준으로 삼는다..reversed()를 붙이지 않은 이유: reversed() 메서드는 바로 앞의 comparing에만 적용되는 것이 아니라, 지금까지 만들어진 전체 비교 체인(chain)을 통째로 뒤집는다. 따라서 조건2를 끝내고 한 번에 reversed()를 써주는 야매 방식을 사용한 것이다..thenComparingInt(String::length)String::length 메서드 참조를 통해 각 단어의 길이(int)를 기준으로 삼음.reversed()를 붙여 조건 1과 2를 모두 내림차순으로 만듦.thenComparing(word -> word)word -> word는 단어 자체(String)를 기준으로 삼으라는 의미String은 기본적으로 알파벳 오름차순으로 비교되므로, 원하는 결과가 나오게된다.하나의 sort 안에서 if문으로 우선순위 가르기
여러 조건을 처리하는 가장 기본적인 방법인 if문을 사용하여 명시적인 람다식을 작성하는 것이다.
이 방식은 위의 Comparator chaining에서 신경써야하는 primitive 타입과 객체 타입의 차이, .reversed()의 함정 등 복잡한 규칙을 신경 쓸 필요 없이 가장 직관적으로 코드를 작성할 수 있게 해준다.
이 또한 https://www.acmicpc.net/problem/20920 백준 문제를 예시로 들어 설명하겠다.
위의 백준 문제를 보면
1. 자주 나오는 단어일수록 앞에 배치한다.
2. 해당 단어의 길이가 길수록 앞에 배치한다.
3. 알파벳 사전 순으로 앞에 있는 단어일수록 앞에 배치한다
이렇게 3가지의 조건을 순서대로 우선순위를 적용하여 정렬해야한다.
단어와 빈도수를 HashMap hm에다가 넣어준 상태라고 가정하자.
그럼 위의 정렬을 위해 아래처럼 코드를 짤 수 있다.
ArrayList<Map.Entry<String, Integer>> list = new ArrayList<>(hm.entrySet());
list.sort((a,b) -> {
// 빈도수가 다르면 빈도수 내림차순
if (!a.getValue().equals(b.getValue())){
return b.getValue().compareTo(a.getValue());
}
// 빈도수가 같으면 길이가 긴 순서대로 내림차순
if (a.getKey().length() != b.getKey().length()){
return Integer.compare(b.getKey().length(), a.getKey().length());
}
// 앞의 두 조건이 모두 같으면 알파벳 사전 순 (오름차순)
return a.getKey().compareTo(b.getKey());
});
❓ 비교 결과를 즉시 return하고 정렬 로직을 종료?
두 단어 사이의 한 번의 비교가 종료되는 것.
sort 메서드는 리스트에 있는 모든 단어들을 토너먼트처럼 계속해서 비교한다. 우리가 만든 람다식은 그 토너먼트의 심판 역할을 한다.
sort는 람다식(심판)의 판정 결과를 받아서 두 단어의 순서를 정리한 뒤, 다시 리스트에서 다른 두 단어를 뽑아 람다식(심판)에게 또 보낸다. 이 과정을 리스트 전체가 정렬될 때까지 계속 반복한다.