Un guide sur LinkedHashMap en Java

Un guide sur LinkedHashMap en Java

1. Vue d'ensemble

Dans cet article, nous allons explorer l'implémentation interne de la classeLinkedHashMap. LinkedHashMap est une implémentation courante de l'interfaceMap.

Cette implémentation particulière est une sous-classe deHashMap et partage donc les blocs de construction de base desHashMap implementation. Par conséquent, il est fortement recommandé de réviser cela avant de poursuivre cet article.

2. LinkedHashMap contreHashMap

La classeLinkedHashMap est très similaire àHashMap dans la plupart des aspects. Cependant, la carte de hachage liée est basée à la fois sur la table de hachage et la liste liée pour améliorer la fonctionnalité de la carte de hachage.

Il gère une liste doublement chaînée parcourant toutes ses entrées en plus d'un tableau sous-jacent de taille par défaut 16.

Pour conserver l'ordre des éléments, la table de hachage liée modifie la classeMap.Entry deHashMap en ajoutant des pointeurs vers les entrées suivantes et précédentes:

static class Entry extends HashMap.Node {
    Entry before, after;
    Entry(int hash, K key, V value, Node next) {
        super(hash, key, value, next);
    }
}

Notez que la classeEntry ajoute simplement deux pointeurs; before etafter qui lui permettent de se raccorder à la liste chaînée. En dehors de cela, il utilise l'implémentation de classeEntry d'un HashMap.

Enfin, rappelez-vous que cette liste liée définit l'ordre d'itération, qui est par défaut l'ordre d'insertion d'éléments (ordre d'insertion).

3. Ordre d'insertionLinkedHashMap

Jetons un coup d'œil à une instance de carte de hachage liée qui classe ses entrées en fonction de la manière dont elles sont insérées dans la carte. Cela garantit également que cet ordre sera maintenu tout au long du cycle de vie de la carte:

@Test
public void givenLinkedHashMap_whenGetsOrderedKeyset_thenCorrect() {
    LinkedHashMap map = new LinkedHashMap<>();
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);

    Set keys = map.keySet();
    Integer[] arr = keys.toArray(new Integer[0]);

    for (int i = 0; i < arr.length; i++) {
        assertEquals(new Integer(i + 1), arr[i]);
    }
}

Ici, nous faisons simplement un test rudimentaire et non concluant sur l'ordre des entrées dans la carte de hachage liée.

Nous pouvons garantir que ce test réussira toujours car l'ordre d'insertion sera toujours maintenu. We cannot make the same guarantee for a HashMap.

Cet attribut peut être très avantageux dans une API qui reçoit une carte, crée une copie à manipuler et la renvoie au code appelant. Si le client a besoin que la mappe renvoyée soit commandée de la même manière avant d'appeler l'API, un hashmap lié est la solution.

L'ordre d'insertion n'est pas affecté si une clé est réinsérée dans la carte.

4. Ordre d'accèsLinkedHashMap

LinkedHashMap fournit un constructeur spécial qui nous permet de spécifier, parmi le facteur de charge personnalisé (LF) et la capacité initiale,a different ordering mechanism/strategy called access-order:

LinkedHashMap map = new LinkedHashMap<>(16, .75f, true);

Le premier paramètre est la capacité initiale, suivie du facteur de charge et deslast param is the ordering mode. Ainsi, en passant entrue, nous avons obtenu l'ordre d'accès, alors que la valeur par défaut était l'ordre d'insertion.

Ce mécanisme garantit que l'ordre d'itération des éléments correspond à l'ordre dans lequel les éléments ont été consultés pour la dernière fois, du plus récent accès au plus récent.

Il est donc très simple et pratique de créer une mémoire cache au moins récemment utilisé (LRU). Une opérationput ouget réussie entraîne un accès pour l'entrée:

@Test
public void givenLinkedHashMap_whenAccessOrderWorks_thenCorrect() {
    LinkedHashMap map
      = new LinkedHashMap<>(16, .75f, true);
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);

    Set keys = map.keySet();
    assertEquals("[1, 2, 3, 4, 5]", keys.toString());

    map.get(4);
    assertEquals("[1, 2, 3, 5, 4]", keys.toString());

    map.get(1);
    assertEquals("[2, 3, 5, 4, 1]", keys.toString());

    map.get(3);
    assertEquals("[2, 5, 4, 1, 3]", keys.toString());
}

Remarquez commentthe order of elements in the key set is transformed as we perform access operations on the map.

