Java의 HashMap

mangooΒ·2022λ…„ 12μ›” 15일
0

πŸ“Œ ν•΄μ‹œλž€ 무엇인지, ν•΄μ‹œ μΆ©λŒμ€ μ™œ μΌμ–΄λ‚˜κ³  μ–΄λ–»κ²Œ 방지할 수 μžˆλŠ”μ§€ μ•Œκ³  μžˆμ–΄μ•Ό ν•œλ‹€. 잘 λͺ¨λ₯Έλ‹€λ©΄ 이 글을 읽고 λŒμ•„μ˜€λŠ” 것을 μΆ”μ²œν•œλ‹€.


Java의 HashMapμ—μ„œ μ‚¬μš©ν•˜λŠ” 방식은 Separate Chaining이닀. Open Addressing λŒ€μ‹  Separate Chaining을 μ‚¬μš©ν•˜λŠ” μ΄μœ λŠ” 크게 두 가지이닀.

  • HashMapμ—μ„œ remove()λŠ” λΉˆλ²ˆν•˜κ²Œ 호좜될 수 μžˆλŠ” λ©”μ„œλ“œμΈλ°, Open Addressing은 데이터 μ‚­μ œ 연산을 효율적으둜 μ²˜λ¦¬ν•˜κΈ° μ–΄λ ΅λ‹€. Link
  • Open Addressing의 경우 ν•΄μ‹œ 버킷을 μ±„μš΄ 밀도가 λ†’μ•„μ§ˆμˆ˜λ‘ Worst Case λ°œμƒ λΉˆλ„κ°€ λ†’μ•„μ§€μ§€λ§Œ Separate Chaining은 보쑰 ν•΄μ‹œ ν•¨μˆ˜λ₯Ό μ΄μš©ν•΄ Worst Case λ°œμƒ 상황을 쀄일 수 μžˆλ‹€.

Java 7κ³Ό Java 8μ—μ„œ HashMap이 μ–΄λ–»κ²Œ κ΅¬ν˜„λ˜μ—ˆλŠ”μ§€ μ‚΄νŽ΄λ³΄μž.


Java 7

μ•„λž˜λŠ” Java 7 HashMap의 put() λ©”μ„œλ“œ κ΅¬ν˜„λΆ€μ΄λ‹€.

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {  
        inflateTable(threshold);    // table λ°°μ—΄ 생성
    }
    if (key == null)   // null을 ν‚€λ‘œ μ‚¬μš©ν•œ 경우
        return putForNullKey(value);

    // hashCode()λ₯Ό μ‚¬μš©ν•˜λŠ” 것이 μ•„λ‹ˆλΌ 보쑰 ν•΄μ‹œ ν•¨μˆ˜λ₯Ό μ΄μš©ν•˜λŠ” λ³€ν˜•λœ ν•΄μ‹œ ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•œλ‹€.    
    int hash = hash(key);    

    // i 값이 ν•΄μ‹œ λ²„ν‚·μ˜ μΈλ±μŠ€μ΄λ‹€.
    // indexFor()λŠ” hash % table.length와 같은 μ˜λ„μ˜ λ©”μ„œλ“œμ΄λ‹€.
    int i = indexFor(hash, table.length);

    // ν•΄μ‹œ 버킷에 μžˆλŠ” Linked Listλ₯Ό μˆœνšŒν•œλ‹€.
    // λ§Œμ•½ 같은 ν‚€κ°€ 이미 μ €μž₯λ˜μ–΄ μžˆλ‹€λ©΄ κ΅μ²΄ν•œλ‹€.
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

     modCount++;

    // λ²„ν‚·μ˜ λ§ˆμ§€λ§‰ Entry μ°Έμ‘°κ°’, key, value, hashλ₯Ό μ €μž₯ν•˜λŠ” μƒˆλ‘œμš΄ Entryλ₯Ό 생성해 μ €μž₯ν•œλ‹€. 
    addEntry(hash, key, value, i);
    return null;
}

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;

    // ... 
}

μ—¬κΈ°μ„œ λͺ‡ 가지 μ€‘μš”ν•œ 뢀뢄을 짚고 λ„˜μ–΄κ°€λ©΄,

  • hashCode()λ₯Ό μ‚¬μš©ν•˜λŠ” 것이 μ•„λ‹ˆλΌ hash()λΌλŠ” λ³€ν˜•λœ ν•΄μ‹œ ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•œλ‹€.
  • ν•΄μ‹œ λ²„ν‚·μ˜ μΈλ±μŠ€λŠ” hash % table.length와 같이 κ³„μ‚°ν•œλ‹€.
  • Entry 클래슀λ₯Ό 톡해 Linked List ꡬ쑰λ₯Ό 이룬닀.

