HashMap Java 8 구현


92

다음 링크 문서에 따라 : Java HashMap 구현

의 구현 HashMap(또는 오히려 향상 HashMap) 과 혼동됩니다 . 내 쿼리는 다음과 같습니다.

첫째로

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

이 상수는 왜 그리고 어떻게 사용됩니까? 이에 대한 명확한 예가 필요합니다. 이를 통해 어떻게 성능 향상을 달성하고 있습니까?

둘째

HashMapJDK에서 의 소스 코드를 보면 다음과 같은 정적 내부 클래스를 찾을 수 있습니다.

static final class TreeNode<K, V> extends java.util.LinkedHashMap.Entry<K, V> {
    HashMap.TreeNode<K, V> parent;
    HashMap.TreeNode<K, V> left;
    HashMap.TreeNode<K, V> right;
    HashMap.TreeNode<K, V> prev;
    boolean red;

    TreeNode(int arg0, K arg1, V arg2, HashMap.Node<K, V> arg3) {
        super(arg0, arg1, arg2, arg3);
    }

    final HashMap.TreeNode<K, V> root() {
        HashMap.TreeNode arg0 = this;

        while (true) {
            HashMap.TreeNode arg1 = arg0.parent;
            if (arg0.parent == null) {
                return arg0;
            }

            arg0 = arg1;
        }
    }
    //...
}

어떻게 사용합니까? 알고리즘에 대한 설명이 필요합니다 .

답변:


225

HashMap특정 수의 버킷을 포함합니다. hashCode어떤 버킷에 넣을지 결정하는 데 사용 됩니다. 단순함을 위해 그것을 계수로 상상하십시오.

해시 코드가 123456이고 버킷이 4 개인 123456 % 4 = 0경우 항목은 첫 번째 버킷 인 버킷 1에 들어갑니다.

HashMap

우리의 해시 코드 함수가 좋다면, 모든 버킷이 어느 정도 균등하게 사용될 수 있도록 균등 한 분포를 제공해야합니다. 이 경우 버킷은 연결 목록을 사용하여 값을 저장합니다.

연결된 버킷

그러나 좋은 해시 함수를 구현하기 위해 사람에게 의존 할 수는 없습니다. 사람들은 종종 불량한 해시 함수를 작성하여 균등하지 않은 분포를 초래합니다. 우리가 입력 한 내용에 대해 불행해질 수도 있습니다.

잘못된 해시 맵

이 분포가 적을수록 O (1) 작업에서 멀어지고 O (n) 작업으로 더 가까워집니다.

Hashmap의 구현은 버킷이 너무 커지면 연결 목록이 아닌 일부 버킷을 트리로 구성하여이를 완화하려고합니다. 이것은 무엇 TREEIFY_THRESHOLD = 8을위한 것입니다. 버킷에 8 개 이상의 항목이 포함 된 경우 트리가되어야합니다.

나무 통

이 나무는 Red-Black 나무입니다. 먼저 해시 코드로 정렬됩니다. 해시 코드가 동일 하면 객체가 해당 인터페이스를 구현하는지 여부와 ID 해시 코드를 사용하는 compareTo방법을 사용합니다 Comparable.

맵에서 항목이 제거되면 버킷의 항목 수가 줄어들어이 트리 구조가 더 이상 필요하지 않을 수 있습니다. 그게 UNTREEIFY_THRESHOLD = 6목적입니다. 버킷의 요소 수가 6 개 미만으로 떨어지면 다시 연결 목록을 사용하는 것이 좋습니다.

마지막으로 MIN_TREEIFY_CAPACITY = 64.

해시 맵의 크기가 커지면 더 많은 버킷을 갖도록 자동으로 크기가 조정됩니다. 우리가 작은 해시 맵을 가지고 있다면, 우리가 물건을 넣을 다른 버킷이 그렇게 많지 않기 때문에 매우 가득 찬 버킷을 얻을 가능성이 상당히 높습니다. 덜 꽉 찬 버킷이 더 많은 더 큰 해시 맵을 갖는 것이 훨씬 낫습니다. 이 상수는 기본적으로 해시 맵이 매우 작은 경우 버킷을 트리로 만들기 시작하지 말라고 말합니다. 대신 먼저 크기를 크게 조정해야합니다.


성능 향상에 대한 질문에 답하기 위해 이러한 최적화를 추가하여 최악의 경우 를 개선했습니다 . 나는 추측에 불과하지만 hashCode기능이 좋지 않은 경우 이러한 최적화로 인해 눈에 띄는 성능 향상을 볼 수 있습니다.


3
균등하지 않은 분포가 항상 잘못된 해시 함수의 징후는 아닙니다. 예를 들어 일부 데이터 유형 Stringint해시 코드 보다 훨씬 더 큰 값 공간을 가지므로 충돌을 피할 수 없습니다. 이제는 실제 값과 같은 실제 값에 따라 달라집니다 String. 균등 분포를 얻었는지 여부에 관계없이지도에 입력합니다. 잘못된 분배는 단지 불운의 결과 일 수 있습니다.
Holger

