História Extra sobre o Crescimento de Programadores – Introdução ao Hashmap

Recentemente, examinei o código-fonte relevante do hashmap na Internet e descobri que o conhecimento básico foi introduzido, mas alguns lugares ainda não são completos o suficiente, então pretendo compartilhar meu entendimento com você.

1. Introdução aos conhecimentos básicos

Todo mundo sabe que HashMap é uma estrutura de dados comum em java e tem uma ampla gama de usos. Por exemplo, pode ser usado como contêiner para passagem de parâmetros e também pode ser usado para gerenciamento de beans na primavera. Ele herda a classe AbstractMap e é descendente de map, portanto, muitas vezes podemos usar upcasting para criar instâncias.
como:

Map<Type1, Type2> map = new HashMap<>();

Além disso, os parâmetros também podem ser passados ​​​​entre colchetes. O significado deste parâmetro é o initCapacity (capacidade inicial) do hashmap, que geralmente é elevado à potência de 2 (por que esse valor é considerado será discutido mais tarde), e o o padrão é 16.
como:

Map<Type1, Type2> map = new HashMap<>(16);

ps: A capacidade aqui também pode ser preenchida com outros números, como 10, 14, etc., mas será automaticamente convertida para uma potência de 2 não inferior ao número durante a inicialização. Como fazer isso será explicado mais tarde.

Se você quiser armazenar dados nele posteriormente, basta escrever assim:

map.put(key1,value1);
map.put(key2,value2);

Para buscar dados, escreva isto:

map.get(key1);   //value1
map.get(key2);	 //value2

Além disso, o elemento de entrada no mapa suporta travessia cíclica, o código é o seguinte:

for(Map.Entry<Type1,Type2> entry: map.entrySet()) {
    
    
	String key = entry.getKey();
	String value = entry.getValue();
	// 后续操作
	...
}

Também pode ser percorrido da seguinte maneira, o código é o seguinte

for(Type1 key: map.keySet()) {
    
    
	String value = map.get(key);
	// 后续操作
	...
}

O mapa também pode ser percorrido através de iteradores, o código é o seguinte:

Iterator it = map.entrySet().iterator();
while(it.hasNext()) {
    
    
	Map.Entry<Type1,Type2> entry = it.next();
	//对entry进行处理
	String key = entry.getKey();
	String value = entry.getValue();
	...
}

map pode remover elementos:

map.remove(key1);

Mas lembre-se de não usar o método put ou remove para modificar o nó nos dois loops acima, caso contrário, um erro java.util.ConcurrentModificationException será relatado, porque no código-fonte, há uma variável expectModCount no iterador da entrada em no mapa, e no mapa Há também uma variável chamada modCount. Tome o método remove como exemplo. Quando o método remove no mapa é chamado, o modCount no mapa será acumulado, mas o esperadoModCount no iterador não foi alterado. Isso é implementado no iterador EntryIterator para obter o próximo. Um erro será relatado durante a operação do nó, o código é o seguinte:

//1.该函数出现在HashMap的HashIterator这个抽象类中
class HashMap<K.V> extends AbstractMap<K.V> implements Map<K,V>, Cloneable, Serializable {
    
    
	...
	transient int modCount;//HashMap结构改变的次数
	...
	abstract class HashIterator {
    
    
		Node<K,V> next;
	    Node<K,V> current;
	   	int expectedModCount //迭代器改变的次数
	    int index; 
		...
		final HashMap.Node<K,V> nextNode() {
    
    
			HashNode node2 = this.next;
			//关键代码
			if(hashMap.this.modCount != this.expectedModCount) {
    
    
				throw new ConcurrentModificationException();
			}
			...
		}
		...
	}
	//2.而在map中则通过内部EntryIterator类实现了这个抽象类
	final class EntryIterator extends HashIterator
		implements Iterator<Map.Entry<K,V>> {
    
    
		public final Map.Entry<K,V> next() {
    
     return nextNode(); }
	}
	//3.在Map.Entry中创建EntryIterator实例
	final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    
    
		public final int size() {
    
     
			return size;
		}
		public final void clear() {
    
     
			HashMap.this.clear();
		}
		public final Iterator<Map.Entry<K,V>> iterator() {
    
    
			return new EntryIterator();
		}
		...
	}
}	

No entanto, se o método remove no iterador for usado, o esperadoModCount no iterador e o modCount no mapa serão atualizados automaticamente de forma síncrona, evitando assim o relatório de erros.

map também pode substituir elementos
map.replace (chave Type1, Type2 oldValue, Type2 newValue)

2. Introdução aos Princípios de Armazenamento

