자바 소스 코드를 분석 컨테이너 시리즈 -HashMap

HashMap의 구현지도 인터페이스를 제공합니다. HashMap에 매우 널리 사용하지만 스레드로부터 안전하지 않습니다, 여러 스레드에서, 당신은 (여러 스레드가 ConcurrentHashMap의를 권장) 필요한 추가 동기화 메커니즘을 제공해야합니다 사용합니다.

HashMap의 클래스 다이어그램은 주로하지만 구현하지, AbstractMap, 참고로 한 가지를 비교적 간단한 상속 Iterable인터페이스를하지만, HashMap의 자체 반복자 기능을 달성했다.

JDK1.8을 기반으로

회원의 변수와 상수

HashMap의은이다 Node[]각 인덱스가 호출되고 배열 버킷 .

각 키 - 값 쌍을 사용 Node이것은 단일 연결리스트 데이터 구조로 저장한다. 각각의 버킷리스트 링크 키 - 값 쌍들의 다수에 저장 될 수있다.

상수

다음과 같이 HashMap의 상수는 그 중요성에 사용 :

// 初始容量(桶的个数) 2^4 
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
// 最大容量(桶的个数) 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的装载因子(load factor),除非特殊原因,否则不建议修改
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 单个桶上的元素个数大于这个值从链表转成树(树化操作)
static final int TREEIFY_THRESHOLD = 8;
// 单个桶上元素少于这个值从树转成链表
static final int UNTREEIFY_THRESHOLD = 6;
// 只有桶的个数大于这个值时,树化操作才会真正执行
static final int MIN_TREEIFY_CAPACITY = 64;
复制代码

멤버 변수

다음에 사용 HashMap의 멤버 변수 :

// HashMap 中的 table,也就是桶
transient Node<K,V>[] table;
// 缓存所有的键值对 
transient Set<Map.Entry<K,V>> entrySet;
// 键值对的个数
transient int size;
// HashMap 被修改的次数,用于 fail-fast 检查
transient int modCount;
// 进行 resize 操作的临界值,threshold = capacity * loadFactor
int threshold;
// 装载因子
final float loadFactor;
复制代码

노드 테이블은 배열 length일반적 2 ^ N이지만 0 일 수있다.

초기화

사실 HashMap의 초기화는 두 가지 일을 수행 :

  • threadhold의 값을 결정
  • 결정된 값이다 loadFactor

사용자는 초기 용량과 부하율을 통과 할 수있다. HashMap의 용량은 항상 2 ^ N매개 변수가 전달되지 않은 경우 2 ^ N,로 변환 될 것입니다 2 ^ N:

// HashMap.tableSizeFor()
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
复制代码

Integer.numberOfLeadingZeros()반환하는 int (32) 이진 표현의 마지막 비 - 제로 숫자 앞의 0의 수. 예를 들어, 2 :

0000 0000 0000 0000 0000 0000 0000 010
复制代码

따라서 Integer.numberOfLeadingZeros (3) (30)는 반환합니다.

이진의 1로 표현 :

1111 1111 1111 1111 1111 1111 1111 1111
复制代码

>>> 부호 시프트 오른쪽 시프트 오른쪽 30 -1 얻어진다

0000 0000 0000 0000 0000 0000 0000 011
复制代码

(3)를 가져옵니다.

그렇게 한 후 -1 >>> Integer.numberOfLeadingZeros(cap - 1)값을 반환해야합니다 2 ^ N-1마지막 반환 값이 있어야합니다, 그래서 2 ^ N관심 테스트에 갈 수 있습니다.

HashMap의 초기화 시간도 Map 객체, 현재 컨테이너에 다음 들어오는지도 객체의 요소를 받아 들일 수 있습니다.

제 키 - 값 쌍에 삽입 할 때 들어오는 맵 오브젝트의 방법의 예에 더하여, 실제 지연 초기화하는 방법이다 버킷 배열을 생성하지 않을, 상기 호출 resize()배럴을 초기화하는 방법.

의는 상세를 살펴 보자 resize()작업을.

확장 메커니즘

ArrayList를 다른 수동 팽창 과정없이 HashMap의 현재 상황에 따라 자동으로 만 팽창 용기.

확장에 의해 운영 resize()수행 방법, 주로 운영의 확장은 세 가지를 건조 :

  • 배럴의 수를 결정
  • 의 값이 임계 값을 결정
  • 새로운 버킷 모든 요소

매개 변수 설명

  • oldCap : 확장 이전 버킷의 수
  • oldThr : 사전 확장 임계치
  • newCap : 확장 후 버킷의 수
  • newThr : 확장 임계치 후

