숑숑이의 개발일기

본글은 'Do it 자바 완전 정복' 책을 통한 공부내용을 정리한 글입니다.

 

Map<K, V> 컬렉션은 상속 구조상 List<E>, Set<E>와 분리되어 있다. 즉, List<E>와 Set<E>가 Collection<E> 인터페이스를 상속받는 반면, Map<K, V>는 별도의 인터페이스로 존재한다.

 

Map<K, V> 컬렉션의 특징

1) Key와 Value 한 쌍으로 데이터를 저장

Map<K, V> 컬렉션은 Key(키)와 Value(값)의 한 쌍으로 데이터를 저장한다. 한 쌍의 데이터를 엔트리(entry)라고 하며, Map.Entry 타입으로 정의된다. Map<K, V>는 데이터를 엔트리 단위로 입력받는 것이다.

 

2) Key : 중복 저장 불가, Value : 중복 가능

얼핏 보면 List<E>와 비슷해 보이지만 List<E>의 인덱스는 고정적인 반면, Map<K, V>의 Key 값으로는 무엇이든 올 수 있다. 데이터를 구분하는 기준이 Key값이므로 Key값은 중복될 수 없다. Value 값은 Key 값으로 구분해 가져올 수 있으므로 중복이 허용된다.

 

Map<K, V> 인터페이스의 주요 메서드

크게 데이터의 추가, 변경, 정보 추출 및 삭제로 나눌 수 있다.

구분 리턴 타입 메서드명 기능
데이터 추가 V put(K value, V value) 입력매개변수(key, value)를 Map 객체에 추가
void putAll(Map<? extends K, ? extends V> m) 입력매개변수의 Map 객체를 통째로 추가
데이터 변경 V replace(K key, V value) Key에 해당하는 값을 Value 값으로 변경(old 값 리턴)
단, 해당 Key가 없으면 null 리턴
boolean replace(K key, V oldValue, V newValue) (key, oldValue)의 쌍을 갖는 엔트리에서 oldValue를 newVluae로 변경
단, 해당 엔트리가 없으면 false를 리턴
데이터 정보 추출 V get(Object key) 매개변수의 Key 값에 해당하는 oldValue를 리턴
boolean containsKey(Object key) 매개변수의 Key 값이 포함돼 있는지 여부
boolean containsValue(Object value) 매개변수의 Value 값이 포함돼 있는지 여부
Set<K> keySet() Map 데이터들 중 Key들만 뽑아 Set 객체로 리턴
Set<Entry<K, V>> entrySet() Map의 각 엔트리들을 Set 객체로 담아 리턴
데이터 삭제 V remove(Object key) 입력매개변수의 Key를 갖는 엔트리 삭제
단, 해당 Key가 없으면 아무런 동작을 하지 않음
boolean remove(Object key, Object value) 입력매개변수의 (key, value)를 갖는 엔트리 삭제
단, 해당 엔트리가 없으면 아무런 동작을 하지 않음
void clear() Map 객체 내의 모든 데이터 삭제

Map<K, V> 인터페이스의 대표적인 구현 클래스로는 HashMap<K, V>, LinkedHashMap<K, V>, Hashtable<K, V>, TreeMap<K, V>를 들 수 있다. 이제 각 구현 클래스들의 특징을 알아보자.

 

HashMap<K, V>

HashMap<K, V>는 Map<K, V>의 대표적인 구현 클래스로, Key 값의 중복을 허용하지 않는다. 즉, 두 Key 객체의 hashCode() 값이 같고, equals() 메서드가 true를 리턴하면 같은 객체로 인식한다.

HashMap<K, V> 객체는 개념상으로 Key 값을 HashSet<E>로 구현한 Map<K, V> 객체다. 따라서 입출력 순서는 동일하지 않을 수 있고, 초기 저장 용량의 기본값은 16이다. (원소의 개수가 16개가 넘어가면 자동으로 늘어난다)

 

HashMap<K, V>의 주요 메서드

// 데이터 추가
Map<Integer, String> hMap1 = new HashMap<Integer, String>();

// 1. put(K key, V value)
hMap1.put(2, "나다라");
hMap1.put(1, "가나다");
hMap1.put(3, "다라마");
System.out.println(hMap1.toString());	// {1=가나다, 2=나다라, 3=다라마} 입력순서 불일치