A estrutura básica do hashmap é mostrada na figura:
insira a descrição da imagem aqui
sua estrutura de dados de nível inferior é uma matriz de listas vinculadas, que é uma matriz composta por listas vinculadas, ou seja, tabela Node<Type1, Type2>[]. Node é uma classe que implementa a interface Map.Entry (era chamada de Entry antes de 1.8, e era chamada de Node depois de 1.8, e o nome foi alterado), que é usada para armazenar pares de valores-chave. Seu código de composição básico é o seguinte:

static class Node<K,V> implements Map.Entry<K,V> {
    
    
	final int hash; //hash值用于确定下标位置
	final K key;
	final V value;
	final Node<K,V> next; //链表下一个元素
	// 注意这里是package的访问修饰符,也就是说外部无法通过
	//Map.Node的形式获取该元素,而是要通过自带的static函数
	// map.entrySet()来获取
	Node(int hash, K key, V value, Node<K,V> next) {
    
    
		this.hash = hash;
		this.key = key;
		this.value = value;
		this.next = next;
	}
	public final K getKey() {
    
    
		return key;
	}
	public final V getValue() {
    
    
		return value;
	}
	public final String toString() {
    
    
		return key + "=" + value;
	}
	public final int hashCode(){
    
    
		return Objects.hashCode(key) ^ Objects.hashCode(value);
	}
}

Amigos cuidadosos devem ter descoberto que o método de construção aqui é pacote, que só pode ser acessado no mesmo pacote, então o exterior não pode obter este elemento através do formulário Map.Node, mas através do método embutido map.entrySet() obter.

Portanto, a questão é: como sabemos que a camada inferior do hashmap é uma matriz de lista vinculada, como ela armazena dados? Como é calculado o valor do hash no nó e qual a sua utilidade?

A comida deve ser comida pedaço por pedaço e os problemas devem ser resolvidos um por um. Primeiro, observe o processo de armazenamento de dados: agora queremos armazenar alguns dados nele: {"nome": "zhangsan", "idade": "16", "sexo", "masculino"}, obviamente isso é uma peça de dados de informações pessoais, também podemos armazená-los no hashmap na forma de pares de valores-chave.

  • Etapa 1: chame o método put(key,value)
  • Etapa 2: No método put(key,value), calcularemos o valor hash deste par chave-valor, e o método de cálculo é o seguinte:
//JDK1.7
static int hash(Object key){
    
    
	int h = hashSeed;
	if (0 != h && k instanceof String) {
    
    
		return sun.misc.Hashing.stringHash32((String) k);
	}
	h ^= k.hashCode();
	h ^= (h >>> 20) ^ (h >>> 12);
	return h ^ (h >>> 7) ^ (h >>> 4);
}
//JDK1.8
static final int hash(Object key) {
    
    
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

Tomando JDK1.8 como exemplo, a operação de deslocamento para a direita não assinado é usada aqui (o bit superior é preenchido com 0 e o bit inferior é deslocado para a direita), e os 16 bits superiores e o valor hash da própria chave são XORed (a operação & ou | não é usada porque as duas probabilidades da operação são tendenciosas para 0 ou 1 e a distribuição não é suficientemente uniforme). A vantagem disso é que o bit baixo mantém as características do bit alto, reduzindo a possibilidade de duplicação no bit baixo, tornando o valor do hash o mais uniforme possível e reduzindo a possibilidade de conflitos repetidos na determinação da posição do primário. chave mais tarde . A função hash usada aqui é uma função hash, que permite que entradas de comprimentos diferentes obtenham resultados do mesmo comprimento . Mas a forma de implementar a função hashCode específica pode exigir a ajuda de pequenos parceiros.

  • Etapa 3: Determine o local de armazenamento do par chave-valor no hashmap de acordo com o valor do hash. Isso é índice. Em relação a este ponto,
    existe uma função chamada indexFor(int h, int length) no JDK1.7, (JDK1.8 foi ajustado mas o princípio é o mesmo), o código fonte é o seguinte
static int indexFor(int h, int length) {
    
    
	return h & (length-1);
}

Deixe-me explicar aqui, h - indica o resultado do cálculo da função hash, comprimento indica o comprimento da matriz hashmap, mas por que o comprimento deveria ser reduzido em 1?
Antes de explicar este ponto, vou compensar as armadilhas anteriores. Deixe-me primeiro falar sobre por que o comprimento padrão da matriz é uma potência de 2 (o padrão é 16): a característica de um comprimento de matriz de uma potência de 2
é que é reduzido em 1. Cada bit após o bit mais alto do valor é 1, e os dados dos bits mais baixos do hash obtidos após realizar uma operação AND (&) com qualquer número são o resultado.

Quais são os benefícios de fazer isso? A vantagem é que a operação de bits é rápida , e a operação AND (não operação OR ou XOR) é realizada entre o valor hash e o número binário com 1 bit, o que reduz a possibilidade de conflito e aumenta a uniformidade de distribuição .
Por exemplo, faça a operação AND: pegue 1101 (hash) e 0111 (comprimento 1 é 8-1) e faça a operação AND para obter 0101 (5) Pegue 1111 (
hash) e 0111 (comprimento 1 é 8-1) para Operação AND obtém 0111 (7) Mas se você fizer a operação OR: pegue 1101 (hash) e
0111 (comprimento-1 é 8-1) Faça a operação OR para obter 0111 (7) Pegue 1111 (hash) e 0111 (comprimento-1 é 8-1)
e o resultado é 0111 (7) Isso entra em conflito.

Resumindo, o processo de armazenamento de hash é o seguinte: put(key,value) -> hash(key) -> indexFor(hash) -> index

Aí vem a pergunta novamente, essa distribuição uniforme é impossível de evitar a possibilidade de conflitos repetidos, como lidar com o hashmap neste caso?

Por que deveria ser armazenado posteriormente na forma de uma árvore rubro-negra? Por que você escolheu um valor de corte de 8?
Aprendemos a estrutura de dados e devemos saber que o comprimento de pesquisa da árvore rubro-negra é O (logn) e a lista vinculada é O (n/2).Quando o comprimento é <8, a diferença entre logn e n
/ 2 não é grande e é necessário gerar uma árvore rubro-negra. O overhead adicional é, portanto, armazenado na forma de uma lista vinculada, mas quando o comprimento é maior que 8 (16, 32, 64... porque a expansão do hashmap é expandido por um fator de 2), a diferença entre logn e n/2 se tornará maior, o que significa que às vezes as árvores rubro-negras são sem dúvida melhores.

3. Introdução ao princípio da expansão

– 2022.1.21 Estou de volta depois de mais de meio mês de trabalho -----------------
A expansão do hashmap na verdade não é complicada se for complicada, e não é simples se for simples. Sem mais delongas, vamos direto ao assunto. Como implementar o mecanismo de expansão do hashmap?
Primeiro, vamos dar uma olhada em um trecho do código-fonte:

	/** 
	 * tableSizeFor 是为了实现hashmap扩容而建立的函数,用于
	 * 对数组进行不小于cap的2的幂次方扩容
	 */
    static final int tableSizeFor(int cap) {
    
    
        int n = cap - 1; 
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

Ok, vamos analisar o código-fonte agora. O limite aqui obviamente se refere ao comprimento do array no hashmap a ser alocado. Mas o que significa a seguinte operação AND e deslocamento para a direita sem sinal? Por que o limite deve ser reduzido primeiro em um e depois adicionado em um (se não exceder o comprimento máximo)?

Primeiro definimos cap para qualquer número, como 31. (Um número primo relativamente clássico)
Então 31 - 1 = 30, convertido em binário é
00000000 00000000 00000000 00011110 (int é 4 bits ou 32 bits)
Podemos ver que o bit mais alto que não é 0 é o quinto bit (da direita para a esquerda )
Então n >>> 1 Sim (deslocamento sem sinal para a direita em um bit, ou seja, todos mudam para a direita em um bit e 0 é adicionado à esquerda
)
Bit, neste momento, se a operação OR , o quarto e o quinto dígitos devem ser 1. (Enquanto houver um número em uma posição que seja 1, o resultado será 1), o resultado de n |= n >>> 1 é o seguinte: 00000000 00000000 00000000 00011111 Depois disso, o resultado acima é sem sinal deslocado para
a
direita por dois bits novamente para obter
00000000 00000000 00000000 00000111
O 1 original no quarto e quinto dígitos é movido para o segundo e terceiro dígitos e então a operação OR é executada, ou seja, o resultado de n |= n >>> 2 é:
00000000 00000000 00000000 00011111
Os resultados após o mesmo raciocínio podem ser provados.
Pode-se observar que após o código acima, olhando da direita para a esquerda, a parte inferior é toda 1, e a parte superior é toda 0, mais 1 é a potência de 2. Este algoritmo não é maravilhoso!

Então, qual é o propósito deste cálculo? Onde está a engenhosidade deste algoritmo?
O objetivo deste algoritmo é converter um número que não seja uma potência de 2 em um número que não seja menor que uma
potência de 2 do número. A combinação engenhosa de operação e deslocamento à direita sem sinal reduz a quantidade de operação, e o resultado pode ser obtido executando apenas 5 vezes. Incrível!

E por que o limite precisa ser subtraído primeiro por um e depois adicionado por um (se não exceder o comprimento máximo)? Vamos dar outro
exemplo, vamos considerar o limite como 32. Se você não subtrair um primeiro,
32 é convertido para binário como
00000000 00000000 00000000 Depois de 00100000
n |= n >>> 1, há
00000000 00000000 00000000 00110000
e então n |= n >>> 2:
00000000 00000000 000 00000 00111100
e assim por diante
...
Finalmente + 1 vai obtenha 64 em vez de 32, ou seja,
0 0000000 00000000 00000000 01000000 ,
então esse número não é menor que 32 elevado a 2 (deve ser 32)

Com o conhecimento básico acima, podemos agora falar sobre o princípio da expansão da capacidade:
Obrigado novamente ao chefe da lkforce pelo compartilhamento, o link é o seguinte:
https://blog.csdn.net/lkforce/article/details/89521318Primeiro
de all Dê uma olhada no código fonte:

	// jdk1.7
    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);
    }
    /**
     * 从原hashmap数组中迁移数据到新数组中.
     */
    void transfer(Entry[] newTable, boolean rehash) {
    
    
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
    
    
            while(null != e) {
    
    
                Entry<K,V> next = e.next;  //step1  
                if (rehash) {
    
     // 重新计算hash值
                    e.hash = null == e.key ? 0 : hash(e.key);
                } 
                int i = indexFor(e.hash, newCapacity);  //step2 
                e.next = newTable[i]; // step3        
                newTable[i] = e;  //断点                     
                e = next;                             
            }
        }
    }