put() λ©”μ„œλ“œλ₯Ό μΆ©λΆ„νžˆ μ΄ν•΄ν–ˆλ‹€λ©΄ μ•„λž˜ μ½”λ“œλ₯Ό μ‹€ν–‰μ‹œμΌ°μ„ λ•Œ μ–΄λ–€ κ²°κ³Όκ°€ λ‚˜μ˜¬μ§€ μ˜ˆμΈ‘ν•΄λ³΄μž.

public static void main(String[] args) {

    HashMap<Book, String> map = new HashMap<>();
    Book book1 = new Book(1, "였브젝트");
    Book book2 = new Book(1, "객체 지ν–₯의 사싀과 μ˜€ν•΄");
    Book book3 = new Book(2, "ν† λΉ„μ˜ μŠ€ν”„λ§ Vol.1");

    map.put(book1, "First Input");
    map.put(book2, "Second Input");
    map.put(book3, "Third Input");

    System.out.println(map.get(book1));  
    System.out.println(map.get(book2));   
}

static class Book {

    private int category;
    private String title;

    public Book(int category, String title) {
        this.category = category;
        this.title = title;
    }

    @Override
    public int hashCode() {
        return category;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book key = (Book) o;
        return category == key.category && Objects.equals(title, key.title);
    }
}

Book 클래슀의 hashCode()κ°€ category 값을 λ¦¬ν„΄ν•˜λ„λ‘ μž¬μ •μ˜ν–ˆκΈ° λ•Œλ¬Έμ— book1κ³Ό book2λŠ” λ™μΌν•œ ν•΄μ‹œκ°’μ„ κ°–κ²Œ λœλ‹€. ν•΄μ‹œ 좩돌이 λ°œμƒν–ˆμ§€λ§Œ 두 κ°μ²΄λŠ” λ™μΌν•œ ν•΄μ‹œ 버킷에 Linked List ν˜•νƒœλ‘œ μ €μž₯되기 λ•Œλ¬Έμ— μ•„λž˜μ™€ 같이 First Input이 λˆ„λ½λ˜μ§€ μ•Šκ³  좜λ ₯λœλ‹€.


Java 8

Java 7κ³Ό 달리 Java 8μ—μ„œλŠ” λ°μ΄ν„°μ˜ κ°œμˆ˜κ°€ λ§Žμ•„μ§€λ©΄ Linked List λŒ€μ‹  Treeλ₯Ό μ‚¬μš©ν•œλ‹€. μ‹€μ œ ν•΄μ‹œ 값은 κ· λ“± 뢄포가 μ•„λ‹ˆλ©°, κ· λ“± 뢄포λ₯Ό λ”°λ₯Έλ‹€κ³  ν•˜λ”λΌλ„ birthday problem처럼 일뢀 ν•΄μ‹œ 버킷 λͺ‡ κ°œμ— 데이터가 집쀑될 수 μžˆλ‹€. λ”°λΌμ„œ, 데이터가 일정 개수 이상일 λ•Œ Linked List λŒ€μ‹  Treeλ₯Ό μ‚¬μš©ν•˜λ©΄ μ„±λŠ₯상 이점이 μžˆλ‹€. μ΄λ•Œ μ‚¬μš©ν•˜λŠ” νŠΈλ¦¬λŠ” Red-Black νŠΈλ¦¬μ΄λ‹€.


Linked Listλ₯Ό μ‚¬μš©ν• μ§€, Treeλ₯Ό μ‚¬μš©ν• μ§€λŠ” ν•˜λ‚˜μ˜ ν•΄μ‹œ 버킷에 ν• λ‹Ήλœ ν‚€-κ°’ 쌍의 κ°œμˆ˜μ— 따라 달라진닀. Java 8은 ν•˜λ‚˜μ˜ ν•΄μ‹œ 버킷에 8개의 ν‚€-κ°’ 쌍이 λͺ¨μ΄λ©΄ Linked Listλ₯Ό Tree둜 λ³€κ²½ν•œλ‹€. λ§Œμ•½ ν•΄λ‹Ή 버킷에 μžˆλŠ” 데이터λ₯Ό μ‚­μ œν•˜μ—¬ κ°œμˆ˜κ°€ 6κ°œμ— 이λ₯΄λ©΄ λ‹€μ‹œ Linked List둜 λ³€κ²½ν•œλ‹€