// 2. putAll(<Map<? extends K, ? extends V> m)
Map<Integer, String> hMap2 = new HashMap<Integer, String>();
hMap2.putAll(hMap1);
System.out.println(hMap2.toString());	// {1=가나다, 2=나다라, 3=다라마}

먼저 HashMap<K, V> 객체를 생성 후 3쌍의 데이터를 put() 메서드를 통해 저장했다. 

두번째로는 putAll() 메서드를 사용해 앞에 생성한 hMap1을 통째로 추가했다.

 

// 데이터 변경

// 3. replace(K key, V value)
hMap2.replace(1, "가가가");
hMap2.replace(4, "라라라");	// 동작 x
System.out.println(hMap2.toString());	// {1="가가가",2="나다라",3="다라마"}

// 4. replace(K key, V oldValue, V newValue)
hMap2.replace(1, "가가가", "나나나");
hMap2.replace(2, "다다다", "라라라");	// 동작 x
System.out.println(hMap2.toString());	// {1="나나나",2="나다라",3="다라마"}

먼저 hMap2에는 4번째의 Key 값이 4인 엔트리 (한 쌍)가 존재하지 않으므로 동작하지 않는다.

두번째 동작안하는 예시에서도 Key 값이 2인 엔트리가 없으므로 동작하지 않는다.

 

// 5. V get(Object key)
// - 특정 Key 값을 갖는 엔트리의 Value 값을 리턴
System.out.println(hMap2.get(1));	// 나나나
System.out.println(hMap2.get(2));	// 나다라
System.out.println(hMap2.get(3));	// 다라마

// 6. containsKey(Object key)
// - 특정 Key가 포함되어있는지 리턴
System.out.println(hMap2.containsKey(1));	// true
System.out.println(hMap2.containsKey(5));	// false

// 7. containsValue(Object value)
// - 특정 Value가 포함되어있는지 리턴
System.out.println(hMap2.containsValue("나나나"));	// true
System.out.println(hMap2.containsValue("다다다"));	// false

// 8. Set<K> keySet()
// - 현재 저장되어 있는 엔트리 중 Key값 만을 Set<E>으로 리턴
Set<Integer> keySet = hMap2.keySet();
System.out.println(keySet.toString());	// [1, 2, 3]

// 9. Set<Map.Entry<K, V>> entrySet()
// - 현재 저장되어 있는 모든 엔트리를 Set<Map.Entry<K, V>>로 리턴
Set<Map.Entry<Integer, String>> entrySet = hMap2.entrySet();
System.out.println(entrySet);	// [1=나나나, 2=나다라, 3=다라마]

// 10.size()
// - 데이터의 개수를 리턴
System.out.println(hMap2.size());	// 3

주석으로 각 메서드의 설명을 달아놓았다. 주의할점은 get() 메서드 사용시 타입이 Integer이므로 혼동할 수 있는데 이는 인덱스 번호가 아닌 Key 값이라는 것을 명심하자.

 

// 데이터 삭제

// 11. remove(Object key)
// - key 값에 해당하는 entry 삭제
hMap2.remove(1);
hMap2.remove(4);	// 해당 키값이 없으므로 동작 x
System.out.println(hMap2.toString());	// {2=나다라, 3=다라마}

// 12. remove(Object key, Object value)
// - key 값과 value가 일치하는 entry 삭제
hMap2.remove(2, "나다라");
hMap2.remove(3, "다다다");	// 해당 키값에 해당하는 엔트리가 없으므로 동작 x
System.out.println(hMap2.toString());	// {3=다라마}

// 13. clear()
// - 모든 데이터를 삭제
hMap2.clear();
System.out.println(hMap2.toString());	// {}

 

Hashtable<K, V>

앞의 HashMap<K, V> 구현 클래스가 단일 쓰레드에 적합한 반면, Hashtable<K, V>은 멀티쓰레드에 안정성을 가진다. 즉, 하나의 Map<K, V> 객체를 2개의 쓰레드가 동시에 접근할 때도 모든 내부의 주요 메서드가 동기화 메서드로 구현되어 있으므로 멀티쓰레드에서도 안전하게 동작한다.

 