En termes simples, toute opération d’accès sur la carte a pour résultat un ordre tel que l’élément auquel on a accédé apparaisse en dernier si une itération devait être effectuée immédiatement.

Après les exemples ci-dessus, il devrait être évident qu'une opérationputAll génère un accès d'entrée pour chacun des mappages dans la carte spécifiée.

Naturellement, l'itération sur une vue de la carte n'affecte pas l'ordre d'itération de la carte de support; only explicit access operations on the map will affect the order.

LinkedHashMap fournit également un mécanisme pour maintenir un nombre fixe de mappages et pour continuer à déposer les entrées les plus anciennes au cas où une nouvelle devrait être ajoutée.

La méthoderemoveEldestEntry peut être remplacée pour appliquer cette stratégie de suppression automatique des mappages périmés.

Pour voir cela en pratique, créons notre propre classe de carte de hachage liée, dans le seul but d'imposer la suppression des mappages périmés en étendantLinkedHashMap:

public class MyLinkedHashMap extends LinkedHashMap {

    private static final int MAX_ENTRIES = 5;

    public MyLinkedHashMap(
      int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor, accessOrder);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }

}

Notre remplacement ci-dessus permettra à la carte d’augmenter jusqu’à 5 entrées. Lorsque la taille dépasse cette limite, chaque nouvelle entrée est insérée au prix de la perte de l’entrée la plus ancienne sur la carte, c.-à-d. l'entrée dont la dernière heure d'accès précède toutes les autres entrées:

@Test
public void givenLinkedHashMap_whenRemovesEldestEntry_thenCorrect() {
    LinkedHashMap map
      = new MyLinkedHashMap<>(16, .75f, true);
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);
    Set keys = map.keySet();
    assertEquals("[1, 2, 3, 4, 5]", keys.toString());

    map.put(6, null);
    assertEquals("[2, 3, 4, 5, 6]", keys.toString());

    map.put(7, null);
    assertEquals("[3, 4, 5, 6, 7]", keys.toString());

    map.put(8, null);
    assertEquals("[4, 5, 6, 7, 8]", keys.toString());
}

Notez que les entrées les plus anciennes au début de l’ensemble de clés ne cessent de diminuer au fur et à mesure que nous en ajoutons de nouvelles sur la carte.

5. Considérations sur les performances

Tout commeHashMap,LinkedHashMap effectue les opérations de baseMap d'ajout, de suppression et de contenu en temps constant, tant que la fonction de hachage est bien dimensionnée. Il accepte également une clé NULL ainsi que des valeurs NULL.

Cependant, ceconstant-time performance of LinkedHashMap is likely to be a little worse than the constant-time of HashMapest dû à la surcharge supplémentaire liée au maintien d'une liste à double liaison.

L'itération sur les vues de collection deLinkedHashMap prend également un temps linéaireO(n) similaire à celui deHashMap. D'un autre côté,LinkedHashMap‘s linear time performance during iteration is better than HashMap‘s linear time.

Cela est dû au fait que, pourLinkedHashMap,n enO(n) est uniquement le nombre d'entrées dans la carte quelle que soit la capacité. Alors que, pourHashMap,n est la capacité et la taille additionnée,O(size+capacity).

Le facteur de charge et la capacité initiale sont définis précisément comme pourHashMap. Notez, cependant, quethe penalty for choosing an excessively high value for initial capacity is less severe for LinkedHashMap than for HashMap, car les temps d'itération pour cette classe ne sont pas affectés par la capacité.

6. Simultanéité

Tout commeHashMap, l'implémentation deLinkedHashMap n'est pas synchronisée. Donc, si vous voulez y accéder à partir de plusieurs threads et qu'au moins un de ces threads est susceptible de le modifier structurellement, il doit être synchronisé de manière externe.

Il est préférable de le faire lors de la création:

Map m = Collections.synchronizedMap(new LinkedHashMap());

La différence avecHashMap réside dans ce qui implique une modification structurelle. In access-ordered linked hash maps, merely calling the get API results in a structural modification. A côté de cela, il y a des opérations commeput etremove.

7. Conclusion

Dans cet article, nous avons exploré la classe JavaLinkedHashMap comme l'une des principales implémentations de l'interfaceMap en termes d'utilisation. Nous avons également exploré son fonctionnement interne en termes de différence avecHashMap qui est sa superclasse.

Heureusement, après avoir lu cet article, vous pourrez prendre des décisions plus éclairées et plus efficaces quant à l’implémentation de Map à utiliser dans votre cas d’utilisation.

Le code source complet de tous les exemples utilisés dans cet article se trouve dans lesGitHub project.