도도한 개발자

[Java] #24. Collection(컬렉션) - Map 본문

Backend/Java

[Java] #24. Collection(컬렉션) - Map

Kiara Kim 2023. 1. 20. 19:30

Map 이란?

 

자바에서의 Map은 키(Key)와 값(Value)으로 이루어져 있다. Map에서 다른 데이터와 구분하기 위한 값의 이름을 키라고 하는 이유를 생객해보면 이해하기 쉬울 것이다.

 

Map의 특징은 다음과 같다.

 

  • 모든 데이터는 키와 값이 존재한다.
  • 키가 없이 값만 저장될 수는 없다.
  • 값 없이 키만 저장할 수도 없다.
  • 키는 해당 Map에서 고유해야만 한다.
  • 값은 Map에서 중복되어도 전혀 상관 없다. 

 

Map은 java.util 패키지의 Map이라는 이름의 인터페이스로 선언되어 있고, 구현해 놓은 클래스들도 많이 있다. 이 Map 인터페이스에 선언되어 있는 메소드들의 주요 기능을 보자.

 

리턴 타입 메소드 이름 및 매개 변수 설명
V put(K key, V value) 첫 번째 매개 변수인 키를 갖는, 두 번째 매개 변수인 값을 갖는 데이터를 저장한다.
void putAll(Map<? extends K, ? extends V> m) 매개 변수로 넘어온 Map의 모든 데이터를 저장한다.
V get(Object key) 매개 변수로 넘어온 키에 해당하는 값을 넘겨준다.
V remove(Object key) 매개 변수로 넘어온 키에 해당하는 값을 넘겨주며, 해당 키와 값은 Map에서 삭제한다.
Set<K> keySet() 키의 목록을 Set 타입으로 리턴한다.
Collection<V> values() 값의 목록을 Collection 타입으로 리턴한다.
Set<Map.Entry<K,V>> entrySet() Map 안에 Entry 라는 타입의 Set을 리턴한다.
int size() Map의 크기를 리턴한다.
void clear() Map의 내용을 지운다.

 

Map을 사용할 때 다음 세가지는 꼭 기억하자.

  • Map에 데이터를 넣는 put() 메소드
  • 데이터를 확인하는 get() 메소드
  • 데이터를 삭제하는 remove() 메소드

 

Map을 구현한 주요 클래스

 

Map 인터페이스를 구현한 클래스들을 매우 많고 다양하다. 그 중 HashMap, TreeMap, LinkedHashMap  등이 가장 유명하고, 개발자들이 애용하는 클래스다. 그리고 HashTable이라는 클래스도 있다. 

 

HashTable 클래스는 Map 인터페이스를 구현하기는 했지만 일반적인 Map 인터페이스를 구현한 클래스들과든 다른 점이 있다.

 

  • Map은 컬렉션 뷰(Collection view)를 사용하지만, HashTable은 Enumeration 객체를 통해 데이터를 처리한다.
  • Map은 키, 값, 키-값 쌍으로 데이터를 순환하여 처리할 수 있지만, HashTable은 이 중 키-값 쌍 데이터를 순환 처리할 수 없다.
  • Map은 이터레이션을 처리하는 도중 데이터를 삭제하는 안전한 방법을 제공하지만, HashTable은 그러한 기능을 제공하지 않는다.

 

HashMap 클래스

 

HashMap 클래스는 다음과 값은 상속 관계를 가진다.

 

java.lang.Object
   java.util.AbstractMap<K, V>
      java.util.HashMap<K, V>

 

대부분의 주요 메소드는 AbstractMap 클래스가 구현해 놓았는데 우선 어떤 인터페이스를 구현했는지부터 알아보자.

 

인터페이스 용도
Serializable 원격으로 객체를 전송하거나, 파일에 저장할 수 있음을 지정
Cloneable Object 클래스의 clone() 메소드가 제대로 수행될 수 있음을 지정
즉, 복제가 가능한 객체임을 의미한다.
Map<E> 맵의 기본 메소드 지정

 

