1. 소개
현실 세계에서는 고유한 “키”마다 어떤 “값”을 대응시키는 상황을 자주 접합니다. 전화번호부는 사람의 이름과 전화번호를 저장하고, 사전은 단어와 그 번역을 연결하며, 성적표에서는 각 학생의 이름과 해당 점수가 짝을 이룹니다.
Java에서는 이런 작업을 위해 Map 인터페이스를 사용합니다. 이는 “키-값” 쌍(key-value pair)을 저장하는 컬렉션입니다.
Map의 핵심 속성:
- 각 키는 고유합니다(중복 불가).
- 하나의 키에는 오직 하나의 값만 매핑됩니다.
- 값은 중복될 수 있습니다.
비유를 들어 봅시다. 리스트(List)가 식당의 줄처럼 각자가 자신의 위치를 갖고 번호로 접근하는 구조라면, 맵(Map)은 사물함과 같습니다. 각 칸에는 번호(키)가 있고, 그 안에 무언가(값)가 들어 있습니다.
Map 인터페이스: 기본 연산
인터페이스 Map은 키-값 쌍을 다루기 위한 기본 메서드를 선언합니다:
| 메서드 | 설명 |
|---|---|
|
키에 값을 추가/대체 |
|
키로 값 가져오기 |
|
키로 엔트리 삭제 |
|
해당 키가 존재하는지 확인 |
|
해당 값이 존재하는지 확인 |
|
맵에 있는 엔트리 수 |
|
맵이 비어 있는지 확인 |
|
모든 엔트리 삭제 |
K와 V는 제네릭 타입 매개변수입니다. K(Key)는 키의 타입, V(Value)는 값의 타입을 의미합니다.
3. HashMap 클래스: 키로 빠르게 접근
HashMap이란?
HashMap은 Map 인터페이스의 가장 대중적인 구현입니다. 키로 값에 빠르게 접근할 수 있습니다.
중요: HashMap은 요소의 저장 순서를 보장하지 않습니다! 특정 순서로 키를 추가했더라도, 순회 시에는 다른 순서로 나올 수 있습니다.
HashMap을 생성하는 방법?
import java.util.HashMap;
import java.util.Map;
public class Example {
public static void main(String[] args) {
// 맵 생성: 키 — String, 값 — Integer
Map<String, Integer> ages = new HashMap<>();
// 요소 추가
ages.put("Vasya", 25);
ages.put("Petya", 30);
ages.put("Masha", 22);
// 키로 값 가져오기
int vasyaAge = ages.get("Vasya");
System.out.println("Vasya의 나이: " + vasyaAge); // 25
// 키 존재 여부 확인
if (ages.containsKey("Masha")) {
System.out.println("Masha가 목록에 있어요!");
}
// 요소 삭제
ages.remove("Petya");
// 모든 키-값 쌍 순회
for (String name : ages.keySet()) {
System.out.println(name + ": " + ages.get(name));
}
}
}
출력:
Vasya의 나이: 25
Masha가 목록에 있어요!
Vasya: 25
Masha: 22
HashMap의 특징
반드시 기억할 점: HashMap의 키는 항상 고유합니다. 이미 존재하는 키로 새 요소를 넣으면 기존 값은 새 값으로 대체됩니다.
값은 중복될 수 있습니다. 서로 다른 여러 키가 같은 값을 가리킬 수 있습니다.
또 하나 중요한 점은 요소의 순서입니다. HashMap은 추가 순서를 따르지 않습니다. 출력 시 레코드들이 섞여 보일 수 있는데 — 이는 정상 동작입니다.
4. TreeMap 클래스: 키로 정렬
HashMap과 달리, TreeMap은 요소를 키 기준으로 정렬된 상태로 저장합니다.
언제 TreeMap을 사용할까?
키가 오름차순(또는 내림차순)으로 정렬되어야 할 때 유용합니다. 예를 들어, 전화번호부를 알파벳 순으로 출력하고 싶을 때입니다.
예시:
import java.util.Map;
import java.util.TreeMap;
public class TreeMapExample {
public static void main(String[] args) {
Map<String, String> phoneBook = new TreeMap<>();
phoneBook.put("Vasya", "+1-900-123-45-67");
phoneBook.put("Masha", "+1-900-555-55-55");
phoneBook.put("Petya", "+1-900-222-33-44");
for (String name : phoneBook.keySet()) {
System.out.println(name + ": " + phoneBook.get(name));
}
}
}
출력:
Masha: +1-900-555-55-55
Petya: +1-900-222-33-44
Vasya: +1-900-123-45-67
주의: 키는 알파벳순으로 정렬됩니다.
5. Map의 기본 연산
추가 및 교체
Map<String, Integer> scores = new HashMap<>();
scores.put("Anna", 90);
scores.put("Ivan", 85);
scores.put("Anna", 95); // 'Anna'의 값을 덮어씀
값 가져오기
Integer annaScore = scores.get("Anna"); // 95
Integer unknown = scores.get("Vasya"); // 키가 없으면 null
키 또는 값 존재 여부 확인
scores.containsKey("Ivan"); // true
scores.containsValue(85); // true
키로 엔트리 삭제
scores.remove("Ivan");
크기와 비우기
int size = scores.size();
scores.clear(); // 모든 요소 삭제
5. Map 요소 순회
Map은 리스트가 아니므로 인덱스가 없습니다. 그러나 다음과 같이 순회할 수 있습니다:
키로 순회:
for (String key : scores.keySet()) {
System.out.println("키: " + key + ", 값: " + scores.get(key));
}
값으로 순회:
for (Integer value : scores.values()) {
System.out.println("값: " + value);
}
키-값 쌍으로 순회(가장 좋은 방법):
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " => " + value);
}
HashMap과 TreeMap은 언제 사용할까?
HashMap은 “기본값” 같은 범용 선택입니다. 키의 순서가 중요하지 않고 연산 속도가 가장 중요하다면 거의 항상 이것을 사용합니다.
TreeMap은 순서가 필요할 때 유용합니다. 키를 자동으로 정렬하여 저장하고, 최소/최대 키를 빠르게 찾거나 구간 연산을 수행할 수 있습니다.
정리: 90%의 경우 HashMap을 사용하고, 데이터가 처음부터 “정렬된” 상태여야 한다면 TreeMap을 사용합니다.
6. Map 사용 예시
예제 1: 전화번호부
Map<String, String> phoneBook = new HashMap<>();
phoneBook.put("Katya", "+1-999-111-22-33");
phoneBook.put("Oleg", "+1-999-222-33-44");
phoneBook.put("Katya", "+1-999-555-66-77"); // Katya의 예전 번호가 새 번호로 바뀐다
for (Map.Entry<String, String> entry : phoneBook.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
출력:
Oleg: +1-999-222-33-44
Katya: +1-999-555-66-77
예제 2: 단어 빈도 세기
단어 목록이 있고, 각 단어가 몇 번 등장하는지 알고 싶다고 가정해 봅시다:
import java.util.*;
public class WordCount {
public static void main(String[] args) {
List<String> words = Arrays.asList("사과", "바나나", "사과", "배", "바나나", "사과");
Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
int oldCount = counts.getOrDefault(word, 0); // 키가 없으면 0
counts.put(word, oldCount + 1);
}
System.out.println(counts); // {배=1, 사과=3, 바나나=2}
}
}
7. Map을 사용할 때 흔한 실수
실수 1: 키와 값을 혼동. 초보자는 리스트처럼 인덱스로 값을 가져오려 하거나, 키가 고유해야 한다는 점을 잊곤 합니다. Map에는 인덱스가 없고 — 오직 키만 있습니다.
실수 2: null 키와 값 사용. HashMap에서는 null 키가 허용되지만, TreeMap에서는 허용되지 않습니다( NullPointerException 발생). 값은 두 구현 모두에서 null일 수 있지만, 실무에서는 드물게 유용합니다.
실수 3: HashMap에서 요소의 순서를 기대. HashMap은 어떤 순서도 보장하지 않습니다. 순서가 필요하다면 LinkedHashMap(추가 순서를 보존) 또는 TreeMap(키로 정렬)을 사용하세요.
실수 4: 순회 중에 Map 수정. Map을 반복하면서 동시에 요소를 추가/삭제하면 ConcurrentModificationException이 발생할 수 있습니다. 이런 경우에는 remove()가 있는 이터레이터나 특수 컬렉션을 사용하세요.
실수 5: 키와 값을 ==로 비교하고 equals를 사용하지 않음. Map은 키(와 값) 비교에 equals 메서드를 사용합니다. 사용자 정의 클래스를 키로 쓸 경우 equals와 hashCode를 반드시 재정의하세요.
GO TO FULL VERSION