다음과 같이 확장 프로세스는 다음과 같습니다

생성하는 신규 노드 (배럴) 때 어레이 확장 키 조작의 재 해시 대한 다음 원래 용기하고 새로운 버킷 내로.

해시 MAP, 용량 제한을 갖고 2 ^ {30}, 1073741824 즉, 배럴의 개수가이 번호를 초과하지 않는 최대 임계치는 2147483647이고, 1 회 이하의 최대 용량이다.

이 설정은 버킷의 개수가 최대 용량에 도달 나타내면, 팽창은 작동하지 않는다.

실현

상술 한 바와 같이,도 HashMap의 구성은, 각각의 버킷은 동일한 키의 해시 값 (해시 충돌) 들어, 동일한 버킷 상에 배치되며, 첫 번째 노드의 목록이다. 이것은 또한 HashMap의 해결 해시 충돌라고 지퍼 법 . JDK1.8 후, 키 - 값 쌍의 삽입, 사용시에 보간의 끝을 보간하는 대신 헤드.

의 HashMap과 Hashtable를 기능 대체로 일치. 키와 값의 HashMap에 널이 될 수 있습니다. 여기에 키 값이 있는지 여부의 비교 널 주류지도입니다 :

지도 키는 null 수 있는지 여부 값은 널 (NULL) 일 수 있는지 여부
HashMap의 그것은이다 그것은이다
해시 테이블 아니오 아니오
ConcurrentHashMap의 아니오 아니오
트리 맵 아니오 그것은이다

HashMap의는 스레드로부터 안전하지 않습니다. 다중 스레드 환경에서는 필요는 사용 추가적인 동기화 메커니즘을 사용한다 Map m = Collections.synchronizedMap(new HashMap(...));.

또한 HashMap의 지원이 실패 빠른 메커니즘을.

해시 방법

의 HashMap가 직접 성능에 영향을위한 해시 방법은 매우 중요합니다. 키 삽입 위치는 해쉬 방법에 의해 결정된다. 가정하자 해시 방법의 기본적인 동작으로, 터브 내의 요소들의 균일 한 분포를 허용 get하고 put, 오퍼레이션 시정 수이다 ( O (1)).

해시 방법은 두 가지 특성을 필요

  • 계산의 결과는 임의 충분히 필요
  • 계산은 금액이 너무 크지 않다

다음에 구체적으로 구현 된 해시 맵 :

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

>>>서명되지 않은 우측 시프트 작업입니다, 우리는 위에서 언급했다. 3373707가 바이너리로 변환됩니다 : 내 컴퓨터 값을 계산 핵심 "이름이"있다 가정하자 :

0000 0000 0011 0011 0111 1010 1000 1011
复制代码

마우스 오른쪽 버튼 (16)은 후 :

0000 0000 0000 0000 0000 0000 0011 0011
复制代码

그런 배타적 논리합 연산 :

0000 0000 0011 0011 0111 1010 1011 1000
复制代码

그것은 N되어야하므로 끝이 값 HashMap의 길이 빼기 1 동작을 2 ^ X는 (N-1)의 모든 이진 1의 다음 동작에 대응하는 여러 개의 해시 값을 취 :

index = (n - 1) & hash
复制代码

인덱스 삽입 위치의 열쇠이다.

해시 () 함수는 실제로라는 임의만큼의 주요 위치에 삽입하는 데 사용됩니다 섭동 기능 이 참조 할 수 있습니다 특정 전략에 관심이 있다면, 기사 .

참고 : 의해 Object.hashCode () 객체는 메모리 주소를 반환하는 기본 방법입니다. 클래스가이 방법을 동일 수정할 경우 기본 비교의 메모리 주소의 Object.equals () 메소드는, 다음의 해시 코드 방법은 또한 일관성이 동등하고 hascode의 동작을 수정해야합니다. 키 - 값 쌍의 결과를보고하는 과정에서 등호있을 것이든 당신이 쌍을 찾을 수 있도록 해시 코드가 동일하지 않습니다, 사실이다.

용량 및 부하 요인

:의 HashMap을 사용하는 경우, 두 개의 매개 변수가 성능에 영향이 있습니다 초기 용량부하 계수를 .

HashMap의 용량이 욕조의 수이며, 초기 용량이 생성 된 인스턴스의 수는 때 욕조의 초기화.

확장 결정하는데 사용 부하율 시간 , 확장 동작은 원래 설정한다 배럴의 수 배는 , 컨테이너의 모든 요소가 재 할당 위치 될 것이며, 큰 확장 비용은 팽창 동작 가능한 많이 감소되어야한다.