3
+1,이 트리 접근 방식이 완화하는 특정 시나리오가 해시 충돌 DOS 공격 이라는 점을 추가하고 싶습니다 . java.lang.String결정 론적 비 암호화 hashCode이므로 공격자는 충돌하는 hashCodes로 구별되는 문자열을 사소하게 만들 수 있습니다. 이 최적화 이전에는 HashMap 작업이 O (n) -time으로 저하 될 수 있었지만 이제는 O (log (n))로 저하됩니다.
MikeFHay

1
+1, if the objects implement that interface, else the identity hash code.다른 부분을 찾고있었습니다.
Number945

1
@NateGlenn 당신이 그것을 재정의하지 않는 경우 기본 해시 코드
Michael

"이 상수는 기본적으로 해시 맵이 매우 작 으면 버킷을 트리로 만들지 말라고합니다. 대신 먼저 크기를 크게 조정해야합니다." 대한 MIN_TREEIFY_CAPACITY. 이것은 "이미 8 ( TREEIFY_THRESHOLD) 키를 포함하는 버킷에 해시 할 키를 삽입하고 에 이미 64 ( MIN_TREEIFY_CAPACITY) 키가있는 HashMap경우 해당 버킷의 링크 된 목록이 균형 트리로 변환됩니다." 라는 의미입니까?
anir

16

더 간단하게 말하면 (더 간단하게 할 수있는 한) + 몇 가지 세부 사항.

이러한 속성은 직접 이해하기 전에 이해하기 매우 멋진 내부 요소에 따라 달라집니다.

TREEIFY_THRESHOLD- > 단일 버킷이 여기에 도달하면 (총 수가를 초과하면 MIN_TREEIFY_CAPACITY) 완벽하게 균형 잡힌 레드 / 블랙 트리 노드 로 변환됩니다 . 왜? 검색 속도 때문입니다. 다른 방식으로 생각해보십시오.

그것은 걸릴 최대 32 단계를 양동이 / 빈 내에서 항목을 검색 에 Integer.MAX_VALUE 항목.

다음 주제에 대한 소개입니다. bin / bucket의 수가 항상 2의 제곱 인 이유는 무엇 입니까? 적어도 두 가지 이유 : 모듈로 연산보다 빠르며 음수에 대한 모듈로는 음수가됩니다. 그리고 항목을 "음수"버킷에 넣을 수 없습니다.

 int arrayIndex = hashCode % buckets; // will be negative

 buckets[arrayIndex] = Entry; // obviously will fail

대신 모듈로 대신 사용되는 멋진 트릭이 있습니다.

 (n - 1) & hash // n is the number of bins, hash - is the hash function of the key

이는 모듈로 연산 과 의미 상 동일 합니다. 하위 비트를 유지합니다. 다음과 같은 경우 흥미로운 결과가 나타납니다.

Map<String, String> map = new HashMap<>();

위의 경우 항목이 어디로 가는지 결정은 해시 코드 의 마지막 4 비트만을 기준 으로합니다.

이것은 버킷을 곱하는 것이 작용하는 곳입니다. 특정 조건에서 ( 정확한 세부 사항 을 설명하는 데 많은 시간이 소요됨 ) 버킷 크기가 두 배가됩니다. 왜? 버킷의 크기가 두 배가되면 한 가지 비트가 더 작용 합니다.

따라서 16 개의 버킷이 있습니다. 해시 코드의 마지막 4 비트는 항목이 어디로 가는지 결정합니다. 버킷을 두 배로 늘립니다. 32 개 버킷-마지막 5 개 비트가 항목이 이동할 위치를 결정합니다.

따라서이 프로세스를 재해 싱이라고합니다. 느려질 수 있습니다. 그것은 HashMap이 빠르고, 빠르며, 빠르며, 느리게 "농담"하기 때문에 (관심있는 사람들을 위해) 입니다. 다른 구현이 있습니다- 일시 중지없는 해시 맵 검색 ...

이제 다시 해싱 후 UNTREEIFY_THRESHOLD 가 작동합니다. 이 시점에서 일부 항목은이 저장소에서 다른 저장소로 이동할 수 있으며 ( (n-1)&hash계산에 1 비트를 더 추가 하여 다른 버킷으로 이동할 수 있음)이 저장소에 도달 할 수 있습니다 UNTREEIFY_THRESHOLD. 이 시점에서로 함을 유지하기 위해 돈을 지불하지 않습니다 red-black tree node하지만 같은, LinkedList같은 대신,

 entry.next.next....

MIN_TREEIFY_CAPACITY 는 특정 버킷이 트리로 변환되기 전의 최소 버킷 수입니다.


10

TreeNode의 단일 저장소에 속하는 항목을 저장하는 다른 방법 HashMap입니다. 이전 구현에서는 bin의 항목이 연결 목록에 저장되었습니다. Java 8에서는 bin의 항목 수가 임계 값 ( TREEIFY_THRESHOLD)을 통과 하면 원래 링크 목록 대신 트리 구조에 저장됩니다. 이것은 최적화입니다.

구현에서 :