Para jdk1.7, a expansão segue principalmente as seguintes etapas:

  1. Alocar novo espaço de memória
  2. copiar dados
  3. O limite de atualização (limite) geralmente é o fator de carga * nova capacidade do array

Aqui falarei principalmente sobre a função de transferência, que usa uma lista vinculada para consultar do início ao fim e usa o método de inserção head para inserir.

  1. Primeiro, na etapa 1, salve o nó sucessor do nó atual no próximo.
  2. Na etapa 2, a posição do índice é obtida novamente de acordo com o valor do hash atual. (Conforme mencionado anteriormente na função indexFor, sua posição é avaliada de acordo com os n bits inferiores)
  3. Na etapa 3, defina o nó sucessor do nó atual como newTable[i] para facilitar a reorganização da lista vinculada, ou seja, aponte o nó sucessor do nó atual para a i-ésima posição na newTable no novo variedade.
  4. A etapa 4 é substituir o valor de newTable[i] pelo nó atual,
  5. A etapa 5 é continuar procurando o próximo nó na matriz original.
    Amigos que se sentem confusos aqui podem conferir o link abaixo. https://blog.csdn.net/lkforce/article/details/89521318

Nota: Quando este método é usado em multithreading, haverá links mortos (listas vinculadas circulares) e perda de dados. Para obter detalhes, consulte:
https://blog.csdn.net/XiaoHanZuoFengZhou/article/details/105238992

Expansão jdk1.8:

    final Node<K,V>[] resize() {
    
    
        Node<K,V>[] oldTab = table;//首次初始化后table为Null
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;//默认构造器的情况下为0
        int newCap, newThr = 0;
        if (oldCap > 0) {
    
    //table扩容过
             //当前table容量大于最大值得时候返回当前table
             if (oldCap >= MAXIMUM_CAPACITY) {
    
    
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
            //table的容量乘以2,threshold的值也乘以2           
            newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
        //使用带有初始容量的构造器时,table容量为初始化得到的threshold
        newCap = oldThr;
        else {
    
      //默认构造器下进行扩容  
             // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
    
    
        //使用带有初始容量的构造器在此处进行扩容
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({
    
    "rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
    
    
            for (int j = 0; j < oldCap; ++j) {
    
    
                HashMap.Node<K,V> e;
                if ((e = oldTab[j]) != null) {
    
    
                    // help gc
                    oldTab[j] = null;
                    if (e.next == null)
                        // 当前index没有发生hash冲突,直接对2取模,即移位运算hash &(2^n -1)
                        // 扩容都是按照2的幂次方扩容,因此newCap = 2^n
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof HashMap.TreeNode)
                        // 当前index对应的节点为红黑树,这里篇幅比较长且需要了解其数据结构跟算法,因此不进行详解,当树的个数小于等于UNTREEIFY_THRESHOLD则转成链表
                        ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else {
    
     // preserve order
                        // 把当前index对应的链表分成两个链表,减少扩容的迁移量
                        HashMap.Node<K,V> loHead = null, loTail = null;
                        HashMap.Node<K,V> hiHead = null, hiTail = null;
                        HashMap.Node<K,V> next;
                        do {
    
    
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
    
    
                                // 扩容后不需要移动的链表
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
    
    
                                // 扩容后需要移动的链表
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
    
    
                            // help gc
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
    
    
                            // help gc
                            hiTail.next = null;
                            // 扩容长度为当前index位置+旧的容量
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

Acho que você gosta

Origin blog.csdn.net/qq_31236027/article/details/122285586
Recomendado
Clasificación