앞서 학습한 List, Set, Queue의 인터페이스들과 비교하면 매우 간단하게 되어 있다. 

 

다음은 HashMap 클래스의 객체를 생성하기 위한 생성자를 알아보자.

 

생성자 설명
HashMap() 16개의 저장 공간을 갖는 HashMap 객체를 생성한다.
HashMap(int initialCapacity) 매개 변수만큼의 저장 공간을 갖는 HashMap 객체를 생성한다.
HashMap(int initialCapacity, float loadFactor) 첫 매개 변수의 저장 공간을 갖고, 두 번째 매개 변수의 로드팩터를 갖는 HashMap 객체를 생성한다.
HashMap(Map<? extends K, ? extends V> m) 매개 변수로 넘어온 Map을 구현한 객체에 있는 데이터를 갖는 HashMap 객체를 생성한다.

 

대부분 HashMap 객체를 생성할 때엔 매개 변수가 없는 생성자를 사용한다. 그러나 HashMap에 담을 데이터 개수가 많은 경우 초기 크기를 지정해주는 것이 권장된다.

 

추가로 HashMap에 저장하는 키가 되는 객체를 직접 만들었을 때에는 유의해야 하는 것이 있다.

 

HashMap의 키는 기본 자료형과 참조 자료형 모두 될 수 있기 때문에 보통 int나 long 같은 숫자나 String 클래스를 키로 많이 사용한다. 그러나 직접 어떤 클래스를 만들어 그 클래스를 키로 사용할 때에는 Object 클래스의 hashCode() 메소드와 equals() 메소드를 잘 구현해 놓아야 한다.

이와 관련하여 더 자세한 내용을 알고자 한다면 구글에 "java map buckets"으로 검색하면 확인할 수 있다.

 

HashMap 객체에 값 넣어보기

 

Collection에서 데이터를 추가하는 것은 add()메소드다. 그러나 Map에서는 추가한다고 표현하지 않고 데이터를 넣는다고 표현한다. 따라서 put()이라는 메소드를 사용한다.

 

다음 예제를 통해 HashMap을 사용해보자.

 

package collection;

import java.util.HashMap;

public class MapSample {
    public static void main(String[] args) {
        MapSample sample = new MapSample();
        sample.checkHashMap();
    }

