static int indexFor(int h, int length) {
return h & (length-1);
}
작업 결과 얻은 해시 코드 hashCode()
와 내부 배열의 길이(셀 수)를 입력으로 사용했습니다. 그리고 결과는 "해시 코드" -> 비트 단위 "AND" -> (배열 길이 - 1)로 반환되었습니다. 클래스는 HashMap
클래스를 상속 AbstractMap
하고 다음 인터페이스를 구현합니다: Map
, Cloneable
, Serializable
. .method는 Java의 해시 함수를 담당합니다 hashCode()
. 기본 구현은 ID 해시 코드hashCode()
라는 값을 반환합니다 . 클래스가 를 재정의하더라도 를 호출하여 언제든지 객체의 ID 해시를 얻을 수 있습니다 . OpenJDK의 기본 구현은 때때로 믿어지는 것처럼 메모리 주소와 아무 관련이 없습니다. 자세한 내용은 여기를 참조하세요. habr HashMap에서 해시 테이블은 단일 연결 목록의 배열(또는 테이블이 확장될 수 있으므로 더 정확하게는 동적)을 기반으로 구현됩니다. 본질적으로 우리는 메소드의 결과로 키의 해시 코드를 얻은 후 수정되고(나중에 고려할 것임) 내부적으로 추가 메소드를 사용하여 결과 값이 필요한 셀에 배포됩니다. 배열 요소(셀)는 버킷 이라고도 하며 개별 노드를 저장하는 데 사용됩니다. 각 버킷은 컬렉션(목록 또는 트리)입니다. 노드는 중첩 클래스 (또는 트리 구조) 의 개체입니다 . 실제로 배열 셀 내부에는 단일 연결 리스트 또는 다른 클래스 구현의 기초가 되는 레드-블랙 트리만 있습니다 . hashCode()
System.identityHashCode(Object o)
hashCode()
hashCode()
Node
TreeNode
LinkedList
TreeMap
HashMap
는 다음 필드를 포함하는 중첩 클래스입니다 .
final int hash
— 키를 해싱한 결과로 얻은 현재 요소의 해시final K key
— 현재 요소의 키입니다. 메소드의 첫 번째 객체로 지정한 내용이 여기에 기록됩니다put()
.V value
— 현재 요소의 값. 그리고 메소드에서 두 번째 객체로 지정하는 내용이 여기에 기록됩니다put()
.Node < K, V> next
— 동일한 바구니 내의 다음 노드에 연결됩니다. 목록은 연결되어 있으므로 다음 노드가 아닌 링크가 필요합니다(있는 경우).
transient Node < K, V> [] table
– 노드 형태로 키-값 쌍을 저장하기 위해 배열을 기반으로 구현된 해시 테이블 자체입니다. 여기에 노드가 저장됩니다.transient int size
— 키-값 쌍의 수int threshold
— 해시 테이블의 크기가 두 배가 되는 최대 요소 수입니다. 공식(capacity * loadFactor)을 사용하여 계산됩니다.final float loadFactor
— 이 매개변수는 현재 해시 테이블이 새 해시 테이블을 생성하는 데 필요한 로드 정도를 결정합니다. 해시 테이블이 75% 차면 새 해시 테이블이 생성되고 현재 요소가 그 테이블로 이동됩니다(모든 요소를 다시 해시해야 하기 때문에 비용이 많이 드는 작업).transient Set< Map.Entry< K,V>> entrySet
entrySet()
- 반복할 수 있는 캐시가 포함되어 있습니다HashMap
.
static final int DEFAULT_INITIAL_CAPACITY= 1 << 4
— 기본 해시 테이블 용량(16)static final int MAXIMUM_CAPACITY = 1 << 30
— 해시 테이블의 가능한 최대 용량(약 10억)static final float DEFAULT_LOAD_FACTOR = 0.75f
- 기본 하중 계수;static final int TREEIFY_THRESHOLD = 8
한 버킷에 있는 요소 수의 "임계값"이며, 이에 도달하면 내부 연결 목록이 트리 구조(레드-블랙 트리)로 변환됩니다.static final int UNTREEIFY_THRESHOLD = 6
— 한 바구니의 요소 수가 6으로 감소하면 트리에서 연결된 목록으로의 역전이가 발생합니다.static final int MIN_TREEIFY_CAPACITY = 64
— 트리 구조로의 전환이 가능한 해시 테이블의 최소 용량(버킷 수)입니다. 저것들. 해시 테이블에 최소 64개의 버킷이 있고 하나의 버킷에 8개 이상의 요소가 있는 경우 트리 구조로 전환됩니다.
public HashMap()
— 기본적으로 해시 디스플레이를 생성합니다: 볼륨(capacity) = 16
및 부하 계수 기준(load factor) = 0.75
;public HashMap(Map< ? extends K, ? extends V> m)
— 다른 매핑의 요소를 수용하기에 충분한 초기 용량을 사용하여 다른 매핑의 요소로 초기화된 해시 매핑을 생성합니다.public HashMap(int initialCapacity)
— 주어진 초기 용량으로 해시 맵을 생성합니다. HashMap이 올바르게 작동하려면 내부 배열의 크기가 2의 거듭제곱(예: 16, 64, 128 등)이어야 합니다.public HashMap(int initialCapacity, float loadFactor)
— 지정된 매개변수(초기 용량 및 부하 계수)를 사용하여 해시 맵을 생성합니다.
Map
클래스를 확장합니다 . 해시 맵은 해당 요소의 순서를 보장하지 않습니다. 따라서 요소가 해시 맵에 입력되는 순서는 반복자가 요소를 검색하는 순서와 반드시 일치하지는 않습니다. 객체 추가 키-값 쌍 추가는 . 객체를 추가할 때 관련된 단계를 살펴보겠습니다. AbstractMap
put()
-
입력된 객체의 키에 대한 해시값을 계산합니다. 키 해시는 우리가 알고 있는 키 메서드
static final int hash(Object key)
에 이미 액세스하는 메서드에 의해 계산됩니다hashCode()
. 이를 위해 재정의된 메서드hashCode()
나 해당 기본 구현이 사용됩니다. 메소드의 추가 변환hash()
:static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
Почему бы просто не вычислить с помощью
hashCode()
? Это сделано из-за того, чтоhashCode()
можно реализовать так, что только нижние битыint
'a будут заполнены. Например, дляInteger
,Float
– если мы в HashMap кладем маленькие значения, то у них и биты хеш-codeов будут заполнены только нижние. В таком случае ключи в HashMap будут иметь тенденцию скапливаться в нижних ячейках, а верхние будут оставаться пустыми, что не очень эффективно. На то, в Howой бакет попадёт новая запись, влияют только младшие биты хеша. Поэтому и придумали различными манипуляциями подмешивать старшие биты хеша в младшие, чтобы улучшить распределение по бакетам (чтобы старшие биты родного хеша an object начали вносить коррективы в то, в Howой бакет попадёт an object) и, How следствие, производительность. Потому и придумана дополнительная функцияhash
внутри HashMap. -
Вычисляем индекс бакета (ячейки массива), в который будет добавлен наш элемент:
i = (n - 1) & hash
где n – длина массива.
-
Создается an object Node.
-
Теперь, зная индекс в массиве, мы получаем список (цепочку) элементов, привязанных к этой ячейке. Если в бакете пусто, тогда просто размещаем в нем элемент. Иначе хэш и ключ нового element поочередно сравниваются с хешами и ключами элементов из списка и, при совпадении этих параметров, meaning element перезаписывается. Если совпадений не найдено, элемент добавляется в конец списка.
Теперь к очень подробному примеру.
-
Создадим an object класса HashMap:
HashMap < String, Integer> map = new HashMap<>();
-
С помощью метода
put()
добавим в хеш-отображение первую пару «ключ-meaning»:map.put("KING", 100);
В этот момент внутри вызывается метод
putVal()
. -
С помощью хеш-функции, роль которой играет метод hash, вычисляется хеш-code ключа, внутри которого предварительно вычисляется хеш-code с помощью метода hashCode() (в данном случае класса String), в результате чего мы получаем его «предварительное meaning» – 2306967. Может проверить в IDEA с помощью
System.out.println("KING".hashCode());
Полученный хеш-code модифицируется по формуле:
(хеш-code) ^ (хеш-code>>> 16)
, и в результате получаем окончательный хеш-code – 2306996. -
Проверяем таблицу на «пустоту»:
if ((tab = table) == null || (n = tab.length) == 0)
где
[] tab
— сама хеш-table: ссылаем tab и table (напомню, что это поле класса HashMap, которое хранит массив для наших элементов) на один и тот же массив, а int n – дополнительная переменная-счетчик.Так How проверка вернёт true (потому что массив для таблицы не был создан с помощью оператора new в конструкторе), будет вызван метод
resize()
, который собственно и создаст таблицу размером на 16 элементов. Да-да, конструкторы класса ниHowой таблицы не создают. Вместо этого всегда происходит вызов методаresize()
при первом добавлении element. Длина созданной таблицы (считай длина массива) будет записана в переменнуюn – n = (tab = resize()).length
, которая в дальнейшем используется для вычисления бакета. -
동시에 객체가 배치될 버킷의 인덱스를 계산하고 해당 객체에 이미 요소가 있는지 확인합니다. 우리는 다음을 계산합니다:
i = (n - 1) & hash i = (16 - 1) & 2306996 i = 4
확인하다:
if ((p = tab[i = (n - 1) & hash]) == null)
-
확인 결과 true(버킷에 요소가 없음)가 나오므로 다음 필드가 있는 Node 객체가 생성됩니다.
{ int hash = 2306996 — сгенерированный хеш-code String key = {"KING"} — ключ Integer value = 100 — meaning Node next = null — link на следующий узел }
생성된 노드 객체는 인덱스 [4] 아래 버킷에 추가됩니다.
tab[i] = newNode(hash, key, value, null); tab[4] = newNode(2306996, “KING”, 100, null);
newNode()
단순히 Node 클래스의 객체를 반환하는 메서드입니다. -
추가한 후에는 현재 요소 수가 매개변수를 초과하는지 확인합니다
threshold
.if (++size > threshold) resize();
resize()
초과하는 경우 해시 테이블의 크기를 늘리는 메서드가 호출됩니다 .이 시점에서 메서드
putVal()
(및 각각put()
)가 작업을 완료합니다.결과는 다음과 같이 그래픽으로 표현될 수 있습니다.
일반적으로 원하는 버킷이 비어 있는 경우 해시 테이블에 노드(키-값 쌍)를 추가하는 모습은 다음과 같습니다 . 이제 충돌을 일으키는 요소를 추가해 보겠습니다(한 버킷에 두 개 이상의 요소가 있는 경우).
-
- 외부 연결 또는 연결 방법(HashMap에서 구현됨) - 즉 셀에는 실제로 목록(체인)이 포함되어 있습니다. 그리고 목록에는 이미 여러 값이 포함될 수 있습니다(해시 코드가 반드시 동일할 필요는 없음).
- 선형 탐색 또는 개방형 주소 지정 방법(IdentityHashMap에서 구현됨) - 해시 함수가 가리키는 셀 다음의 첫 번째 빈 셀을 검색하는 것으로 구성됩니다.
-
이 방법을 사용하여
put()
해시 매핑에 또 다른 키-값 쌍을 추가합니다.map.put("BLAKE", 10);
-
우리는 "예비 해시" – 63281361을 계산합니다. 이를 수정하고 결과적으로 최종 해시 코드 – 63281940을 얻습니다.
-
"비어 있음"에 대한 첫 번째 검사는 이제 false를 반환하므로(테이블을 만들 필요가 없음) 객체가 배치될 버킷의 인덱스를 즉시 계산합니다.
i = (n - 1) & hash i = (16 - 1) & 63281940 i = 4
-
지정된 인덱스의 버킷에 요소가 있는지 확인하고
if ((p = tab[i = (n - 1) & hash]) == null)
이 경우 조건이 충족되지 않으므로(버킷에 이미 요소가 있음) 블록으로 이동합니다else
. -
먼저 추가되는 요소를 버킷 내부 연결 목록의 첫 번째 요소와 비교합니다.
(p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
При проверке сначала сравниваются хеши ключей. Если этот «участок»
(p.hash == hash)
возвращает false, тогда остальная часть условия игнорируется (&&
), так How an objectы гарантированно разные. Иначе затем сравниваются ключи по ссылке (==) и в случае неequalsства, ключи сравниваются уже посредством методаequals()
. Сравнение осуществляется в таком порядке во благо производительности. Если все три выражения возвращают true, это означает, что ключи равны и новый элемент будет записан в дополнительную переменную, чтобы в дальнейшем с её помощью перезаписать meaning у ключа:if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
В результате сравнения ключей мы получаем false уже на первом этапе (разные хеши).
-
Игнорируем condition (
p instanceof TreeNode
), так How структура в бакете не является древовидной на данном этапе. -
Далее переходим в цикл
for
, где в пределах одного бакета проверяем у элементов указатель на следующий элементnext
, и если он equals null (значит элемент в списке последний и единственный), добавляем новый элемент Node в конец списка:if ((e = p.next) == null){ p.next = newNode(hash, key, value, null) ... };
Вы можете спросить, а где же проверка на equalsство ключей? Мы же не можем просто взять и добавить новый элемент. Так вот она уже была заранее осуществлена в пункте (5). Благодаря этому, теперь мы можем просто проверить указатель этого element, и так How он сейчас equals null, можно сделать вывод о том, что в списке только один элемент. И так How мы уже проверяли их ключи, мы можем безболезненно добавлять новый элемент.
Если же при первой итерации указатель не equals null, это говорит о том, что в списке How минимум два element, поэтому в таком случае мы переходим к следующему условия и сравниваем ключ добавляемого element со всеми ключами элементов в списке (способом, описанным в пятом пункте).
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
Если при сравнении ключей будет найдено совпадение, новый элемент будет записан в дополнительную переменную, чтобы в дальнейшем с её помощью перезаписать meaning у ключа.
После добавления второго element наш an object HashMap графически выглядит так:
В итоге, добавление элементов при коллизии (в пределах одного бакета) выглядит следующим образом:
- проверяем с помощью методов
hashCode()
иequals()
, что оба ключа одинаковы. - если ключи одинаковы, заменить текущее meaning новым.
- иначе связать новый и старый an objectы с помощью структуры данных "связный список", указав ссылку на следующий an object в текущем и сохранить оба под нужным индексом; либо осуществить переход к древовидной структуре
- проверяем с помощью методов
-
После каждой итерации (добавления нового element) в цикле for увеличивается счетчик, который отвечает за количество элементов в бакете:
for (int binCount = 0; ; ++binCount)
До тех пор, пока их количество не станет равно or больше 7:
binCount >= TREEIFY_THRESHOLD – 1
В таком случае произойдет вызов метода
treeifyBin()treeify()
для непосредственного построения древовидной структуры. Однако, если количество бакетов в текущей хеш-таблице меньше 64:if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize()
트리 구조로 이동하는 대신 해시 테이블의 크기를 늘리고 요소를 재배포하는 메서드가 호출됩니다 .treeify()
그러면 연결된 목록이TreeNode
레드-블랙 트리로 변환됩니다. 이 메서드는resize()
현재 저장소의 모든 요소를 반복하고 해당 인덱스를 다시 계산하고(새 크기를 고려하여) 요소를 새 배열로 재배포합니다.간단히 말해서, 레드-블랙 트리의 구조를 자세히 설명하지 않고 다음과 같은 일이 발생합니다.
-
목록의 첫 번째 요소는 전체 트리의 루트(검은색)로 기록됩니다.
if (root == null) { x.parent = null; x.red = false; root = x; }
-
기타 요소의 경우:
해시 값에 따라 왼쪽이나 오른쪽으로 배포합니다.
if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1;
모든 왼쪽 자식은 루트 노드보다 작거나 같아야 하며, 오른쪽 자식은 모두 커야 합니다.
-
두 요소의 해시가 동일하고 다른 방법으로 비교할 수 없는 경우(인터페이스를 구현하지 않음
Comparable
) 트리 구성을 중단하고 메서드를 호출합니다tieBreakOrder()
. 그러면 기본 메서드를 사용하여System.identityHashCode()
전역적으로 고유한 해시 코드를 계산합니다. .자세한 내용은 여기: 기사 링크
-
TreeNode
하위(왼쪽 또는 오른쪽) 0 요소가 발견될 때까지 트리 요소(객체)를 확인합니다 .if ((p = (dir <= 0) ? p.left : p.right) == null)
-
하위 노드를 추가합니다(dir에 따라 왼쪽 또는 오른쪽).
x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x;
-
새로운 요소를 추가하면 트리의 균형이 깨질 수 있으므로 재조정 방법을 호출합니다.
root = balanceInsertion(root, x);
여기에서 CCH 균형 조정에 대해 읽을 수 있습니다: habr
-
균형을 맞춘 후에 루트 요소가 변경될 수 있습니다. 전달된 루트가 첫 번째 노드가 되도록 보장하는 메서드를 호출하여 이 문제를 해결합니다.
moveRootToFront(tab, root)
여기에서 레드-블랙 트리가 어떻게 구축되고 자체 균형을 맞추는지 명확하게 볼 수 있습니다. 시각화
-
- 키의 해시 코드를 계산합니다.
- 버킷 인덱스를 계산합니다.
- 인덱스를 살펴보고 첫 번째 요소의 키를 기존 값과 비교합니다. 동일하면 값을 반환하고, 그렇지 않으면 다음 요소가 있는지 확인합니다.
- 다음 Node 객체가 null이면 null을 반환합니다.
- 다음 Node 개체가 null이 아니면 해당 개체로 이동하여 요소를 찾거나 다음 Node 개체가 null이 될 때까지 처음 세 단계를 반복합니다.
get()
"KING" 키의 값을 얻습니다.
map.get("KING");
내부에서는 키 자체("KING")와 해당 해시(2306996)가 전달되는 메소드가 호출되며 getNode(int hash, Object key)
, 이는 작업 중과 동일한 방식으로 사전 계산됩니다 put()
.
-
우리는 다음을 확인합니다:
-
해시 테이블이 존재합니까?
(tab = table) != null
HashMap을 생성할 때 테이블의 배열은 생성자에서 생성되지 않으며, 이는
resize()
해시 테이블에 첫 번째 요소를 추가할 때 항상 호출되는 메서드에서 나중에 발생합니다. 따라서 HashMap에 요소가 추가되지 않은 경우 요소를 저장할 내부 배열이 없습니다. -
이전 표현식이 true를 반환하는 경우 내부 배열의 길이가 0보다 큰지 확인해야 합니다.
(n = tab.length) > 0;
-
이전 표현식도 true를 반환하는 경우 이전에 계산된 인덱스(이 경우 4)의 버킷으로 이동하여 요소가 있는지 확인합니다.
(first = tab[(n - 1) & hash]) != null)
-
우리가 찾고 있는 키를 버킷 내부 목록의 첫 번째 요소의 키와 비교합니다. 왜냐하면 대부분의 버킷에는 (충돌이 있는 모든 곳은 아니지만) 단 하나의 요소(우리의 경우)만 있기 때문입니다. 메소드의 경우와 마찬가지로
put()
해시를 비교하고 일치하면 키를 참조로 비교한 다음 으로만 비교합니다equals()
.if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k))))
우리의 경우 "KING" 키가 "BLAKE" 키보다 앞에 있으므로(연결된 목록 내에서는 새 요소가 끝에 추가되고 KING이 먼저 추가됨) 이 지점에서 멈추고 첫 번째 개체를 반환합니다. get() 메서드에 Node를 입력하면 값이 (100)인 필드를 "낚아챌" 수 있습니다.
return (e = getNode(hash(key), key)) == null ? null : e.value;
-
-
버킷 내부에 두 개 이상의 요소가 있는 경우 다음을 수행합니다.
-
do – while
버킷이 연결된 목록인 경우 일치하는 항목을 찾을 때까지 루프의 각 요소를 통해 목록을 살펴봅니다 .do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null);
-
버킷이 트리 구조인 경우 메소드가 추가로 호출되고
getTreeNode()
, 이 메소드는 필요한 키를 찾기 위해 해당 메소드를 사용합니다find()
. 우리는 트리 검색을 수행합니다. 해시를 비교하고 검색할 왼쪽 또는 오른쪽 루트 노드를 결정합니다. 키가 동일하면(참조 또는equals
) 이 노드를 반환합니다. 왼쪽 또는 오른쪽 하위 노드가 null인 경우 CompareTo를 사용하여 추가로 키를 비교합니다(키가 인터페이스를 구현하는 경우Comparable
). 그렇지 않으면 일치 항목을 찾을 때까지 트리(오른쪽 또는 왼쪽 하위 트리)를 통해 재귀 검색을 수행합니다.
-
-
원하는 버킷으로 이동합니다(다시 말하지만 사전 계산됨).
-
버킷에 개체가 하나만 있는 경우(널 포인터 확인) 해시, 링크를 비교하고
equals
(갑자기 해시가 동일하지 않은 경우) 일치하는 항목을 찾았나요? 좋습니다. 이것이 우리의 키입니다. 이를 삭제하고(=null) 이 키의 값을 반환합니다. -
버킷에 요소가 두 개 이상 있으면 해당 요소를 찾거나 목록 끝에 도달할 때까지 루프의 각 요소를 확인합니다.
-
요소를 찾을 수 없으면 null을 반환합니다.
GO TO FULL VERSION