11.4 Map: HashMap, LinkedHashMap, TreeMap
Map은 현실 세계의 "사전(Dictionary)" 과 같은 구조입니다. 단어(Key)를 찾으면 그에 대한 뜻(Value)이 나오는 것처럼, 특정 키(Key)로 값(Value)을 즉시 조회 할 수 있는 것이 Map의 가장 큰 강점입니다.
1. Map 인터페이스 특징
- Key 중복 불허: 같은 Key로 값을 다시 put하면 기존 값이 덮어씌워집니다.
- Value 중복 허용: 서로 다른 Key가 같은 Value를 가질 수 있습니다.
- null Key:
HashMap은 null Key를 1개 허용합니다.TreeMap은 불허합니다.
import java.util.HashMap;
import java.util.Map;
Map<String, Integer> map = new HashMap<>();
map.put("국어", 95);
map.put("수학", 88);
map.put("국어", 100); // 같은 Key → 기존 값(95) 덮어씌움
System.out.println(map.get("국어")); // 100
System.out.println(map.get("체육")); // null (없는 키)
2. HashMap
가장 많이 쓰이는 Map 구현체입니다. 키(Key)는 중복 불가, 값(Value)은 중복 가능 합니다. 또한 HashSet처럼 저장 순서를 보장하지 않습니다.
주요 메서드 완전 정리
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Collection;
public class HashMapMethods {
public static void main(String[] args) {
HashMap<String, Integer> scoreMap = new HashMap<>();
// ========== 추가/수정 ==========
scoreMap.put("국어", 95);
scoreMap.put("수학", 88);
scoreMap.put("영어", 92);
scoreMap.put("과학", 88); // 값 중복은 허용
scoreMap.put("국어", 100); // 같은 키 → 덮어씀
System.out.println("전체: " + scoreMap);
// ========== 조회 ==========
System.out.println("수학: " + scoreMap.get("수학")); // 88
System.out.println("체육: " + scoreMap.get("체육")); // null
// getOrDefault: 키가 없을 때 기본값 반환 (null 방지)
System.out.println("체육(기본0): " + scoreMap.getOrDefault("체육", 0)); // 0
// ========== 포함 여부 ==========
System.out.println("영어 키 있음?: " + scoreMap.containsKey("영어")); // true
System.out.println("999 값 있음?: " + scoreMap.containsValue(999)); // false
System.out.println("크기: " + scoreMap.size()); // 4
System.out.println("비어있음?: " + scoreMap.isEmpty()); // false
// ========== 삭제 ==========
scoreMap.remove("과학"); // 키로 삭제
scoreMap.remove("영어", 99); // 키+값이 정확히 일치할 때만 삭제 (조건부 삭제)
System.out.println("삭제 후: " + scoreMap);
// ========== 키/값 뷰 ==========
Set<String> keys = scoreMap.keySet(); // 키 집합
Collection<Integer> values = scoreMap.values(); // 값 컬렉션
Set<Map.Entry<String, Integer>> entries = scoreMap.entrySet(); // 키-값 쌍 집합
System.out.println("키: " + keys);
System.out.println("값: " + values);
// entrySet으로 순회 (가장 효율적)
System.out.println("=== 전체 순회 ===");
for (Map.Entry<String, Integer> entry : entries) {
System.out.printf(" %s: %d점%n", entry.getKey(), entry.getValue());
}
// ========== 전체 초기화 ==========
scoreMap.clear();
System.out.println("clear 후 size: " + scoreMap.size()); // 0
}
}
편의 메서드 (Java 8+)
import java.util.HashMap;
import java.util.Map;
public class HashMapConvenienceMethods {
public static void main(String[] args) {
HashMap<String, Integer> wordCount = new HashMap<>();
// putIfAbsent: 키가 없을 때만 put
wordCount.putIfAbsent("hello", 1);
wordCount.putIfAbsent("hello", 999); // 이미 있으므로 무시
System.out.println("putIfAbsent: " + wordCount); // {hello=1}
// computeIfAbsent: 키가 없을 때 계산해서 put
wordCount.computeIfAbsent("world", k -> k.length()); // 키 없으면 길이로 초기화
System.out.println("computeIfAbsent: " + wordCount); // {hello=1, world=5}
// compute: 키-값을 함수로 변환 (키 있든 없든 실행)
wordCount.compute("hello", (k, v) -> v == null ? 1 : v + 1);
wordCount.compute("hello", (k, v) -> v == null ? 1 : v + 1);
System.out.println("compute: " + wordCount); // {hello=3, world=5}
// merge: 키가 없으면 put, 있으면 기존값과 병합
wordCount.merge("hello", 1, Integer::sum); // hello: 3 + 1 = 4
wordCount.merge("java", 1, Integer::sum); // java: 없으므로 1
System.out.println("merge: " + wordCount); // {hello=4, world=5, java=1}
// replaceAll: 모든 값을 함수로 일괄 변환
wordCount.replaceAll((k, v) -> v * 10);
System.out.println("replaceAll(*10): " + wordCount); // {hello=40, world=50, java=10}
// forEach: 모든 항목 순회 (람다)
System.out.println("=== forEach ===");
wordCount.forEach((k, v) -> System.out.printf(" %s=%d%n", k, v));
}
}
3. LinkedHashMap
LinkedHashMap은 HashMap과 동일하지만 삽입 순서를 유지 합니다. 추가로 accessOrder 옵션을 활성화하면 최근 접근 순서로 정렬되어 LRU 캐시 구현에 활용할 수 있습니다.
삽입 순서 유지
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapExample {
public static void main(String[] args) {
LinkedHashMap<String, Integer> scores = new LinkedHashMap<>();
scores.put("국어", 95);
scores.put("수학", 88);
scores.put("영어", 92);
scores.put("과학", 87);
// 삽입 순서 그대로 출력
System.out.println(scores); // {국어=95, 수학=88, 영어=92, 과학=87}
// HashMap은 순서 보장 없음
java.util.HashMap<String, Integer> hashMap = new java.util.HashMap<>(scores);
System.out.println(hashMap); // 순서 다를 수 있음
}
}
LRU 캐시 구현
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
// accessOrder=true: get/put 시 해당 항목이 맨 뒤로 이동 (가장 최근 사용)
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 크기가 capacity를 초과하면 가장 오래된(앞의) 항목 자동 삭제
return size() > capacity;
}
public static void main(String[] args) {
LRUCache<Integer, String> cache = new LRUCache<>(3);
cache.put(1, "페이지1");
cache.put(2, "페이지2");
cache.put(3, "페이지3");
System.out.println("초기: " + cache); // {1=페이지1, 2=페이지2, 3=페이지3}
cache.get(1); // 1번 페이지 접근 → 1이 맨 뒤로 이동 (최근 사용)
cache.put(4, "페이지4"); // 용량 초과 → 가장 오래된 2번 제거
System.out.println("4 추가 후: " + cache); // {3=페이지3, 1=페이지1, 4=페이지4}
}
}
LinkedHashMap LRU 캐시
LinkedHashMap의 accessOrder=true + removeEldestEntry 오버라이드 조합은 간단한 LRU(Least Recently Used) 캐시를 구현하는 가장 간단한 방법입니다.
4. TreeMap
TreeSet처럼, TreeMap은 키(Key)를 기준으로 자동 정렬 해서 보관합니다. 내부적으로 Red-Black 트리를 사용하며 NavigableMap 인터페이스를 구현합니다.
import java.util.TreeMap;
import java.util.Map;
public class TreeMapExample {
public static void main(String[] args) {
TreeMap<String, Integer> sortedMap = new TreeMap<>();
sortedMap.put("banana", 2);
sortedMap.put("apple", 5);
sortedMap.put("cherry", 1);
sortedMap.put("date", 3);
// 키(알파벳) 기준 자동 정렬
System.out.println(sortedMap); // {apple=5, banana=2, cherry=1, date=3}
// NavigableMap 기능
System.out.println("첫 번째 키: " + sortedMap.firstKey()); // apple
System.out.println("마지막 키: " + sortedMap.lastKey()); // date
// ceiling: 주어진 키보다 크거나 같은 최솟값 키
System.out.println("'b' 이상 첫 키: " + sortedMap.ceilingKey("b")); // banana
// floor: 주어진 키보다 작거나 같은 최댓값 키
System.out.println("'c' 이하 마지막 키: " + sortedMap.floorKey("c")); // cherry
// 범위 SubMap
Map<String, Integer> subMap = sortedMap.subMap("banana", true, "cherry", true);
System.out.println("banana~cherry 범위: " + subMap); // {banana=2, cherry=1}
// headMap: 주어진 키 미만
System.out.println("cherry 미만: " + sortedMap.headMap("cherry")); // {apple=5, banana=2}
// tailMap: 주어진 키 이상
System.out.println("cherry 이상: " + sortedMap.tailMap("cherry")); // {cherry=1, date=3}
// 역순 정렬
System.out.println("역순: " + sortedMap.descendingMap());
// {date=3, cherry=1, banana=2, apple=5}
}
}
5. Map 구현체 비교표
| 비교 항목 | HashMap | LinkedHashMap | TreeMap |
|---|---|---|---|
| 내부 구조 | 해시 테이블 | 해시 테이블 + 연결 리스트 | Red-Black 트리 |
| 키 순서 | 없음 | 삽입/접근 순서 유지 | 정렬 순서 유지 |
put 성능 | O(1) | O(1) | O(log n) |
get 성능 | O(1) | O(1) | O(log n) |
| null Key | 허용 (1개) | 허용 (1개) | 불허 |
| 범위 검색 | 불가 | 불가 | 가능 (NavigableMap) |
| 용도 | 일반적인 키-값 저장 | 순서 유지 캐시, LRU | 정렬된 키 조회 |
6. Map.of()와 Map.entry() (Java 9+)
import java.util.Map;
public class ImmutableMap {
public static void main(String[] args) {
// Map.of(): 최대 10개 키-값 쌍으로 불변 Map 생성
Map<String, Integer> scores = Map.of(
"국어", 95,
"수학", 88,
"영어", 92
);
System.out.println(scores);
System.out.println("국어: " + scores.get("국어")); // 95
// scores.put("과학", 87); // UnsupportedOperationException!
// Map.ofEntries(): 10개 초과 시 사용
Map<String, String> capitals = Map.ofEntries(
Map.entry("Korea", "Seoul"),
Map.entry("Japan", "Tokyo"),
Map.entry("USA", "Washington D.C."),
Map.entry("France", "Paris"),
Map.entry("Germany", "Berlin")
);
System.out.println("한국 수도: " + capitals.get("Korea")); // Seoul
// Map.copyOf(): 기존 Map을 불변으로 복사
java.util.HashMap<String, Integer> mutable = new java.util.HashMap<>();
mutable.put("A", 1);
mutable.put("B", 2);
Map<String, Integer> immutableCopy = Map.copyOf(mutable);
mutable.put("C", 3); // 원본 변경
System.out.println("불변 복사본: " + immutableCopy); // {A=1, B=2} (영향 없음)
}
}
7. 실전 예제 1: 단어 빈도수 카운터
import java.util.HashMap;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.stream.Collectors;
public class WordFrequencyCounter {
public static void main(String[] args) {
String text = "the quick brown fox jumps over the lazy dog " +
"the fox was quick the dog was lazy";
String[] words = text.split(" ");
// 단어 빈도수 계산
HashMap<String, Integer> freq = new HashMap<>();
for (String word : words) {
// merge 활용: 없으면 1, 있으면 기존값 + 1
freq.merge(word, 1, Integer::sum);
}
System.out.println("전체 빈도수: " + freq);
// 빈도수 내림차순 정렬하여 출력
System.out.println("\n=== 빈도수 순위 ===");
freq.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed()
.thenComparing(Map.Entry.comparingByKey()))
.forEach(e -> System.out.printf(" %-10s: %d회%n", e.getKey(), e.getValue()));
// 특정 단어만 필터링 (2회 이상 등장)
System.out.println("\n=== 2회 이상 단어 ===");
freq.entrySet().stream()
.filter(e -> e.getValue() >= 2)
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.forEach(e -> System.out.printf(" %s: %d회%n", e.getKey(), e.getValue()));
// 가장 많이 등장한 단어
String mostFrequent = freq.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse("없음");
System.out.println("\n가장 많이 등장한 단어: " + mostFrequent + " (" + freq.get(mostFrequent) + "회)");
}
}
8. 실전 예제 2: 학생 성적 관리 Map
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class StudentScoreMap {
record StudentInfo(String name, int score, String subject) {}
public static void main(String[] args) {
// 학생-점수 Map
HashMap<String, Map<String, Integer>> studentScores = new HashMap<>();
// 학생별 과목 점수 등록
addScore(studentScores, "김철수", "국어", 85);
addScore(studentScores, "김철수", "수학", 92);
addScore(studentScores, "김철수", "영어", 78);
addScore(studentScores, "이영희", "국어", 95);
addScore(studentScores, "이영희", "수학", 88);
addScore(studentScores, "이영희", "영어", 91);
addScore(studentScores, "박민준", "국어", 72);
addScore(studentScores, "박민준", "수학", 65);
addScore(studentScores, "박민준", "영어", 80);
System.out.println("=== 전체 성적 ===");
studentScores.forEach((student, subjects) -> {
System.out.printf("[%s] %s%n", student, subjects);
});
System.out.println("\n=== 학생별 평균 점수 (내림차순) ===");
studentScores.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().values().stream()
.mapToInt(Integer::intValue).average().orElse(0)
))
.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.forEach(e -> System.out.printf(" %s: %.1f점%n", e.getKey(), e.getValue()));
System.out.println("\n=== 국어 성적 순위 ===");
// 국어 점수 TreeMap으로 정렬
TreeMap<Integer, String> koreanRank = new TreeMap<>((a, b) -> b - a); // 내림차순
studentScores.forEach((student, subjects) -> {
koreanRank.put(subjects.get("국어"), student);
});
int rank = 1;
for (Map.Entry<Integer, String> entry : koreanRank.entrySet()) {
System.out.printf(" %d위: %s (%d점)%n", rank++, entry.getValue(), entry.getKey());
}
// 특정 학생 성적 조회
System.out.println("\n=== 김철수 성적 조회 ===");
Map<String, Integer> cheolsuScores = studentScores.getOrDefault("김철수", Map.of());
cheolsuScores.forEach((subject, score) ->
System.out.printf(" %s: %d점%n", subject, score));
// 총점 계산
int total = cheolsuScores.values().stream().mapToInt(Integer::intValue).sum();
System.out.printf(" 총점: %d점, 평균: %.1f점%n", total, (double) total / cheolsuScores.size());
}
static void addScore(HashMap<String, Map<String, Integer>> map, String student, String subject, int score) {
map.computeIfAbsent(student, k -> new LinkedHashMap<>()).put(subject, score);
}
}
9. Map 순회 방법 총정리
import java.util.HashMap;
import java.util.Map;
public class MapIterationSummary {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
// 방법 1: entrySet() - 키와 값 모두 필요할 때 (가장 효율적)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
// 방법 2: keySet() - 키만 필요하거나 키로 값을 조회할 때
for (String key : map.keySet()) {
System.out.println(key + " -> " + map.get(key));
}
// 방법 3: values() - 값만 필요할 때
for (int value : map.values()) {
System.out.println(value);
}
// 방법 4: forEach() 람다 - 간결하게 (Java 8+)
map.forEach((k, v) -> System.out.println(k + ": " + v));
// 방법 5: Stream + entrySet - 필터링/변환이 필요할 때
map.entrySet().stream()
.filter(e -> e.getValue() > 1)
.forEach(e -> System.out.println(e.getKey() + "=" + e.getValue()));
}
}
Map 순회 최적화
entrySet()순회가 가장 효율적 입니다.keySet()+map.get(key)조합은 두 번의 해시 조회가 발생합니다.- 값만 필요하다면
values()를 사용하세요. - Java 8 이상에서는
forEach()가 가장 간결합니다.
이로써 자바의 3대 컬렉션(List, Set, Map)과 그 구현체들을 모두 마스터했습니다. 이 세 가지를 상황에 맞게 선택해서 쓰는 것만으로도 대부분의 실무 데이터 처리 문제를 해결할 수 있습니다.