    public void checkHashMap() {
        HashMap<String, String> map = new HashMap<>();
        map.put("A", "a");
    }

여기서 선언한 map이라는 HashMap 객체의 키와 값의 타입은 String이다. 그러므로 map에는 다른 타입의 키와 다른 타입의 값을 넣을 수 없다. 

 

다음 줄에 "A"라는 값이 첫 번째 매개 변수로 넘어가고, "a"라는 값이 두 번째 매개 변수로 넘어갔다. 여기서 키와 값은 각각 무엇일까?

 

Map은 항상 키-값 순서대로 생각하면 편하다. "A"가 키, "a"가 값이 되는 것이다. map에 있는 값을 꺼낼 때엔 다음과 같이 get() 메소드를 사용하면 된다.

 

public void checkHashMap() {
    HashMap<String, String> map = new HashMap<>();
    map.put("A", "a");

    System.out.println(map.get("A"));
    System.out.println(map.get("B"));
}

A라는 키를 갖는 값과 B라는 키를 갖는 값을 출력하도록 했다. 결과는 어떻게 될까?

 

당연히 첫 번째 출력문의 값은 a가 나오지만, 두 번째 출력문의 값은 정확히 모르겠다.

 

Collection에서는 해당 위치에 값이 없을 때엔 ArrayIndexOutOfBoundsException라는 예외가 발생해 버린다. 해당 위치가 존재하지 않는 것을 개발자에게 알려줘야 하기 때문이다. 그런데, Map에서는 존재하지 않는 키로 get()을 할 경우 null을 리턴한다. 따라서 위 예제의 결과는 다음과 같다.

 

a
null

그렇다면 HashMap 객체에 put() 메소드를 사용하여 이미 존재하는 키로 값을 넣을 때엔 어떻게 될까?

 

기존의 값을 새로운 값으로 대치한다. 

 

HashMap 객체의 값을 확인하는 다른 방법

 

HashMap에 어떤 키가 있는 확인하기 위해 keySet()이라는 메소드를 사용하는 방법도 있다. get()이라는 데이터를 조회하는 메소드를 사용하려면, 어떤 키들이 있는지 확인해야 하므로, Map을 사용할 때 이 메소드는 꼭 알고 있어야 한다. 메소드 이름에서 알 수 있듯 keySet() 메소드의 리턴 타입은 Set이다. 그러므로 Set의 제네릭 타입은 키의 제네릭 타입과 동일하게 지정해주면 된다.

 

public void checkKeySet() {
    HashMap<String, String> map = new HashMap<>();
    map.put("A", "a");
    map.put("C", "c");
    map.put("D", "d");
    Set<String> keySet = map.keySet();
    for (String tempKey : keySet) {
        System.out.println(tempKey + " = " + map.get(tempKey));
    }
}

이렇게 메소드를 사용하여 컴파일한 뒤 실행한 결과는 다음과 같다.

 

A = a
C = c
D = d

결과의 순서는 반드시 이렇게 나오지 않을 수 있다.

 

자바의 자료 구조 중에서 저장 순서가 중요한 것은 List와 Queue뿐이다. Set과 Map은 데이터 추가 순서는 중요하지 않다.

Set은 데이터가 중복되지 않는 것이 중요하고, Map은 키가 중복되지 않는 것이 중요하다. 

 

만약 HashMap 객체에 담겨 있는 값만 필요할 경우 이렇게 keySet() 메소드를 사용하며 키 목록을 얻어내고, 하나 하나 받아올 필요는 없다. 왜냐하면 values()라는 메소드가 있기 때문이다.

 

public void checkValues() {
    HashMap<String, String> map = new HashMap<>();
    map.put("A", "a");
    map.put("C", "c");
    map.put("D", "d");

    Collection<String> values = map.values();
    for (String tempValues : values) {
        System.out.println(tempValues);
    }
}

values()라는 메소드를 사용하면 HashMap에 담겨있는 값의 목록을 Collection 타입의 목록으로 리턴해준다. 이 메소드의 출력 결과는 다음과 같다.

 

a
c
d

Map에 저장되어 있는 모든 값을 출력할 때엔 values()메소드를 사용하는 것이 keySet() 메소드로 모든 키 값을 가져 온 후 처리하는 것보다 간편하다.

 

그런데 이렇게 데이터를 꺼내는 방법 외에 entrySet()이라는 메소드를 사용할 수도 있다. 이 메소드를 사용하면 Map에 선언된 Entry라는 타입의 객체를 리턴한다. 이 Entry에는 단 하나의 키와 값만이 저장된다. 따라서, getKey()와 getValue()라는 메소드를 사용하면 키와 값을 간단하게 가져올 수 있다.

 

public void checkHashMapEntry() {
    HashMap<String, String> map = new HashMap<>();
    map.put("A", "a");
    map.put("B", "b");
    map.put("C", "c");
    map.put("D", "d");

    Set<Map.Entry<String, String>> entries = map.entrySet();
    for (Map.Entry<String, String> tempEntry : entries) {
        System.out.println(tempEntry.getKey() + " = " + tempEntry.getValue());
    }
}

이 메소드를 수행한 결과는 다음과 같다.

 

A = a
B = b
C = c
D = d

이처럼 Map에 있는 데이터를 꺼낼 때에는 목적에 맞는 메소드를 적절히 선택하는 것이 중요하다.

 

Map에 데이터 존재 유무 확인

 

Map에 어떤 키나 값이 존재하는지를 확인하는 containsKey()와 containsValue() 메소드에 대해 알아보자.

 

public void checkContains(){
    HashMap<String, String> map = new HashMap<>();
    map.put("A", "a");
    map.put("B", "b");
    map.put("C", "c");
    map.put("D", "d");

    System.out.println(map.containsKey("A"));
    System.out.println(map.containsKey("Z"));
    System.out.println(map.containsValue("a"));
    System.out.println(map.containsValue("z"));
}

containsKey() 메소드는 매개 변수로 넘긴 키가 존재하는지를, 

 

containsValue() 메소드는 매개 변수로 넘긴 값이 존재하는지를 리턴해주며 모두 boolean 타입의 결과를 제공한다. 

 

위 메소드의 결과는 다음과 같다.

 

true
false
true
false

 

정렬된 Key 목록을 위한 TreeMap

 

HashMap 객체의 키를 정렬하는 방법은 여러가지가 있다. 가장 간단한 방법 중 하나는 Arrays 클래스를 사용하는 것이지만 사용시 불필요한 객체가 생기는 단점이 있다. 

 

이러한 점을 보안하기 위한 TreeMap클래스가 있다.

 

이 클래스는 저장하면서 키를 정렬한다. 정렬되는 순서는 "숫자 > 알파벳 대문자 > 알파벳 소문자 > 한글" 순이다. 이 순서는 String과 같은 문자열이 저장되는 순서를 말하는 것이며, 객체나 숫자가 저장될 때는 그 순서가 달라진다.

 

예제를 통해 알아보자.

 

package collection;

import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class TreeMapSample {
    public static void main(String[] args) {
        TreeMapSample sample = new TreeMapSample();
        sample.checkTreeMap();
    }

    public void checkTreeMap() {
        TreeMap<String, String> map = new TreeMap<>();
        map.put("A", "a");
        map.put("가", "e");
        map.put("1", "f");
        map.put("a", "g");

        Set<Map.Entry<String, String>> entries = map.entrySet();
        for (Map.Entry<String, String> tempEntry : entries) {
            System.out.println(tempEntry.getKey() + " = " + tempEntry.getValue());
        }
    }
}

이 메소드의 수행 결과는 어떻게 될까?

 

1 = f
A = a
a = g
가 = e

즉, TreeMap은 키를 정렬하여 저장하고, 키의 목록을 가져와서 출력해 보면 정렬된 순서대로 제공되는 것을 볼 수 있다. 

 

TreeMap이 키를 정렬하는 것은 SortedMap이라는 인터페이스를 구현했기 때문이다. 

 

Map을 구현한 Properties 클래스

 

Properties 클래스는 HashTable을 확장(extends)하였다. 따라서 Map 인터페이스에서 제공하는 모든 메소드를 사용할 수 있고 자바에서는 시스템의 속성을 이 클래스를 사용해 제공한다. 

 

다음 예제를 통해 시스테믜 속성값들을 확인하는 방법을 살펴보자.

 

package collection;

import java.util.Properties;
import java.util.Set;

public class PropertiesSample {
    public static void main(String[] args) {
        PropertiesSample sample = new PropertiesSample();
        sample.checkProperties();
    }

