W3docs

Java HashMap

Utilisez HashMap, la table de hachage Java, pour des recherches clé-valeur rapides et non ordonnées.

HashMap<K, V> est l'implémentation Map par défaut dans le JDK et la structure de données la plus utilisée dans le code d'application Java. Elle sert de base à HashSet (qui est un HashMap avec toutes les valeurs définies sur un même objet factice), c'est ce que Collectors.toMap construit, et c'est la structure derrière chaque « table de correspondance » que vous écrivez sans besoin de tri ni de concurrence. Les opérations sont en O(1) attendu — un hachage, un index de seau, une ou deux vérifications equals — indépendamment de la taille.

Opérations principales

Ce sont les méthodes que vous utilisez quotidiennement. Chacune est en O(1) attendu.

MéthodeCe qu'elle fait
put(k, v)Insère ou écrase ; retourne la valeur précédente (ou null).
get(k)Retourne la valeur, ou null si la clé est absente.
getOrDefault(k, def)Comme get, mais retourne def au lieu de null en cas d'absence.
putIfAbsent(k, v)Définit la valeur uniquement si la clé est absente ou associée à null.
merge(k, v, fn)Combine une valeur existante avec v via fn — le schéma compteur.
computeIfAbsent(k, fn)Calcule et stocke une valeur en cas d'absence — le schéma cache.
remove(k)Supprime l'entrée ; retourne la valeur supprimée, ou null.
containsKey(k)Le seul moyen fiable de distinguer « absent » de « associé à null ».

Itérez sur les entrées (pas sur les clés, quand vous avez aussi besoin des valeurs) pour éviter une seconde recherche par clé :

Map<String, Integer> scores = new HashMap<>();
scores.put("alice", 90);
scores.put("bob", 75);

for (Map.Entry<String, Integer> e : scores.entrySet()) {
    System.out.println(e.getKey() + " -> " + e.getValue());
}

// Or the lambda form:
scores.forEach((name, score) -> System.out.println(name + " -> " + score));

Organisation interne de la table

Un HashMap maintient un tableau de seaux dont la taille est une puissance de deux. L'insertion d'une entrée effectue cinq opérations :

  1. Calculer h = hashCode(key). Mélanger les 16 bits supérieurs et inférieurs ensemble — h ^ (h >>> 16) — pour qu'un hash comme 0x12340000 ne perde pas ses bits supérieurs lors du masquage.
  2. Masquer : i = h & (table.length - 1). C'est h mod length pour les longueurs en puissance de deux, et c'est plus rapide que l'opérateur modulo.
  3. Parcourir la chaîne à table[i]. Si un nœud avec une clé égale existe, écraser sa valeur et retourner l'ancienne.
  4. Sinon, ajouter (ou, depuis Java 8, en fin de liste) un nouveau nœud.
  5. Si size > capacity * loadFactor, redimensionner : doubler la table et re-répartir chaque entrée.

Jusqu'à Java 7, la chaîne de seaux était une liste simplement chaînée, point final. Depuis Java 8, dès qu'une chaîne atteint huit entrées, le seau est converti en un petit arbre équilibré (un arbre rouge-noir) indexé par le hash. La recherche dans ce seau devient O(log n) au lieu de O(n), ce qui limite les dégâts d'une attaque par déni de service exploitant des hashs en collision. Si l'arbre revient à six entrées ou moins, il redevient une liste. Vous ne verrez pas cela dans du code normal — cela n'importe que si votre hashCode est adversariel ou pathologiquement mauvais.

Capacité, facteur de charge et pré-dimensionnement

Les mêmes paramètres que HashSet :

  • Capacité initiale — 16 par défaut, arrondie à la puissance de deux supérieure.
  • Facteur de charge — 0.75 par défaut. Quand size > capacity * 0.75, la table double.

Si vous connaissez la taille à l'avance, pré-dimensionnez :

Map<String, User> users = new HashMap<>(expectedSize * 4 / 3); // skip the doublings

Ou, depuis Java 19, la fabrique explicite :

Map<String, User> users = HashMap.newHashMap(expectedSize);

C'est l'expression la plus claire de l'intention — elle calcule la bonne capacité initiale à partir d'une taille cible pour que la table n'ait pas besoin de croître.

Clés null et valeurs null

HashMap autorise une clé null (stockée dans le seau 0 avec le hash 0) et un nombre quelconque de valeurs null. C'est un avantage sur Hashtable (qui rejette les deux), mais cela brouille la signification de get(k) == null :