/*
 * Implementation notes.
 *
 * This map usually acts as a binned (bucketed) hash table, but
 * when bins get too large, they are transformed into bins of
 * TreeNodes, each structured similarly to those in
 * java.util.TreeMap. Most methods try to use normal bins, but
 * relay to TreeNode methods when applicable (simply by checking
 * instanceof a node).  Bins of TreeNodes may be traversed and
 * used like any others, but additionally support faster lookup
 * when overpopulated. However, since the vast majority of bins in
 * normal use are not overpopulated, checking for existence of
 * tree bins may be delayed in the course of table methods.

정확히 사실이 아닙니다 . 그들이 전달하는 경우 TREEIFY_THRESHOLD 빈의 총 수는 적어도입니다 MIN_TREEIFY_CAPACITY. 나는 ... 내 대답에 그 다루 시도했습니다
유진

3

시각화해야합니다. 항상 동일한 값을 반환하도록 hashCode () 함수 만 재정의 된 클래스 키가 있다고 가정합니다.

public class Key implements Comparable<Key>{

  private String name;

  public Key (String name){
    this.name = name;
  }

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

  public String keyName(){
    return this.name;
  }

  public int compareTo(Key key){
    //returns a +ve or -ve integer 
  }

}

그런 다음 다른 곳에서는 모든 키가이 클래스의 인스턴스 인 HashMap에 9 개의 항목을 삽입합니다. 예 :

Map<Key, String> map = new HashMap<>();

    Key key1 = new Key("key1");
    map.put(key1, "one");

    Key key2 = new Key("key2");
    map.put(key2, "two");
    Key key3 = new Key("key3");
    map.put(key3, "three");
    Key key4 = new Key("key4");
    map.put(key4, "four");
    Key key5 = new Key("key5");
    map.put(key5, "five");
    Key key6 = new Key("key6");
    map.put(key6, "six");
    Key key7 = new Key("key7");
    map.put(key7, "seven");
    Key key8 = new Key("key8");
    map.put(key8, "eight");

//Since hascode is same, all entries will land into same bucket, lets call it bucket 1. upto here all entries in bucket 1 will be arranged in LinkedList structure e.g. key1 -> key2-> key3 -> ...so on. but when I insert one more entry 

    Key key9 = new Key("key9");
    map.put(key9, "nine");

  threshold value of 8 will be reached and it will rearrange bucket1 entires into Tree (red-black) structure, replacing old linked list. e.g.

                  key1
                 /    \
               key2   key3
              /   \   /  \

트리 순회는 LinkedList {O (n)}보다 {O (log n)} 더 빠르며 n이 커질수록 차이가 더 커집니다.


모두 동일한 해시 코드와 순서 지정에 도움이되지 않는 equals 메서드 이외의 키를 비교할 방법이 없기 때문에 효율적인 트리를 구축 할 수 없습니다.
user253751

@immibis 그들의 해시 코드가 반드시 동일하지는 않습니다. 그들은 상당히 다를 가능성이 있습니다. 클래스가이를 구현하는 경우 추가로 compareTofrom Comparable. identityHashCode사용하는 또 다른 메커니즘입니다.
Michael

@Michael이 예제에서 모든 해시 코드는 반드시 동일하며 클래스는 Comparable을 구현하지 않습니다. identityHashCode는 올바른 노드를 찾는 데 쓸모가 없습니다.
user253751

@immibis 아 예, 나는 그것을 훑어 보았지만 당신 말이 맞습니다. 따라서 Key구현되지 않은 Comparable, identityHashCode사용됩니다 :)
Michael

@EmonMishra 불행히도 단순히 시각적으로 충분하지 않을 것입니다. 나는 내 대답에서 그것을 다루려고 노력했습니다.
Eugene

2

HashMap 구현의 변경 사항이 JEP-180 에 추가되었습니다 . 목적은 다음과 같습니다.

맵 항목을 저장하기 위해 링크 된 목록이 아닌 균형 잡힌 트리를 사용하여 높은 해시 충돌 조건에서 java.util.HashMap의 성능을 향상시킵니다. LinkedHashMap 클래스에서 동일한 개선 사항 구현

그러나 순수한 성능이 유일한 이득은 아닙니다. 또한 해시 맵을 사용하여 사용자 입력을 저장하는 경우 HashDoS 공격방지 합니다. 버킷에 데이터를 저장하는 데 사용되는 레드-블랙 트리는 O (log n)에서 최악의 삽입 복잡성을 갖기 때문 입니다. 이 트리는 특정 기준이 충족 된 후에 사용됩니다 . Eugene의 답변을 참조하십시오 .


-1

hashmap의 내부 구현을 이해하려면 해싱을 이해해야합니다. 가장 간단한 형태의 해싱은 속성에 공식 / 알고리즘을 적용한 후 변수 / 객체에 고유 코드를 할당하는 방법입니다.

진정한 해시 함수는이 규칙을 따라야합니다.

“해시 함수는 동일하거나 동일한 객체에 함수가 적용될 때마다 매번 동일한 해시 코드를 반환해야합니다. 즉, 두 개의 동일한 객체가 일관되게 동일한 해시 코드를 생성해야합니다.”


이것은 질문에 대한 답이 아닙니다.
Stephen C
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.