관리 메뉴

나만을 위한 블로그

[DS] Map이란? HashMap 사용법 본문

개인 공부/Data structure

[DS] Map이란? HashMap 사용법

참깨빵위에참깨빵 2021. 7. 1. 18:52
728x90
반응형

이번 포스팅에선 자바 자료구조 중 하나인 Map에 대해 정리하려고 한다. 또한 Map 인터페이스를 구현한 HashMap, TreeMap, LinkedHashMap에 대해서도 정리할 건데 먼저 HashMap만 정리하고 TreeMap, LinkedHashMap은 차차 정리하려고 한다.

 

오라클 docs에선 Map에 대해 아래와 같이 설명하고 있다.

https://docs.oracle.com/javase/tutorial/collections/interfaces/map.html

 

The Map Interface (The Java™ Tutorials > Collections > Interfaces)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

Map은 키를 값에 매핑하는 객체다. Map은 중복 키를 포함할 수 없다. 각 키는 최대 하나의 값에 매핑할 수 있다. 수학적 함수 추상화를 모델링한다. Map 인터페이스에는 기본 작업(put, get, remove, containsKey, containsValue, size, empty), 대량 작업(putAll, clear) 및 컬렉션 뷰(keySet, entrySet 및 값)에 대한 메서드가 포함된다. 자바 플랫폼에는 HashMap, TreeMap, LinkedHashMap의 3가지 범용 Map 구현이 포함되어 있다. 이들의 동작, 성능은 인터페이스 설정 섹션에 설명된대로 HashSet, TreeSet, LinkedHashSet과 유사하다. 

 

아래 링크에는 Map에 대한 설명이 일부 섞여있는 듯해 가져왔다.

https://www.mathworks.com/help/matlab/matlab_prog/overview-of-the-map-data-structure.html

 

Overview of Map Data Structure - MATLAB & Simulink

You clicked a link that corresponds to this MATLAB command: Run the command by entering it in the MATLAB Command Window. Web browsers do not support MATLAB commands.

www.mathworks.com

Map은 개별 요소에 대한 유연한 인덱싱 수단을 제공하는 빠른 key 조회 자료구조 유형이다...(중략)...Map의 인덱스는 거의 모든 스칼라 숫자값 또는 문자형 벡터일 수 있다. 맵 요소에 대한 인덱스를 key라고 한다. 이런 키는 연관된 데이터 값과 함께 Map 안에 저장된다. Map의 각 항목은 정확히 하나의 고유 키와 해당 값을 포함한다.

https://www.codebyamir.com/blog/how-to-use-a-map-in-java

 

How to use a Map in Java - Code by Amir | Amir Boroumand

Overview In this article, we'll cover how a map works in Java and explore what's new in Java 9.

www.codebyamir.com

Map은 빠른 조회를 위해 설계된 자료구조다. 데이터는 모든 키가 고유한 키밸류 쌍으로 저장된다. 각 키는 값에 매핑되므로 이름이 된다. 이런 쌍을 map entries(맵 항목)라고 한다. JDK에서 java.util.map은 삽입, 제거, 검색을 위한 메서드 시그니처가 포함된 인터페이스다. Map 인터페이스는 컬렉션 프레임워크의 여러 클래스에 의해 구현된다. 각 클래스는 서로 다른 기능과 쓰레드 세이프를 제공한다. 가장 일반적인 구현은 HashMap이다. Map은 컬렉션 인터페이스를 확장하거나 구현하지 않는 유일한 컬렉션이다. 단일 값 대신 쌍으로 작업해야 하므로 동일한 계약에 맞지 않는다.

위의 설명과 함께 첨부된 이미지다. 하나의 키가 하나의 값을 가리키고 있는 걸 볼 수 있다.

https://www.dummies.com/programming/java/what-is-a-java-map/

 

What Is a Java Map? - dummies