Hashtable<K, V>의 주요 메서드

Hashtable<K, V>는 HashMap<K, V>와 비교해 멀티 쓰레드에도 안전하다는 특징 말고는 완벽히 HashMap<K, V>와 동일한 특징을 가진다. 주요 메서드 및 메서드의 활용 방법 또한 동일하게 사용되므로 추가설명은 생략한다.

 

 

LinkedHashMap<K, V>

LinkedHashMap<K, V>는 HashMap<K, V>의 기본적인 특성에 입력 데이터의 순서 정보를 추가로 갖고 있는 컬렉션이다. 따라서 저장 데이터를 출력하면 항상 입력된 순서대로 출력된다. HashMap<K, V>에서는 Key를 HashSet<E>로 관리하는 반면, LinkedHashMap<K, V>는 Key를 LinkedHashSet<E>로 관리한다. Key의 순서 정보를 갖고 있으므로 Key 값을 기반으로 출력되는 LinkedHashMap<K, V> 또한 순서 정보를 갖게 되는 것이다.

 

LinkedHashMap<K, V>의 주요 메서드

LinkedHashMap<K, V>에서 사용되는 메서드 또한 출력이 입력의 순으로 나오는 것을 제외하곤 HashMap<K, V>과 완벽히 동일하므로 추가 설명을 생략한다.

 

TreeMap<K, V>

TreeMap<K, V>은 Map<K, V>의 기본 기능에 정렬 및 검색 기능이 추가된 컬렉션으로 입력 순서와 관계 없이 데이터를 Key 값의 크기 순으로 저장한다. 따라서 반드시 Key 객체는 크기 비교의 기준을 갖고 있어야 한다.

크기 비교의 방법과 객체에 크기 비교 기준을 포함하는 모든 과정은 이전 게시물인 TreeSet<E>에 있다. SortedMap<K, V>와 NavigableMap<K, V> 인터페이스의 자식 클래스 이므로 TreeMap<K, V> 생성자로 객체를 생성해도 Map<K, V> 타입으로 선언하면 추가된 정렬 및 검색 기능을 사용할 수 없다. 즉 TreeMap<K, V>으로 생성해야 추가된 정렬 및 검색 메서드를 호출 해야 한다.

Map<Integer, String> treeMap = new TreeMap<Integer, String>();
// Map<K, V> 메서드만 사용 가능

TreeMap<Integer, String> treeMap = new TreeMap<Integer, String>();
// Map<K, V> 메서드와 추가된 정렬, 검색 메서드 사용 가능

 

TreeMap<K, V>의 주요 메서드

Map<K, V>의 주요 메서드 종류와 활용법은 이전에 설명했으므로 TreeMap<K, V>에서 추가로 사용할 수 있는 정렬과 검색 관련 메서드에 대해서만 설명한다. 크게 데이터 검색, 데이터 꺼내기, 데이터 부분 집합 생성, 정렬 기능을 가진 메서드다. TreeSet<E>의 메서드와 매우 비슷하며 단지 데이터가 Key, Value 쌍의 엔트리 형태로 저장되기 때문에 Key와 엔트리에 데이터를 검색하거나 추출하는 메서드가 포함된다는 점에 차이가 있다.