부하율의 디폴트 값은 트레이드 오프 인 0.75 - 시간 성능공간 오버 헤드 값의. 큰 부하 계수는 공간의 비용 감소, 설정되어 있지만 작업과 같은 성능보기는 떨어질 것이고, 그 반대의 경우도 마찬가지입니다.

HashMap의 초기화에서, 초기 용량과 부하 계수의 값이 조심스럽게 가능한 확장 작업만큼 줄이기 위해 측정해야하며, 특별한 경우 경우, 기본 매개 변수를 사용할 수 있습니다.

개수 HashMap의 용량에 비례 용기 (배럴의 개수) 및 소자를 통과하는 시간이 필요했다. 반복 시간 성능이 중요하다면, 보내지 않는 초기 용량 도 설정을,도해야 부하율이 제공됩니다 작습니다.

나무 작업

구체적인 방법을 설명하기 전에, 당신은 중요한 내부 운영을 HashMap의 알 필요가 : 트리 .

HashMap의 충돌을 해결하기 위해 해시 지퍼 방법을 사용하여. 이 연결 함께 목록에 따라 할당 된 동일한 버킷에 대한 여러 키를 누르십시오. 그러나이 목록이 너무 긴 경우, 다음 많은 작업의 HashMap에 보관할 수 없습니다, 문제에 직면하게 될 것이다 O (1)작동 시간을.

극단적 인 경우, 양동이에있는 모든 키 - 값 쌍. 그런 다음 GET의 시간 복잡도는 제거하고 모두 다른 작업 O (N). HashMap의 솔루션을 사용하는 것입니다 레드 - 블랙 트리 대신 연결리스트, 레드 - 블랙 트리에서 쿼리 안정의 시간 복잡도를 O (logn).

욕조가 트리 목록 (같은로 바뀝니다 뒤에 HashMap의 단일 버킷의 요소의 수는, 욕조 (64) (MIN_TREEIFY_CAPACITY)의 수보다 8 (TREEIFY_THRESHOLD)와 이상을 초과하는 경우 TreeMap),이 작업은 나무 작업이라고합니다.

참고, 욕조 (8)의 단일 요소보다 더 많은,하지만 배럴의 수가 적은 64보다 나무가 작업을 수행하지 않지만, 될 것입니다 때 확장 작업이, 같은 다음과 같습니다 :

// HashMap.treeifyBin() method
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // other code...
}
复制代码

프로세스의 나무는, 다음 성분 (특히 레드 - 블랙 트리의 건설 과정은 레드 - 블랙 트리이 볼 수있는 모든 노드가 교체 TreeNode를 나열하는 기사를 ). 그리고 각 노드 사이의 상대적인 관계시 나무의 목록에 차례로 노드를 통해 변경되지 않는 next관계 변수를 유지하기 위해.

노드 트리 트리 미만 6 (UNTREEIFY_THRESHOLD) 인 경우,이 트리 구조의 목록을 재 변환한다. 링크 된 목록으로, 다음 목록을 재구성하여 트리 노드의 각 노드 :

// HashMap.ubtreeify()
final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    for (Node<K,V> q = this; q != null; q = q.next) {
        Node<K,V> p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
            tl = p;
    }
    return hd;
}
复制代码

심지어 극단적 인 상황 (배럴의 모든 키 - 값 쌍)의 얼굴에, 나무 작업은하지 않습니다 타락한 너무 많은 HashMap의 성능을 보장합니다.

CRUD 작업

방법을 얻을 : 실제적인 방법은는 getNode 성취 get 메소드를 사용하는 것입니다.

// HashMap.getNode()
final Node<K,V> getNode(int hash, Object key) {
    // 首先检查容器是否为 null 以及 key 在容器中是否存在
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 找到相应的桶,从第一个节点开始查找,如果第一个节点不是要找的,后续节点就分成链表或者红黑树进行查找
        if (first.hash == hash &&
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 如果链表已经转成了红黑树,则在红黑树中查找
            if (first instanceof TreeNode)
               return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                // 如果不是树,则在链表中查找
                if (e.hash == hash &&
                   ((k = e.key) == key || (key != null && key.equals(k))))
                   return e;
            } while ((e = e.next) != null);
        }
    }
}
复制代码

풋 방법 : 키 - 값 쌍을 삽입 또는 업데이트가 실제로 사용되는 HashMap.putVal()방법. 당신은 키 - 값 쌍을 삽입 처음이 트리거 확장 작업을.