static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;

Java 8μ—μ„œλŠ” Entry 클래슀 λŒ€μ‹  Node 클래슀λ₯Ό μ‚¬μš©ν•œλ‹€. λ˜ν•œ, Tree ꡬ쑰둜 값을 μ €μž₯ν•  수 μžˆλ„λ‘ Node의 ν•˜μœ„ 클래슀인 TreeNodeκ°€ μ‘΄μž¬ν•œλ‹€.

static class Node<K, V> implements Map.Entry<K, V> {
    final int hash;
    final K key;
    V value;
    Node<K, V> next;
    
    Node(int hash, K key, V value, Node<K, V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    		
    // ...
}
    
static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {
    TreeNode<K, V> parent;
    TreeNode<K, V> left;
    TreeNode<K, V> right;
    TreeNode<K, V> prev;
    boolean red;
    
    TreeNode(int hash, K key, V val, Node<K, V> next) {
        super(hash, key, val, next);
    }
    
    // ...
}    

ν•΄μ‹œ 버킷 동적 ν™•μž₯

ν•΄μ‹œ 버킷 개수의 기본값은 16이고, λ°μ΄ν„°μ˜ κ°œμˆ˜κ°€ μž„κ³„μ μ— 이λ₯Ό λ•Œλ§ˆλ‹€ ν•΄μ‹œ 버킷 개수의 크기λ₯Ό 두 λ°°μ”© μ¦κ°€μ‹œν‚¨λ‹€. μž„κ³„μ μ€ ν˜„μž¬μ˜ 데이터 κ°œμˆ˜κ°€ (load factor x ν˜„μž¬μ˜ ν•΄μ‹œ 버킷 개수)에이λ₯Ό λ•Œμ΄λ‹€. load factor의 기본값은 0.75이닀

버킷 κ°œμˆ˜κ°€ 두 배둜 증가할 λ•Œλ§ˆλ‹€, λͺ¨λ“  ν‚€-κ°’ 데이터λ₯Ό 읽어 μƒˆλ‘œμš΄ Separate Chaining을 ꡬ성해야 ν•œλ‹€. λ”°λΌμ„œ, HashMap 객체에 μ €μž₯될 λ°μ΄ν„°μ˜ κ°œμˆ˜κ°€ μ–΄λŠ 정도인지 예츑 κ°€λŠ₯ν•œ κ²½μš°μ—λŠ” μƒμ„±μžλ‘œ 버킷 개수λ₯Ό 지정해 λΆˆν•„μš”ν•œ Separate Chaining μž¬κ΅¬μ„±μ΄ μΌμ–΄λ‚˜μ§€ μ•Šλ„λ‘ ν•  수 μžˆλ‹€.

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    
    // μƒˆ ν•΄μ‹œ 버킷을 μƒμ„±ν•œ λ‹€μŒ κΈ°μ‘΄ 데이터λ₯Ό μƒˆ ν•΄μ‹œ 버킷에 μ €μž₯ν•œλ‹€.
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
    
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    // λͺ¨λ“  ν•΄μ‹œ 버킷에 μžˆλŠ” Linked Listλ₯Ό μˆœνšŒν•œλ‹€.
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
             if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
}

κ·ΈλŸ¬λ‚˜, μ΄λ ‡κ²Œ ν•΄μ‹œ 버킷 크기λ₯Ό 2배둜 ν™•μž₯ν•˜λ©΄ ν•΄μ‹œ λ²„ν‚·μ˜ 개수 M이 2의 κ±°λ“­μ œκ³± ν˜•νƒœκ°€ 되기 λ•Œλ¬Έμ— ν•΄μ‹œ λ²„ν‚·μ˜ 인덱슀λ₯Ό X.hashCode() % M둜 계산할 λ•Œ X.hashCode()의 ν•˜μœ„ a개의 λΉ„νŠΈλ§Œ μ‚¬μš©ν•˜κ²Œ λœλ‹€.