구분 리턴 타입 메서드명 기능
데이터 검색 K firstKey() Map 원소 중 가장 작은 Key 값 리턴
Map.Entry<K, V> firstEntry() Map 원소 중 가장 작은 Key 값을 갖는 엔트리 리턴
K lastKey() Map 원소 중 가장 큰 Key 값 리턴
Map.Entry<K, V> lastEntry() Map 원소 중 가장 큰 Key 값을 갖는 엔트리 리턴
K lowerKey(K key) 매개변수로 입력된 Key 값보다 작은 Key 값 중 가장 큰 Key 값 리턴
Map.Entry<K, V> lowerEntry(K Key) 매개변수로 입력된 Key 값보다 작은 Key 값 중 가장 큰 Key 값을 갖는 엔트리 리턴
K higherKey(K key) 매개변수로 입력된 Key 값보다 큰 Key 값 중 가장 작은 Key 값 리턴
Map.Entry<K, V> higherEntry(K key) 매개변수로 입력된 Key 값보다 큰 Key 값 중 가장 작은 Key 값을 갖는 엔트리 리턴
데이터 추출 Map.Entry<K, V> pollFirstEntry() Map 원소 중 가장 작은 Key 값을 갖는 엔트리를 꺼내 리턴
Map.Entry<K, V> pollLastEntry() Map 원소 중 가장 큰 Key 값을 갖는 엔트리를 꺼내 리턴
데이터 부분 집합 생성 SortedMap<K, V> headMap(K toKey) toKey 미만의 Key 값을 갖는 모든 엔트리를 포함한 Map 객체 리턴(toKey 미포함)
NavigableMap<K, V> headMap(K toKey, boolean inclusive) toKey 미만/이하의 Key 값을 갖는 모든 엔트리를 포함한 Map 객체 리턴(inclusive=true라면 toKey 포함, false라면 미포함)
SortedMap<K, V> tailMap(K fromKey) fromKey 이상인 Key 값을 갖는 모든 엔트리를 포함한 Map 객체 리턴(fromKey 포함)
NavigableMap<K, V> tailMap(K fromKey, boolean inclusive) fromKey 초과/이상인 Key 값을 갖는 모든 엔트리를 포함한 Map 객체 리턴(inclusive=true라면 fromKey 포함, false라면 미포함)
SortedMap<K, V> subSet(K fromKey, K toKey) fromKey이 이상 toKey 미만의 Key 값을 갖는 모든 엔트리를 포함한 Map 객체 리턴(fromKey 포함, toKey 미포함)
NavigableMap<K, V> subSet(K fromKey, boolean frominclusive, K toKey, boolean toinclusive) fromKey 초과/이상 toKey 미만/이하인 Key 값을 갖는 모든 엔트리를 포함한 Map 객체 리턴(frominclusive=true/false fromKey 포함/미포함, toinclusive=true/false toKey 포함/미포함)
데이터 정렬 NavigableSet<K> descendingKeySet()  Map에 포함된 모든 Key 값의 정렬을 반대로 변환한 Set 객체 리턴
NavigableMap<K, V> descendingMap() Map에 포함된 모든 Key 값의 정렬을 반대로 변환한 Map 객체 리턴
import java.util.*;

public class TreeMapMethod1 {
	public static void main(String[] args) {
    	TreeMap<Integer, String> treeMap = new TreeMap<Integer, String>();
        for (int i = 20; i > 0; i -= 2) {
        	treeMap.put(i, i+" 번째 데이터");
        }
        
        System.out.println(treeMap.toString());
        // {2=2번째 데이터, 4=4번째 데이터, 6=6번째 데이터, 8=8번째 데이터, 10=10번째 데이터, 
        12=12번째 데이터, 14=14번째 데이터, 16=16번째 데이터, 18=18번째 데이터, 20=20번째 데이터}
        
        // 1. firstKey()
        // 첫번째 Key 값 반환
        System.out.println(treeMap.firstKey());	// 2
        
        // 2.firstEntry()
        // 첫번째 엔트리 반환
        System.out.println(treeMap.firstEntry());	// 2=2번째 데이터
        
        // 3. lastKey()
        // 마지막 key 값 반환
        System.out.println(treeMap.lastKey());	// 20
        
        // 4. lastEntry()
        // 마지막 엔트리 반환
        System.out.println(treeMap.lastEntry());	// 20=20번째 데이터
        
        // 5. lowerKey(K key)
        // 매개변수의 키 값에서 가장 가깝고 작은 키 값 반환
        System.out.println(treeMap.lowerKey(11));	// 10
        System.out.println(treeMap.lowerKey(10));	// 8
        
        // 6. higherKey(K Key)
        // 매개변수의 키 값에서 가장 가깝고 큰 키 값 반환
        System.out.println(treeMap.higherKey(11));	// 12
        System.out.println(treeMap.higherKey(10));	// 12
        
        // 7. pollFirstEntry()
        // 첫 번째 엔트리를 꺼내 반환
        System.out.println(treeMap.pollFirstEntry());	// 2=2번째 데이터
        
        // 8. pollLastEntry()
        // 마지막 엔트리를 꺼내 반환
        System.out.println(treeMap.pollLastEntry());	// 20=20번째 데이터
        
