static int indexFor(int h, int length) {
return h & (length-1);
}
Como entrada, tomou-se o código hash obtido como resultado do trabalho hashCode()
e o comprimento do array interno (número de células). E retornou o resultado “código hash” -> bit a bit “AND” -> (comprimento do array – 1). A classe HashMap
herda da classe AbstractMap
e implementa as seguintes interfaces: Map
, Cloneable
, Serializable
. O .method é responsável pela função hash em Java hashCode()
. A implementação padrão hashCode()
retorna um valor chamado código hash de identidade . Mesmo que uma classe seja substituída hashCode()
, você sempre poderá obter o hash de ID do objeto chamando System.identityHashCode(Object o)
. A implementação padrão hashCode()
no OpenJDK não tem nada a ver com o endereço de memória, como às vezes se acredita. Mais detalhes aqui: habr No HashMap , a tabela hash é implementada com base em um array (ou, mais precisamente, dinâmico, já que a tabela pode se expandir) de listas vinculadas individualmente. Essencialmente, obtemos o código hash da chave como resultado do método hashCode()
, que é então modificado (como consideraremos mais tarde) e internamente, usando um método adicional, os valores resultantes são distribuídos para as células necessárias. Os elementos da matriz (células) também são chamados de buckets , que são usados para armazenar nós individuais. Cada um dos buckets é uma coleção (lista ou árvore). Um nó é um objeto de uma classe aninhada Node
(ou TreeNode
em uma estrutura em árvore). Na verdade, dentro da célula da matriz existe LinkedList
apenas uma lista vinculada individualmente, ou uma árvore vermelha e preta, que fundamenta a implementação de outra classe - TreeMap
.
HashMap
da qual possui os seguintes campos:
final int hash
— o hash do elemento atual, que obtemos como resultado do hash da chave;final K key
— a chave do elemento atual. É aqui que o que você especifica como o primeiro objeto no método é gravadoput()
;V value
— o valor do elemento atual. E o que você especifica como segundo objeto no método está escrito aquiput()
;Node < K, V> next
— link para o próximo nó dentro da mesma cesta. A lista está conectada, portanto não precisa de um link para o próximo nó, se houver.
transient Node < K, V> [] table
– a própria tabela hash, implementada com base em um array, para armazenar pares de valores-chave na forma de nós. É aqui que nossos Nodes são armazenados;transient int size
— número de pares de valores-chave;int threshold
— o número máximo de elementos, ao atingir o qual o tamanho da tabela hash dobra. Calculado usando a fórmula (capacidade * loadFactor);final float loadFactor
— este parâmetro determina em que grau de carga a tabela hash atual precisa para criar uma nova tabela hash, ou seja, assim que a tabela hash estiver 75% cheia, uma nova tabela hash será criada e os elementos atuais serão movidos para ela (uma operação dispendiosa, uma vez que todos os elementos devem ser refeitos);transient Set< Map.Entry< K,V>> entrySet
- contém um arquivo em cacheentrySet()
, com o qual podemos iterarHashMap
.
static final int DEFAULT_INITIAL_CAPACITY= 1 << 4
— capacidade padrão da tabela hash (16);static final int MAXIMUM_CAPACITY = 1 << 30
— a capacidade máxima possível da tabela hash (aproximadamente mil milhões);static final float DEFAULT_LOAD_FACTOR = 0.75f
— fator de carga padrão;static final int TREEIFY_THRESHOLD = 8
é o “limiar” do número de elementos em um balde, ao atingir o qual a lista vinculada interna será convertida em uma estrutura em árvore (árvore vermelha e preta).static final int UNTREEIFY_THRESHOLD = 6
— se o número de elementos em uma cesta diminuir para 6, ocorrerá uma transição reversa de uma árvore para uma lista vinculada;static final int MIN_TREEIFY_CAPACITY = 64
— a capacidade mínima (número de buckets) de uma tabela hash na qual é possível uma transição para uma estrutura em árvore. Aqueles. Se houver pelo menos 64 buckets na tabela hash e houver 8 ou mais elementos em um bucket, ocorrerá uma transição para uma estrutura em árvore.
public HashMap()
— cria uma exibição hash por padrão: por volume(capacity) = 16
e com fator de carga(load factor) = 0.75
;public HashMap(Map< ? extends K, ? extends V> m)
— cria um mapeamento hash inicializado pelos elementos de outro mapeamento com capacidade inicial suficiente para acomodar os elementos de outro mapeamento;public HashMap(int initialCapacity)
— cria um mapa hash com uma determinada capacidade inicial. Para que o HashMap funcione correta e corretamente, o tamanho do array interno deve ser uma potência de dois (ou seja, 16, 64, 128, etc.);public HashMap(int initialCapacity, float loadFactor)
— cria um mapa hash com os parâmetros especificados: capacidade inicial e fator de carga.
Map
e estende uma classe AbstractMap
sem adicionar seus próprios métodos. Um mapa hash não garante a ordem de seus elementos. Portanto, a ordem em que os elementos são inseridos no mapa hash não é necessariamente a ordem em que são recuperados pelo iterador. Adicionando objetos A adição de um par chave-valor é feita usando o arquivo put()
. Vejamos as etapas envolvidas ao adicionar um objeto:
-
O valor hash da chave do objeto inserido é calculado. O hash da chave é calculado por um método
static final int hash(Object key)
que já acessa ohashCode()
método da chave que conhecemos. Para fazer isso, é usado um método substituídohashCode()
ou sua implementação padrão. Transformações adicionais no métodohash()
: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
, которая в дальнейшем используется для вычисления бакета. -
Ao mesmo tempo, calculamos o índice do bucket onde nosso objeto será colocado e verificamos se já existem elementos nele. Calculamos:
i = (n - 1) & hash i = (16 - 1) & 2306996 i = 4
verificar:
if ((p = tab[i = (n - 1) & hash]) == null)
-
Como como resultado da verificação obtemos true (não há elementos no bucket), será gerado um objeto Node com os seguintes campos:
{ int hash = 2306996 — сгенерированный хеш-code String key = {"KING"} — ключ Integer value = 100 — meaning Node next = null — link на следующий узел }
Nosso objeto Node gerado será adicionado ao bucket no índice [4]:
tab[i] = newNode(hash, key, value, null); tab[4] = newNode(2306996, “KING”, 100, null);
newNode()
é um método que simplesmente retorna um objeto da classe Node. -
Após a adição, será feita uma verificação para ver se o número atual de elementos excede o parâmetro
threshold
:if (++size > threshold) resize();
Se exceder, um método será chamado
resize()
para aumentar o tamanho da tabela hash.Neste ponto, o método
putVal()
(e, respectivamenteput()
) completará seu trabalho.O resultado pode ser representado graficamente da seguinte forma:
Em geral, é assim que se parece adicionar um Node (par chave-valor) a uma tabela hash se o bucket desejado estiver vazio . Agora vamos tentar adicionar um elemento que levará a uma colisão (quando houver mais de um elemento em um balde).
-
- encadeamento externo ou método de encadeamento (implementado no HashMap) - ou seja, a célula na verdade contém uma lista (cadeia). E a lista já pode conter vários valores (não necessariamente com o mesmo código hash).
- método de sondagem linear ou endereçamento aberto (implementado em IdentityHashMap) - consiste em procurar a primeira célula vazia após aquela apontada pela função hash;
-
Usando o método,
put()
adicionaremos outro par de valores-chave ao mapeamento hash:map.put("BLAKE", 10);
-
Calculamos o “hash preliminar” – 63281361. Nós o modificamos e como resultado obtemos o código hash final – 63281940.
-
Como a primeira verificação de “vazio” agora retornará falso (não há necessidade de criar uma tabela), calculamos imediatamente o índice do bucket onde nosso objeto será colocado:
i = (n - 1) & hash i = (16 - 1) & 63281940 i = 4
-
O balde no índice especificado é verificado quanto à presença de elementos nele e, como a condição
if ((p = tab[i = (n - 1) & hash]) == null)
neste caso não é atendida (já existe um elemento no balde), então vamos para o blocoelse
. -
Primeiro de tudo, comparamos o elemento que está sendo adicionado com o primeiro elemento da lista vinculada dentro do bucket:
(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)
Ao invés de ir para a estrutura em árvore, será chamado um método
resize()
para aumentar o tamanho da tabela hash, redistribuindo os elementos.treeify()
por sua vez, a lista vinculada deTreeNode
se converte em uma árvore rubro-negra. O métodoresize()
itera por todos os elementos do armazenamento atual, recalcula seus índices (levando em consideração o novo tamanho) e redistribui os elementos em um novo array.Resumidamente, sem entrar em detalhes da estrutura da árvore rubro-negra, acontece o seguinte:
-
O primeiro elemento da lista é escrito como a raiz de toda a árvore (preto):
if (root == null) { x.parent = null; x.red = false; root = x; }
-
Para outros elementos:
distribua-os para a esquerda ou direita dependendo do valor do hash:
if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1;
Todos os filhos da esquerda devem ser menores (ou iguais) ao seu nó raiz, enquanto todos os filhos da direita devem ser maiores.
-
Se dois elementos possuem os mesmos hashes e não podem ser comparados de nenhuma outra forma (eles não implementam a interface
Comparable
), interrompemos a construção da árvore e chamamos o métodotieBreakOrder()
, que por sua vez utiliza o método nativoSystem.identityHashCode()
para calcular um código hash globalmente único .Mais detalhes aqui: link para o artigo
-
Verificamos os elementos da árvore (objetos
TreeNode
) até que um elemento zero filho (esquerdo ou direito) seja encontrado.if ((p = (dir <= 0) ? p.left : p.right) == null)
-
Adicione um nó filho (esquerdo ou direito dependendo do diretório):
x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x;
-
Como adicionar um novo elemento pode perturbar o equilíbrio da árvore, chamamos o método de reequilíbrio:
root = balanceInsertion(root, x);
Você pode ler sobre como equilibrar o CCH aqui: habr
-
Após o balanceamento, o elemento raiz pode mudar. Corrigimos isso chamando um método que garante que a raiz passada para ele será o primeiro nó:
moveRootToFront(tab, root)
Você pode ver claramente como uma árvore rubro-negra é construída e se autoequilibra aqui: visualização
-
- Calcule o código hash da chave.
- Calcule o índice do balde.
- Percorra o índice e compare a chave do primeiro elemento com o valor existente. Se forem iguais, retorne o valor, caso contrário, verifique o próximo elemento, se existir.
- Se o próximo objeto Node for nulo, retorne nulo.
- Se o próximo objeto Node não for nulo, vá até ele e repita as três primeiras etapas até que o elemento seja encontrado ou o próximo objeto Node seja nulo.
get()
obtemos o valor da chave “KING”:
map.get("KING");
Dentro é chamado o método getNode(int hash, Object key)
, para o qual são passadas a própria chave (“KING”) e seu hash (2306996), que é pré-calculado da mesma forma que durante a operação put()
.
-
Nós verificamos:
-
existe uma tabela hash:
(tab = table) != null
Deixe-me lembrar que ao criar um HashMap, um array para a tabela não é criado no construtor, isso acontece posteriormente no método
resize()
, que é sempre chamado ao adicionar o primeiro elemento à tabela hash. Portanto, se nenhum elemento tiver sido adicionado ao HashMap, simplesmente não haverá array interno para armazenar os elementos. -
se a expressão anterior retornar verdadeiro, você precisa ter certeza de que o comprimento da matriz interna é maior que 0:
(n = tab.length) > 0;
-
se a expressão anterior também retornar verdadeiro, vá até o balde do índice (no nosso caso 4), que foi calculado anteriormente, e verifique a presença de elementos:
(first = tab[(n - 1) & hash]) != null)
-
Comparamos a chave que procuramos com a chave do primeiro elemento da lista dentro do bucket, já que na maioria dos buckets haverá (nem em todos os lugares onde temos colisões) apenas um elemento (nosso caso). Assim como no caso do método
put()
, os hashes são comparados e, caso correspondam, as chaves são comparadas por referência, e só então porequals()
.if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k))))
Como no nosso caso a chave “KING” precederá a chave “BLAKE” (dentro de uma lista encadeada, novos elementos são adicionados ao final, e KING foi adicionado primeiro), pararemos neste ponto e retornaremos o primeiro objeto de type Node ao método get(), que “arranca” dele um campo com o valor (100):
return (e = getNode(hash(key), key)) == null ? null : e.value;
-
-
Se houver mais de um elemento dentro do bucket, então:
-
se o bucket for uma lista vinculada, percorremos a lista por cada um dos elementos em um loop
do – while
até que uma correspondência seja encontrada:do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null);
-
se o intervalo for uma estrutura de árvore, o método será chamado adicionalmente
getTreeNode()
, que por sua vez usa o método para encontrar a chave necessáriafind()
. Realizamos uma pesquisa em árvore - os hashes são comparados e o nó raiz esquerdo ou direito a ser pesquisado é determinado. Se as chaves forem iguais (por referência ou porequals
), retorne este nó. Se os nós filhos esquerdo ou direito forem nulos, comparamos adicionalmente as chaves usando compareTo (se as chaves implementarem a interfaceComparable
), caso contrário, realizamos uma pesquisa recursiva na árvore (subárvore direita ou esquerda) até que uma correspondência seja encontrada.
-
-
vá até o balde desejado (novamente, é pré-calculado);
-
se houver apenas um objeto no bucket (verificamos seu ponteiro nulo), comparamos hashes, links e
equals
(se de repente os hashes não forem iguais). Encontrou uma correspondência? Ótimo, esta é a nossa chave - exclua-a (=null) e retorne o valor desta chave. -
se houver mais de um elemento no balde, verificamos cada elemento em um loop até encontrar o elemento ou chegar ao final da lista.
-
se o elemento não foi encontrado, retornamos nulo.
GO TO FULL VERSION