    public void checkProperties() {
        Properties prop = System.getProperties();
        Set<Object> keySet = prop.keySet();
        for (Object tempObject : keySet) {
            System.out.println(tempObject + " = " + prop.get(tempObject));
        }
    }
}

실제로 결과를 출력해보면, 매우 많은 데이터를 출력하는 것을 볼 수 있다. HashTable을 확장한 클래스이기 때문에 키와 값 형태로 데이터가 저장되어 있다. 

 

시스템에서 제공하는 여러 속성 중 앞으로 개발하면서 많이 사용할 값들을 적어본다.

 

속성 설명
user.language 사용자의 사용 언어
user.dir 현재 사용중인 기본 디렉토리
user.home 사용자 계정의 홈 디렉토리
java.io.tmpdir 자바에서 사용하는 임시 디렉토리
file.encoding 파일의 기본 인코딩
sun.io.unicode.encoding 유니코드 인코딩
path.separator 경로 구분자
file.separator 파일 구분자
line.separator 줄(line) 구분자

그런데 왜 Properties 클래스를 사용할까? 

HashTable이나 HashMap에 있는 속성을 사용하면 되지 않을까?

 

그 이유는 Properties 클래스에서 추가로 제공하는 메소드를 보면 알 수 있다.

 

리턴 타입 메소드 이름 및 매개 변수 설명
void load(InputStream inStream) 파일에서 속성을 읽는다.
void load(Reader reader)
void loadFromXML(InputStream in) XML로 되어 있는 속성을 읽는다.
void store(OutputStream out, String comments) 파일에 속성을 저장한다.
void store(Writer writer, String comments)
void storeToXML(OutputStream os, String comment) XML로 구성되는 송성 파일을 생성한다.
void storeToXML(OutputStream os, String comment, String encoding)

물론 이 외에도 여러가지 메소드가 존재한다. 

 

하지만 Properties 클래스를 이용하는 주된 이유는 여기에서 제공하는 메소드들 때문이다.

comments라고 되어 있는 매개 변수들을 저장되는 속성 파일에 주석으로 저장된다.

 

자바의 자료 구조를 정리해보자.

 

자바의 자료구조

Collection을 구현한 것은 List, Set, Queue이며, Map은 별도의 인터페이스로 되어 있다.

 

배열 목록을 처리하기 위한 List의 대표적인 클래스로는 ArrayList와 LinkedList가 있으며, 보통 ArrayList를 많이 사용한다.

 

List처럼 목록을 처리하긴 하지만 데이터의 중복이 없고, 순서가 필요없는 Set의 대표적인 클래스는 HashSet, TreeSet, LinkedHashSet이 있다.

 

데이터가 들어온 순서대로 처리하기 위해 사용하는 Queue의 대표적인 클래스는 LinkedList와 PriorityQueue 등이 있으며, LinkedList는 List에도 속하고 Queue에도 속하는 특이한 클래스다.

 

Map의 대표적인 클래스에는 HashMap, TreeMap, LinkedHashMap이 있으며, 사용 용도에 따라 다르겠지만 대부분 HashMap을 많이 사용한다.

 

Map의 "키" 목록은 keySet() 메소드를 사용하면 Set 타입의 데이터를 얻을 수 있고, "값" 목록은 values() 메소드를 통하여 Collection 타입의 데이터를 얻을 수 있다.

 

Collection의 데이터를 처리하기 위해선 for 루프를 사용할 수 있지만, iterator() 메소드를 통해 Iterator 객체를 얻어 각 데이터를 처리할 수도 있다.

 

정리

 

1. Map 형태의 자료구조는 무엇과 무엇으로 구성되어 있나요?

- 키, 값

 

2. Map에서 데이터를 저장하는 메소드는 무엇인가요?

- put()

 

3. Map에서 특정 키에 할당된 값을 가져오는 메소드는 무엇인가요?

- get() 

 

4. Map에서 특정 키와 관련된 키와 데이터를 지우는 메소드는 무엇인가요?

- remove()

 

5. Map에서 키의 목록을 가져오는 메소드는 무엇인가요?

- keySet()

 

6. Map에 저장되어 있는 데이터의 크기를 가져오는 메소드는 무엇인가요?

- size()

 

7. HashMap과 Hashtable 중에서 키나 값에 null을 저장할 수 있는 것은 무엇인가요?

- HashMap

 

8. HashMap과 Hashtable 중에서 여러 쓰레드에서 동시에 접근해도 문제가 없는 것은 무엇인가요?

- Hashtable

 

9. HashMap에서 특정 키가 존재하는지 확인하는 메소드는 무엇인가요?

- containsKey

 

10. 이 장에서 살펴본 클래스 중, 키가 저장되면서 정렬되는 Map에는 어떤 것이 있나요?

- TreeMap

 

11. Properties 클래스는 어떤 클래스를 확장한 것인가요?

- Hashtable

 

12. Properties 클래스의 객체에 있는 데이터를 파일로 저장할 때에는 어떤 메소드들을 사용하면 되나요?

- store