        // 데이터를 꺼낸 이후의 변화
        System.out.println(treeMap.toString());
        // {4=4번째 데이터, 6=6번째 데이터, 8=8번째 데이터, 10=10번째 데이터, 
        12=12번째 데이터, 14=14번째 데이터, 16=16번째 데이터, 18=18번째 데이터}
        
        // 9. SortedMap<K, V> headMap(K toKey)
        // 매개변수의 키값보다 작은 키값을 가진 엔트리를 리턴
        SortedMap<Integer, String> sortedMap = treeMap.headMap(8);
        System.out.println(sortedMap);	// {4=4번째 데이터, 6=6번째 데이터}
        
        // 10. NavigableMap<K, V> headMap(K toKey, boolean inclusive)
        // inclusive가 true라면 toKey 포함, false라면 toKey 미포함하여 작은 키값을 가진 엔트리 리턴
        NavigableMap<Integer, String> navigableMap = treeMap.headMap(8, true);
        // 두번째 매개변수의 값이 true이므로 8의 키값을 가진 엔트리 포함
        System.out.println(navigableMap);	// {4=4번째 데이터, 6=6번째 데이터, 8=8번째 데이터}
        
        // 11. SortedMap<K, V> tailMap(K toKey)
        // 매개변수의 키값을 포함해 큰 키값을 가진 엔트리를 리턴 
        sortedMap = treeMap.tailMap(14);
        System.out.println(sortedMap);
        // {14=14번째 데이터, 16=16번째 데이터, 18=18번째 데이터}
        
        // 12. NavigableMap<K, V> headMap(K toKey, boolean inclusive)
        // inclusive가 true라면 toKey 포함, flase라면 toKey를 미포함하여 큰 값을 가진 엔트리 리턴
        navigableMap = treeMap.tailMap(14, false);
        System.out.println(navigableMap);
        // {16=16번째 데이터, 18=18번째 데이터}
        
        // 13. SortedMap<K, V> subMap(K fromKey, K toKey)
        // fromKey부터 toKey까지의 엔트리를 리턴(fromKey 포함, toKey 미포함)
        sortedMap = treeMap.subMap(6, 10);
        System.out.println(sortedMap);
        // {6=6번째 데이터, 8=8번째 데이터}
        
        // 14. NavigableMap<K, V> subMap(K toKey, boolean inclusive)
        // inclusive가 true라면 포함, false라면 미포함
        navigableMap = treeMap.subMap(6, false, 10, true);
        System.out.println(navigableMap);
        // {8=8번째 데이터, 10=10번째 데이터}
        
        // 15. NavigableSet<K> descendingKeySet()
        // Map에 포함된 Key값의 정렬을 반대로 변환한 Set 리턴
        NavigableSet<Integer> navigableSet = treeMap.descendingKeySet();
        System.out.println(navigableSet.toString());
        // [18, 16, 14, 12, 10, 8, 6, 4]
        System.out.println(navigableSet.descendingSet());
        // [4, 6, 8, 10, 12, 14, 16, 18]
        
        // 16. NavaigableMap<K, V> descendingMap();
        // Map에 포함된 Key값의 정렬을 반대로 변환한 Map 리턴
        navigableMap = treeMap.descendingMap();
        System.out.println(navigableMap.toString());
        // {18=18번째 데이터, 16=16번째 데이터, 14=14번째 데이터, 12=12번째 데이터,
        10=10번째 데이터, 8=8번째 데이터, 6=6번째 데이터, 4=4번째 데이터}
        System.out.println(navigableMap.descendingSet());
        // {4=4번째 데이터, 6=6번째 데이터, 8=8번째 데이터, 10=10번째 데이터, 
        12=12번째 데이터, 14=14번째 데이터, 16=16번째 데이터, 18=18번째 데이터}
    }
}

Key 값의 크기에 따라 오름차순으로 정렬하는 과정에서 적용되는 크기 비교 매커니즘은 TreeSet<E>과 모든 내용이 동일하다. 차이점으로는 TreeMap<K, V>일 때 Key 값의 크기를 비교해 정렬을 수행한다는 점이다.

profile

숑숑이의 개발일기

@숑숑-

풀스택 개발자 준비중입니다