Arrays and specialized lists make it possible to perform an amazing array of tasks with Java. However, there are situations where a Java application needs something that’s more akin to a database, without actually having all the database baggage (such as

www.dummies.com

Map은 인터페이스다. Map은 정보를 저장하기 위한 키밸류 쌍을 제공하는 클래스를 설명한다. 키는 데이터에 고유한 이름을 부여한다. 두 항목이 동일한 키를 가질 수 없으므로 키를 검색하고 항상 고유한 값을 반환할 수 있다. 다른 인터페이스와 마찬가지로 사용하기 전에 구현(implementation)을 만들어야 한다. 

 

그 외 다른 글도 찾아봤지만 결국 Map이 무엇인지에 대한 건 아래로 정의할 수 있었다.

 

  • Map은 데이터를 키밸류 쌍으로 저장하는 자료구조다.
  • Map은 중복된 키를 가질 수 없으며 하나의 키에 하나의 값이 매핑된다.
  • 키를 활용해 키에 연결된 값을 검색할 수 있다.

 

Map을 구현한 클래스는 HashMap, TreeMap, LinkedHashMap인 듯하니 HashMap부터 사용법을 확인해보자.

import java.util.HashMap;
import java.util.Map;

public class HashMapPrac
{
    public static void main(String[] args)
    {
        HashMap<Integer, String> hashMap = new HashMap<>();
        hashMap.put(1, "자바");
        hashMap.put(2, "안드로이드");
        hashMap.put(3, "php");
        hashMap.put(4, "티스토리");

        System.out.println("HashMap을 반복하면서 데이터를 가져온다");
        for (Map.Entry entry : hashMap.entrySet()) {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }
    }
}

 

먼저 HashMap을 만들었다. 주의할 것은 HashMap 우측의 제네릭 안에 Integer와 String이 있다는 것이다.

이것의 의미는 첫 번째 Integer 값을 key, 두 번째 String 값을 value로 하겠다는 뜻이다.

그래서 그 밑으로 put()을 통해 Integer, String 순서로 키와 값을 각각 넣고 있는 걸 볼 수 있다.

put() 안에 들어가는 정수형 매개변수는 new Integer(1) 형식으로도 넣어줄 수 있지만 현재 이 방법은 deprecated되었다.

 

그리고 출력할 때는 for-each문을 사용해서 키와 값을 각각 빼내는데, Map.Entry와 hashMap.entrySet()이 특이하다.

자바에서 HashMap을 사용할 때는 키 또는 값을 얻으려면 getKey() 또는 getValue()를 사용하는데, Map.Entry 인터페이스에 이 두 메서드가 포함돼 있다. 그래서 getKey()와 getValue()를 사용하려면 Map.Entry의 인스턴스를 얻어야 하는데, for-each문의 우항에 entrySet()을 호출해야 Map.Entry의 인스턴스를 얻을 수 있다.

 

또 위에서 Map은 중복된 키를 가질 수 없다고 설명한 부분을 봤다. 진짜인지 확인해보자.

public class HashMapPrac
{
    public static void main(String[] args)
    {
        HashMap<Integer, String> hashMap = new HashMap<>();
        hashMap.put(1, "자바");
        hashMap.put(2, "안드로이드");
        hashMap.put(3, "php");
        hashMap.put(1, "티스토리");

        System.out.println("HashMap을 반복하면서 데이터를 가져온다");
        for (Map.Entry entry : hashMap.entrySet()) {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }
    }
}
HashMap을 반복하면서 데이터를 가져온다
키 : 1, 값 : 티스토리
키 : 2, 값 : 안드로이드
키 : 3, 값 : php
키 : 4, 값 : 티스토리

 

4번째 put() 안의 키를 4에서 1로 바꿨다. 이렇게 하면 인텔리제이 기준으로 두 개의 1 밑에 노란 줄이 생기며, 그것에 마우스를 갖다대면 아래 문장이 나온다.

 

Duplicate Map key

 

중복된 키라는 뜻 같다. 그럼에도 이걸 무시하고 실행하면 아래와 같은 결과가 나온다.

HashMap을 반복하면서 데이터를 가져온다
키 : 1, 값 : 티스토리
키 : 2, 값 : 안드로이드
키 : 3, 값 : php

 

4번째 키는 아예 나오지도 않는다. 중복된 키가 있을 경우 먼저 생성된 키와 값은 유지하되 늦게 만들어진 키와 값은 유지하지 않는 것 같다.

 


 

put()으로 키밸류를 넣는 것 말고도 putIfAbsent()라는 메서드가 있다.

이 메서드의 원형은 아래와 같다.

@Override
    public V putIfAbsent(K key, V value) {
        return putVal(hash(key), key, value, true, true);
    }

 

put()과 같이 키, 값 순서로 매개변수를 받는데, putVal()의 실행 결과를 반환해주는 메서드다.

putVal()의 원형은 아래와 같다.

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

 

좀 길어서 읽기 귀찮은데 인텔리제이로 확인하면 putVal() 위에 이런 주석이 쓰여 있다.

Implements Map.put and related methods.

Params:
hash – hash for key
key – the key
value – the value to put
onlyIfAbsent – if true, don't change existing value
evict – if false, the table is in creation mode.

Returns: previous value, or null if none

 

Map 인터페이스의 put()의 구현이라는 시시콜콜한 정보가 쓰여 있고, Params 부분의 onlyIfAbsent가 보인다.

직역하면 "없을 경우에만" 이라는 뜻인데, true가 올 경우 존재하는 값을 바꾸지 않는다고 써 있다.

그리고 evict는 '퇴거시키다' 라는 뜻인데, false인 경우 테이블이 생성 모드에 있다고 써 있다. 또한 리턴값은 "이전 값 또는 없는 경우 null" 이라고 쓰여 있다.

이걸 토대로 다시 아래 코드로 돌아와 생각해보자.

@Override
    public V putIfAbsent(K key, V value) {
        return putVal(hash(key), key, value, true, true);
    }

 

hash()를 먹인 key와 그냥 key를 매개변수로 받고 onlyIfAbsent가 true니까 이미 해당 key가 존재할 경우 바꾸지 않을 것이다. evict는 잘 모르겠다. 그냥 해보자.

public class HashMapPrac
{
    public static void main(String[] args)
    {
        HashMap<Integer, String> hashMap = new HashMap<>();
        hashMap.put(1, "자바");
        hashMap.put(2, "안드로이드");
        hashMap.put(3, "php");
        hashMap.put(4, "티스토리");

        System.out.println("HashMap을 반복하면서 데이터를 가져온다");
        for (Map.Entry entry : hashMap.entrySet()) {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }
        System.out.println("================================================");

        hashMap.putIfAbsent(4, "구글");
        System.out.println("putIfAbsent() 호출 이후");
        for (Map.Entry entry : hashMap.entrySet()) {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }

    }
}

 

이미 4번 키에 티스토리라는 문자열이 연결돼 있으니까 이를 바꾸지 않고, 덮어쓰지도 않는 걸 볼 수 있다.

그럼 putIfAbsent() 안의 숫자를 5로 바꿔서 다시 해보자.

 

정상적으로 5와 구글이 해시맵에 들어간 걸 볼 수 있다.

 

그리고 HashMap 안에 다른 HashMap을 넣는 것 또한 간단하다. putAll()을 쓰면 해결된다.

public class HashMapPrac
{
    public static void main(String[] args)
    {
        HashMap<Integer, String> hashMap = new HashMap<>();
        hashMap.put(1, "자바");
        hashMap.put(2, "안드로이드");
        hashMap.put(3, "php");
        hashMap.put(4, "티스토리");

        System.out.println("HashMap을 반복하면서 데이터를 가져온다");
        for (Map.Entry entry : hashMap.entrySet()) {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }
        System.out.println("================================================");

        hashMap.putIfAbsent(5, "구글");
        System.out.println("putIfAbsent() 호출 이후");
        for (Map.Entry entry : hashMap.entrySet()) {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }
        System.out.println("================================================");

        System.out.println("HashMap 안에 HashMap 넣기");
        HashMap<Integer, String> otherHashMap = new HashMap<>();
        otherHashMap.put(6, "네이버");
        otherHashMap.putAll(hashMap);
        for (Map.Entry entry : otherHashMap.entrySet()) {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }

    }
}

 

해시맵 안의 키를 삭제할 때는 remove(key)를 사용하면 된다.

public class HashMapPrac
{
    public static void main(String[] args)
    {
        HashMap<Integer, String> hashMap = new HashMap<>();
        hashMap.put(1, "자바");
        hashMap.put(2, "안드로이드");
        hashMap.put(3, "php");
        hashMap.put(4, "티스토리");

        System.out.println("HashMap을 반복하면서 데이터를 가져온다");
        for (Map.Entry entry : hashMap.entrySet()) {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }
        System.out.println("================================================");

        hashMap.putIfAbsent(5, "구글");
        System.out.println("putIfAbsent() 호출 이후");
        for (Map.Entry entry : hashMap.entrySet()) {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }
        System.out.println("================================================");

        System.out.println("HashMap 안에 HashMap 넣기");
        HashMap<Integer, String> otherHashMap = new HashMap<>();
        otherHashMap.put(6, "네이버");
        otherHashMap.putAll(hashMap);
        for (Map.Entry entry : otherHashMap.entrySet()) {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }

        System.out.println("================================================");
        System.out.println("HashMap - remove()");
        otherHashMap.remove(6);
        System.out.println(otherHashMap);

    }
}

 

굳이 필요없는 부분은 캡쳐하지 않았다. 6이라는 키를 삭제하고 출력해보니 6이 사라져있는 걸 볼 수 있다.

그리고 println() 안에 해시맵 객체를 넣으니 중괄호 안에 "키=값" 형식으로 안에 들어있는 데이터들이 출력된다.

 

이번엔 replace()와 replaceAll()를 살펴본다. replace는 '바꾸다'라는 뜻이 있는데, 해시맵에서도 이와 같은 기능을 제공하는지 아래 코드를 확인해보자.

public class HashMapPrac
{
    public static void main(String[] args)
    {
        HashMap<Integer, String> hashMap = new HashMap<>();
        hashMap.put(1, "자바");
        hashMap.put(2, "안드로이드");
        hashMap.put(3, "php");
        hashMap.put(4, "티스토리");

        System.out.println("HashMap을 반복하면서 데이터를 가져온다");
        for (Map.Entry entry : hashMap.entrySet()) {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }
        System.out.println("================================================");

        hashMap.putIfAbsent(5, "구글");
        System.out.println("putIfAbsent() 호출 이후");
        for (Map.Entry entry : hashMap.entrySet()) {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }
        System.out.println("================================================");

        System.out.println("HashMap 안에 HashMap 넣기");
        HashMap<Integer, String> otherHashMap = new HashMap<>();
        otherHashMap.put(6, "네이버");
        otherHashMap.putAll(hashMap);
        for (Map.Entry entry : otherHashMap.entrySet()) {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }

        System.out.println("================================================");
        System.out.println("HashMap - remove()");
        otherHashMap.remove(6);
        System.out.println(otherHashMap);

        System.out.println("================================================"); // <- 여기부터 확인
        System.out.println("HashMap - replace()");
        otherHashMap.replace(6, "네이버에서 다음으로 변경!");
        for (Map.Entry entry : otherHashMap.entrySet())
        {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }

        System.out.println("================================================");
        System.out.println("HashMap - replaceAll()");
        otherHashMap.replaceAll((k, v) -> "replaceAll()을 사용한 결과는 이렇게 됩니다");
        for (Map.Entry entry : otherHashMap.entrySet())
        {
            System.out.println("키 : " + entry.getKey() + ", 값 : " + entry.getValue());
        }
    }
}

이렇게 보니 replace()와 replaceAll()의 기능적 차이가 무엇인지 한번에 볼 수 있다.

replace() 안에 바꾸고 싶은 키와 바꾸고자 하는 내용을 넣으면 바로 변경되며, replaceAll()을 사용하면 모든 값들이 바뀐다.

반응형
Comments