O HashMap é a estrutura de dados de mapeamento (par chave-valor) mais utilizada por desenvolvedores Java. Com a evolução do JDK, particularmente no JDK 1.8, a implementação subjacente do HashMap foi otimizada, introduzindo estruturas como árvores vermelho-preto e aprimoramentos no processo de redimensionamento.
Estrutura Interna e Campos
Internamente, o HashMap combina um array de nós, listas encadeadas e, a partir do JDK 1.8, árvores vermelho-preto. O array subjacente é denominado Node[] table. Cada nó dentro deste array representa um par chave-valor e pode apontar para os seguintes nós, formando uma lista encadeada.
static class NodeMap<K, V> {
final int chaveHash;
final K chave;
V valor;
NodeMap<K, V> proximo;
NodeMap(int chaveHash, K chave, V valor, NodeMap<K, V> proximo) {
this.chaveHash = chaveHash;
this.chave = chave;
this.valor = valor;
this.proximo = proximo;
}
// Métodos como hashCode(), equals(), getValue(), setValue() omitidos para brevidade.
}
A escolha de endereçamento encadeado para resolução de colisões é a técnica empregada. Ao calcular o hash de uma chave, o índice correspondente no array é determinado. Se múltiplas chaves resultarem no mesmo índice, elas formam uma lista encadeada naquele local. A eficiência depende da uniformidade do algoritmo de hash e do tamanho do array.
Campos Importantes
Diversos campos controlam o comportamento do HashMap:
limite: O número máximo de pares chave-valor que o mapa pode conter antes de acionar um redimensionamento. É calculado comotamanho_do_array * fator_de_carga.fatorDeCarga: Um valor (padrão 0.75) que determina quando o array deve ser expandido.tamanhoAtual: O número real de pares chave-valor armazenados.modificações: Contador para falhas rápidas em iterações concorrentes.
Uma característica importante do JDK 1.8 é a conversão de listas encadeadas longas (acima de 8 nós) em árvores vermelho-preto, otimizando operações como busca e inserção em casos de alta colisão.
Implementação de Métodos
Cálculo do Índice do Bucket
A localização eficiente no array é crucial. O processo envolve obter o hashCode da chave, aplicar uma operação de "mixing" nos bits altos e baixos, e então realizar uma operação de módulo usando um deslocamento de bits.
static int calculaHash(int codigoHash) {
return codigoHash ^ (codigoHash >>> 16);
}
int obterIndice(int hash, int comprimentoArray) {
return hash & (comprimentoArray - 1);
}
Inserção (Método inserir)
O fluxo do método de inserção no JDK 1.8 segue uma sequência lógica:
- Verifica se o array está vazio ou nulo e, se necessário, realiza o redimensionamento.
- Calcula o índice de inserção.
- Se a posição estiver vazia, insere um novo nó.
- Se a posição contiver um nó, verifica se a chave já existe (substituindo o valor, se coincidir).
- Caso contrário, verifica se a estrutura na posição é uma árvore vermelho-preto e realiza a inserção adequada.
- Se for uma lista encadeada, percorre-a. Se o comprimento exceder 8, converte a lista em uma árvore. Se a chave já for encontrada, substitui o valor.
- Após a inserção, verifica se o tamanho excedeu o limite e, em caso afirmativo, redimensiona o mapa.
public V inserir(K chave, V valor) {
// Implementação simplificada - omitindo detalhes de tratamento de nulos e resize inicial.
int hashCalculado = calculaHash(Objects.hashCode(chave));
int idx = obterIndice(hashCalculado, array.length);
NoMapa<K, V> existente = array[idx];
if (existente == null) {
array[idx] = new NoMapa<>(hashCalculado, chave, valor, null);
} else {
// Lógica complexa de travessia da lista/árvore, atualização ou inserção.
// Inclui verificação de conversão de lista para árvore.
}
// ... lógica pós-inserção e verificação de redimensionamento.
return valorAntigo;
}
Mecanismo de Redimensionamento
Quando o número de elementos excede o limite, o array subjacente é redimensionado (geralmente dobrado). O JDK 1.8 aprimorou este processo:
- Novo comprimento do array é sempre uma potência de 2.
- Durante a re-hashing dos elementos, em vez de recalcular completamente o índice, verifica-se um único bit do hash original para determinar se o elemento permanece no mesmo índice relativo ou se move para um novo índice (deslocado pelo tamanho antigo do array).
Esta otimização evita recálculos completos de hash e distribui os elementos de forma mais eficiente, mantendo a estabilidade da ordem nas listas encadeadas (diferente do JDK 1.7, onde a ordem era invertida).
void redimensionar() {
int novoComprimento = array.length * 2;
NoMapa<K, V>[] novoArray = new NoMapa[novoComprimento];
for (NoMapa<K, V> no : array) {
while (no != null) {
NoMapa<K, V> proximo = no.proximo;
// Lógica otimizada do JDK 1.8 usando o bit extra.
int novoIndice = (no.chaveHash & antigoComprimento) == 0 ? indiceAntigo : indiceAntigo + antigoComprimento;
no.proximo = novoArray[novoIndice];
novoArray[novoIndice] = no;
no = proximo;
}
}
this.array = novoArray;
this.limite = (int)(novoComprimento * fatorDeCarga);
}
Segurança em Ambientes Concorrentes
O HashMap não é seguro para uso em ambientes multithread. Modificações concorrentes podem levar a comportamentos indefinidos, incluindo perda de dados e loops infinitos durante o redimensionamento. O JDK 1.7 era particularmente suscetível a loops infinitos devido à implementação do transfer durante o resize.
Para aplicações que requerem segurança em concorrência, deve-se utilizar ConcurrentHashMap ou as versões sincronizadas de mapa fornecidas por Collections.synchronizedMap.
Comparação de Desempenho: JDK 1.8 vs JDK 1.7
Testes demonstram que o HashMap do JDK 1.8 oferece desempenho superior, especialmente em cenários de alta colisão, graças à conversão automática de listas encadeadas em árvores vermelho-preto. Em testes com distribuição uniforme de hashes, o JDK 1.8 mostrou-se até 15% mais rápido. Em cenários com hashing pobre (altíssimo número de colisões em um único bucket), a vantagem do JDK 1.8 é mais pronunciada, transformando complexidade linear (O(n)) em logarítmica (O(log n)).
Considerações Finais
- Inicialize o
HashMapcom uma capacidade estimada para minimizar redimensionamentos custosos. - O fator de carga padrão (0.75) é um equilíbrio eficiente entre tempo e espaço.
- Prefira
ConcurrentHashMapem ambientes multithread. - Aproveite as otimizações do JDK 1.8, que estendem-se muito além do
HashMap.