m.put("key", null);
m.get("key");          // returns null
m.containsKey("key"); // returns true

Le coût de la désambiguïsation est réel. Préférez ne pas stocker de valeurs null ; utilisez Optional, une valeur sentinelle, ou n'incluez tout simplement pas la clé. La fabrique Java 9+ Map.of(...) vous impose cette contrainte.

hashCode et equals sont votre contrat

Placer votre propre classe dans un HashMap ne fonctionne que si hashCode et equals sont cohérents. Les mêmes règles que pour HashSet :

  • Les objets égaux doivent avoir des codes de hachage égaux.
  • Les objets inégaux peuvent entrer en collision (c'est normal, c'est pourquoi les seaux sont des chaînes).
  • Muter une clé après insertion est un comportement indéfini.

Utilisez un record si vous le pouvez — les deux méthodes sont générées correctement. Ou laissez l'IDE les générer. Ne codez jamais hashCode à la main si vous pouvez l'éviter.

record UserId(String tenant, String localPart) {}
Map<UserId, User> directory = new HashMap<>();
directory.put(new UserId("acme", "alice"), new User(/*...*/));
directory.get(new UserId("acme", "alice")); // hit

Ordre d'itération — explicitement indéfini

HashMap ne garantit pas l'ordre d'itération. L'ordre dépend de la disposition des seaux, qui dépend du hash, de la capacité et de l'historique des redimensionnements — il peut changer entre les exécutions et entre les versions de la JVM. Si vous vous fiez à cet ordre, votre code est incorrect ; si vos tests en dépendent, ils sont instables.

Si l'ordre d'itération est important, utilisez LinkedHashMap pour l'ordre d'insertion ou TreeMap pour l'ordre trié. Les deux sont des remplaçants directs.

Non thread-safe

HashMap se corrompra lui-même en cas de mutation concurrente — et historiquement, un mode d'échec notoire était une boucle infinie lors d'un redimensionnement concurrent. Ne partagez pas un HashMap entre des threads. La bonne structure pour le code multi-thread est ConcurrentHashMap (couvert plus tard dans la partie concurrence). Collections.synchronizedMap(new HashMap<>()) existe, mais utilise un verrou unique autour de chaque opération, ce qui est plus lent et rarement la bonne réponse.

Un exemple concret : compteur, table de correspondance et idiomes modernes

Le programme ci-dessous utilise un HashMap de plusieurs façons : un compteur de mots via merge, un cache de mémoïsation récursif, une démonstration de l'ambiguïté des valeurs null, la fabrique newHashMap de Java 19, et un record comme clé composite.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • merge condense les trois étapes « get, valeur-par-défaut-ou-plus-un, put » en un seul appel. Utilisez-le chaque fois que vous maintenez un compteur ou une somme par clé.
  • Le cache de Fibonacci transforme une récursion exponentielle en récursion linéaire : vérifier la map, récurser en cas d'absence, puis put le résultat. Notez qu'il utilise get + put plutôt que computeIfAbsent — un computeIfAbsent récursif mute la map pendant que sa propre fonction de mappage est en cours d'exécution, et depuis Java 9, cela lève une ConcurrentModificationException. Réservez computeIfAbsent pour les lookups « charger-ou-calculer » non récursifs.
  • L'ambiguïté des valeurs null est réelle. get a retourné null de la même façon pour une clé présente et une clé absente. Le seul moyen de les distinguer est containsKey — ou en décidant de ne pas stocker de null en premier lieu.
  • Le pré-dimensionnement avec HashMap.newHashMap(1_000_000) permet à un million d'insertions de se terminer sans aucun rehachage — la table démarre avec la bonne capacité.
  • Le record UserId fournit des equals/hashCode corrects gratuitement. C'est la façon moderne de composer des clés de HashMap à partir de plusieurs champs.

Prochaine étape

HashMap ne garantit pas l'ordre d'itération. Si vous avez besoin que l'ordre d'insertion soit mémorisé — par exemple pour sérialiser la map en JSON avec une sortie stable — le bon outil est LinkedHashMap. C'est aussi la base d'un cache LRU classique, que nous couvrons dans le même chapitre.

Pratique

Pratique
Vous voyez `m.merge(key, 1, Integer::sum)` dans du code où `m` est un `Map<String, Integer>`. Que fait-il ?
Vous voyez `m.merge(key, 1, Integer::sum)` dans du code où `m` est un `Map<String, Integer>`. Que fait-il ?
Was this page helpful?