μ•„λž˜ μ˜ˆμ‹œλ₯Ό 보자. 2의 κ±°λ“­μ œκ³±μœΌλ‘œ λ‚˜λ¨Έμ§€ 연산을 ν•˜λ©΄ μ§€μˆ˜λ§ŒνΌμ˜ ν•˜μœ„ λΉ„νŠΈλ₯Ό μ‚¬μš©ν•œλ‹€λŠ” 것을 μ•Œ 수 μžˆλ‹€.

// μ§€μˆ˜κ°€ 2일 λ•Œ ν•˜μœ„ 2개의 λΉ„νŠΈλ§Œ μ‚¬μš©ν•¨. 
18 % 4 = 10010 % 2^2 = 10 = 2
20 % 4 = 10100 % 2^2 = 00 = 0

// μ§€μˆ˜κ°€ 3일 λ•Œ ν•˜μœ„ 3개의 λΉ„νŠΈλ§Œ μ‚¬μš©ν•¨. 
25 % 8 = 11001 % 2^3 = 001 = 1
43 % 8 = 101011 % 2^3 = 011 = 3

즉, ν•¨μˆ˜κ°€ 32λΉ„νŠΈ μ˜μ—­μ„ κ³ λ₯΄κ²Œ μ‚¬μš©ν•˜λ„λ‘ λ§Œλ“€μ—ˆλ‹€ ν•˜λ”λΌλ„ ν•΄μ‹œ 값을 2의 κ±°λ“­μ œκ³±μœΌλ‘œ λ‚˜λˆ„λ©΄ ν•΄μ‹œ 좩돌이 μ‰½κ²Œ λ°œμƒν•  수 μžˆλ‹€.


보쑰 ν•΄μ‹œ ν•¨μˆ˜

index = X.hashCode() % M을 계산할 λ•Œ μ‚¬μš©ν•˜λŠ” M 값은 μ†Œμˆ˜μΌ λ•Œ index κ°’ 뢄포가 κ°€μž₯ κ· λ“±ν•  수 μžˆλ‹€. κ·ΈλŸ¬λ‚˜ M 값이 μ†Œμˆ˜κ°€ μ•„λ‹ˆκΈ° λ•Œλ¬Έμ— λ³„λ„μ˜ 보쑰 ν•΄μ‹œ ν•¨μˆ˜λ₯Ό μ΄μš©ν•˜μ—¬ index κ°’ 뢄포가 가급적 κ· λ“±ν•  수 μžˆλ„λ‘ ν•΄μ•Ό ν•œλ‹€.

Java 7

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // μƒμœ„ λΉ„νŠΈμ˜ 값이 ν•΄μ‹œ λ²„ν‚·μ˜ 인덱슀 값을 κ²°μ •ν•  λ•Œ 반영될 수 μžˆλ„λ‘
	// shift μ—°μ‚°κ³Ό XOR 연산을 μ‚¬μš©ν•œλ‹€.
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

Java 8

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

Java 8의 보쑰 ν•΄μ‹œ ν•¨μˆ˜λŠ” Java 7에 λΉ„ν•΄ κ½€λ‚˜ λ‹¨μˆœν•œ ν˜•νƒœμ˜ 보쑰 ν•΄μ‹œ ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•œλ‹€. κ·Έ μ΄μœ λŠ” 두 가지인데

  • Java 8μ—μ„œλŠ” ν•΄μ‹œ 좩돌이 많이 λ°œμƒν•˜λ©΄ Linked List λŒ€μ‹  Treeλ₯Ό μ‚¬μš©ν•˜λ―€λ‘œ ν•΄μ‹œ 좩돌 μ‹œ λ°œμƒν•  수 μžˆλŠ” μ„±λŠ₯ λ¬Έμ œκ°€ μ™„ν™”λ˜μ—ˆκΈ° λ•Œλ¬Έμ΄λ‹€.
  • 졜근의 ν•΄μ‹œ ν•¨μˆ˜λŠ” κ· λ“± 뢄포가 잘 되게 λ§Œλ“€μ–΄μ§€λŠ” κ²½ν–₯이 λ§Žμ•„, Java 7κΉŒμ§€ μ‚¬μš©ν–ˆλ˜ 보쑰 ν•΄μ‹œ ν•¨μˆ˜μ˜ νš¨κ³Όκ°€ 크지 μ•ŠκΈ° λ•Œλ¬Έμ΄λ‹€.

Reference

0개의 λŒ“κΈ€