"Top 9 questions about Java Maps"라는 글을 번역한 내용이다.
(https://www.programcreek.com/2013/09/top-9-questions-for-java-map/)
이 포스팅은 자바 클래스 내부에서 맵을 사용할 때 질문했던 질문들 중에서 대표적인 질문 9개를 요약한 내용입니다. 의역과 오역이 많으니 참고해 주세요.
일반적으로 맵은 [Key-Value] 쌍으로 이루어져 있고, Key는 해당 맵 내부에서 유일한 식별 값이다. 참고로 아래에 예제들은 편의를 위해 특별한 타입을 정의하지 않고 "K" 혹은 "V"로 표기하였다.
0. 맵을 리스트로 변경하기 (Convert a Map to a List)
자바에서 맵 인터페이스는 [Key], [Value], [Key-Value] 이렇게 3가지 컬렉션을 제공한다. 이 컬렉션은 생성자 혹은 addAll() 메서드를 통해 모두 List 자료형으로 변환이 가능하다. 아래 예제 코드는 Map을 ArrayList로 만들어 주는 예제이다.
// Key List
List keyList = new ArrayList(map.keySet());
// Value List
List valueList = new ArrayList(map.values());
// Key-Value List
List entryList = new ArrayList(map.entrySet());
1. 반복문 사용하기 (Iterator over a Map)
맵의 Key-Value를 다루는 방법 중에 가장 기초적인 연산자는 반복문이다. Map.entrySet() 메서드를 통해 맵 내부에 저장된 Key-Value Set를 조회할 수 있다. 아래는 예제는 반복문을 통해 맵 내부에 존재하는 Key, Value를 조회하는 예제이다.
for (Entry entry : map.entrySet()) {
//get key
K key = entry.getKey();
// get value
V value = entry.getValue();
}
참고로, JDK.1.5 이전에는 아래와 같은 방법으로 조회를 했었다.
Iterator itr = map.entrySet().iterator();
while(itr.hasNext()) {
Entry entry = itr.next();
// get key
K key = entry.getKey();
// get value
V value = entry.getValue();
}
2. 키 기준으로 맵 정렬 하기 (Sort a Map on the keys)
키를 기준으로 하는 맵 정렬은 자주 쓰이는 방법이다. 맵을 리스트에 넣고 리스트의 대상을 Comparator 클래스를 사용하여 정렬한다.
List list = new ArrayList(map.entrySet());
Collections.sort(list, new Comparator() {
@Override
public int compare(Entry e1, Entry e2) {
return e1.getKey().compareTo(e2.getKey());
}
});
다른 방법으로는 SortedMap을 사용해도 무관하다. SortedMap 같은 경우는 맵에 자료를 넣을 때 정렬을 보장한다. 이렇게 정렬을 할 수 있게 해주는 클래스를 만들 수 있다. Comparable를 상속받은 구현체 클래스(직접 재 정의 가능)를 사용하거나, Comparator 클래스를 사용하면 된다.
추가로 SortedMap로 구현된 클래스 중에 TreeMap이라는 게 있는데, 이 클래스 역시 Comparator를 사용할 수 있다. 아래 코드는 일반적으로 사용하는 Map을 SortedMap으로 변경하는 방법의 예를 보여준다.
SortedMap sortedMap = new TreeMap(new Comparator() {
@Override
public int compare(K k1, K k2) {
return k1.compareTo(k2);
}
});
sortedMap.putAll(map);
3. 값 기준으로 맵 정렬 하기 (Sort a Map on the values)
키 기준 정렬과 비슷한 케이스이지만 다른 점은 값을 기준으로 정렬한다는 점이다. 기준 값은 Entry.getValue()를 사용한다.
List list = new ArrayList(map.entrySet());
Collections.sort(list, new Comparator() {
@Override
public int compare(Entry e1, Entry e2) {
return e1.getValue().compareTo(e2.getValue());
}
});
우리는 값을 기준으로 맵을 정렬할 수 있지만, 정렬의 기준이 되는 값은 중복이 없는 게 좋다. 또한 맵 자료 구조를 [키=값]을 [값=키]으로 변경이 가능하지만 추천하지 않는 방법이다. (키 값에 중복이 존재하면 자료를 다루기가 힘들어진다.)
4. 스태틱 혹은 불변 맵의 초기화 (Initailize a static/immutable Map)
맵을 상수(Constant)처럼 쓰길 원한다면 불변 맵 (Immutable Map)은 좋은 방법이 될 수 있다. 이 방법은 방어적인 프로그래밍 예를 들어 안전하게 인스턴스를 생성하거나 스레드에 안전한 맵을 구성할 때 좋다. 스태틱/불변 맵을 초기화 하기 위해서는 아래와 같이 static 필드를 사용해야 한다. 하지만 이 코드의 문제는 맵이 static final 선언이 되었음에도 불구하고 여전히 초기화 후에 사용해야 한다. 만약 아래 코드에 T1.map.put(3, "three")을 사용할 수 있지만 이것은 진정한 의미의 불변(Immutable)은 아니다.
그렇다면 스태틱 필드를 사용하여 불변(Immutable)을 만들려면 어떻게 해야 할까? 익명 클래스를 하나 만들고 초기화의 마지막 단계에서 수정할 수 없는 맵에 복사한다. 아래의 두 번째 코드를 참고하면 된다.
만약 T2.map.put(3, "three")를 실행하면 "UnsupportedOperationException" 이 발생한다.
public class Test {
static class T1 {
public static final Map map;
static {
map = new HashMap();
map.put(1, "one");
map.put(2, "two");
}
}
static class T2 {
public static final Map map;
static {
Map aMap = new HashMap();
aMap.put(1, "one");
aMap.put(2, "two");
map = Collections.unmodifiableMap(aMap);
}
}
// 사용
public static void main(String[] args) {
T1.map.put(3, "three");
T2.map.put(3, "three"); // java.lang.UnsupportedOperationException
System.out.println(T1.map.entrySet());
System.out.println(T2.map.entrySet());
}
}
참고로 Guava 라이브러리는 스태틱과 불변 컬렉션 다른 방법으로 초기화할 수 있다. (참고 사이트: https://github.com/google/guava)
5. HashMap vs TreeMap vs HashTable 차이점
HashMap, TreeMap, HashTable은 맵 인터페이스의 구현체이다. 3개의 클래스의 차이점은 아래와 같이 나타낼 수 있다.
- 반복 순서 (The order of iterator)
HashMap과 Hashtable은 맵 자료 순서를 보장하지 않는다. 하지만 TreeMap은 전체 엔트리(Key-Value)에 대해 순서를 보장한다. 이를 맵 키에 대한 "natural ordering"이라고 한다. - 키-값 (Key-Value) 접근
HashMap은 null key와 null value를 허용한다. (중복 키를 허용하기 않기 때문에 null key는 1개만 존재한다.) 반면에 Hashtable은 null key와 null value를 허용하지 않는다. 순서를 보장하는 TreeMap 같은 경우는 null을 허용하지 않음으로 예외가 발생하게 된다. - 동기화 (Synchronized)
Hashtable만 유일하게 동기화가 가능하다. 만약 스레드에 안정성이 요구되지 않는 상황이라면 HashMap을 추천한다.
표로 정리를 하게 되면 아래와 같다.
HashMap | Hashtable | TreeMap | |
Iterator order | no | no | yes |
null key-value | yes-yes | no-no | no-yes |
synchronized | no | yes | no |
time performance | O(1) | O(1) | O(lon n) |
implementation | buckets | buckets | red-black tree |
6. A Map with reverse view/lookup
가끔 맵에서 key-key 쌍이 필요한 경우가 있다. 이게 가능하려면 key와 value 모두 유일한 값으로 구성이 되어야 한다. 이러한 제약조건이 맵에서 "inverse lookup/view"를 만들 수 있게 해 준다. 따라서 우리는 value 기준으로 key를 찾을 수 있게 된다.
Apache Common Collections과 Guava에서는 BidiMap과 BiMap과 같은 양방향 맵(bidirectional map)을 제공한다. 이 또한 key-value의 1:1 조건을 만족한다.
7. 얕은 복사 (Shallow Copy of a Map)
전부는 아니지만 자바 내의 맵은 다른 맵으로 복사할 수 있는 생성자를 제공한다. 하지만 복사는 동기화되지 않는다. 이른 한 개의 스레드에서 맵을 복사하는 경우 다른 쪽에서 맵의 구조 변경이 가능하다는 것을 의미하기도 한다. 이러한 동기화되지 않는 상황을 막기 위해 아래 코드와 같이 Collections.synchronizedMap()을 사용한다.
Map copiedMap = Collections.synchronizedMap(map);
얕은 복사의 방법 중에 재미있는 방법이 있는데, clone() 메서드를 사용하는 것이다. 하지만 이 방법은 Java collection framework을 고안한 Josh Bloch 추천하지 않는 방법이다. (Effective java에서도 clone() 메서드를 언급을 하였다.)
8. 빈 맵 생성 (Create an empty Map)
만약 맵이 불변(Immutable)이면 아래와 같이 사용하면 된다.
map = Collections.emptyMap();
위와 다르게 불변(Immutable)이 아닌 경우라면 아래와 같이 사용한다.
map = new HashMap();