// HashMap.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);
                   // 如果链表的长度大于等于 8,就会尝试进行树化操作
                   if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                        break;
                }
                // 如果找到了 key,则跳出循环
                if (e.hash == hash &&
                   ((k = e.key) == key || (key != null && key.equals(k))))
                   break;
                p = e;
            }
        }
        // 如果 key 已经存在,则把 value 更新为新的 value
        if (e != null) { 
           V oldValue = e.value;
           if (!onlyIfAbsent || oldValue == null)
               e.value = value;
            return oldValue;
        }
    }
    // fail-fast 版本号更新
    ++modCount;
    // 如果容器中元素的数量大于扩容临界值,则进行扩容
    if (++size > threshold)
        resize();
    return null;
}
复制代码

달성과 방법을 얻기 위해 유사한 방법을 제거합니다.

분명한 방법은 모든 버킷가 쌍을 취소 null로 설정되어 매핑합니다.

다른 작업을 완료하는 데 몇 가지 기본적인 작업의 조합입니다.

JDK8의 새로운 기능

JDK8에서지도는 이러한 방법의 HashMap가 르파 메커니즘에 대한 지원을 추가하기 위해 다시 작성되었습니다 몇 가지 새로운 방법을 추가했습니다.

이러한 방법은 CRUD 달성 상기 방법을 사용한다.

getOrDefault 방법은 값이 디폴트 값 반환 존재하지 않는 경우 :

HashMap map = new HashMap<>();
map.put("name", "xiaomi");

map.getOrDefault("gender","genderNotExist"); // genderNotExist
复制代码

foreach는 방법은 이송지도 키 - 값 쌍은 람다 식을 획득 할 수있다 :

HashMap<String, Object> map = new HashMap<>();
map.put("name", "xiaomi");

map.forEach((k, v) -> System.out.println(k +":"+ v));
复制代码

키에 삽입 된 키가 존재하지 않는 경우에만 적 putIfAbsent 방법 :

HashMap<String, Object> map = new HashMap<>();
map.put("name", "xiaomi");

map.putIfAbsent("gender", "man");
复制代码

computeIfAbsent 방법은 어떤 작업을 간단하게하기 위해, 다음 방법 (1) 및 다음 주요 일부 사후 처리 키 값이 삽입 된지도에 의해, 존재하지 않는 경우와 같이 2 개 기능 :

HashMap<String, Object> map = new HashMap<>();
map.put("name", "xiaomi");

// 方法1:
Integer age = (Integer)map.get("key");
if (age == null) {
    age = 18;
    map.put("key", age);
}
// 方法2:
map.computeIfAbsent("age",  k -> {return 18;});
复制代码

computeIfPresent 방법은, 키, 키 - 값 쌍들의 처리의 존재이고, 그지도 똑같은 기능 다음 방법 1과 2를 업데이트

HashMap<String, Object> map = new HashMap<>();
map.put("name", "xiaomi");

// 方法1:
Integer age = (Integer)map.get("key");
Integer age = 18 + 4;
map.put("key", age);

// 方法2:
map.computeIfPresent("age", (k,v) -> {return 18 + 4;});
复制代码

동일한 키 값을 병합하는 데있어서, 다음 방법 (1)과 일치하는 특징이 결합된다 :

HashMap<String, Object> map = new HashMap<>();
map.put("name", "xiaomi");

// 方法1:
Integer age = (Integer)map.get("key");
age += 14;
map.put("key", age);

// 方法2:
map.merge("age", 18, (oldVal, newVal) -> {return (Integer)oldVal + (Integer)newVal;});
复制代码

다른 기능

HashMap의도, HashMap의 반복자의 세 가지 특정 실현있다 반복 기능을 실현 :

  • KeyIterator는 : 키의지도를 탐색
  • ValueIterator는 : 값의지도를 탐색
  • EntryIterator : 맵의 키와 값을 통과하면서

그러나 세 반복자는 직접하지만, 간접적으로 HashMap의 메소드를 호출하여 얻을 수없는 것입니다.

  • HashMap.keySet 의해 KeyIterator () 획득 및 사용 방법
  • HashMap.vlaues 의해 ValueIterator () 획득 및 사용 방법
  • TreeMap.entrySet 의해 EntryIterator () 획득 및 사용 방법

유사한 구현 Spliterator 달성 된 각 키 값 및 키 값 +위한 반복기 Spliterator.

독창적 인

관련 기사

어떤 마이크로 채널이 없습니다 주목, 이야기에 뭔가 다른

추천

출처juejin.im/post/5e06b72ee51d455846232c93