Analyse du code source de base TreeMap et LinkedHashMap

Analyse du code source de base TreeMap et LinkedHashMap

Après vous être familiarisé avec HashMap, jetons maintenant un coup d'œil à TreeMap et LinkedHashMap pour voir comment TreeMap est trié en fonction des clés et comment LinkedHashMap est accessible à l'aide de deux stratégies.

Un: connaissances de base

Avant de comprendre TreeMap, examinons les deux façons de trier dans le travail quotidien. En tant que réserve de base pour notre apprentissage, les deux façons sont:

  1. Implémentez l'interface Comparable;
  2. Utilisez le comparateur de tri externe pour trier;

Regardons maintenant l'implémentation du code de ces deux méthodes de tri:

@Data
class DTO implements Comparable<DTO> {
    
    

    private Integer id;

    public DTO(Integer id) {
    
    
        this.id = id;
    }

    public Integer getId() {
    
    
        return id;
    }

    @Override
    public int compareTo(DTO o) {
    
    
        //默认从小到大排序
        return id - o.getId();
    }
}

@Test
public void testComparable1() {
    
    
    // 第一种排序,从小到大排序,实现 Comparable 的 compareTo 方法进行排序
    List<DTO> list = new ArrayList<>();
    for (int i = 5; i > 0; i--) {
    
    
        list.add(new DTO(i));
    }
    Collections.sort(list);
    log.info(JSON.toJSONString(list));
}

@Test
public void testComparable2() {
    
    
    // 第二种排序,从大到小排序,利用外部排序器 Comparator 进行排序
    Comparator comparator = (Comparator<DTO>) (o1, o2) -> o2.getId() - o1.getId();
    List<DTO> list2 = new ArrayList<>();
    for (int i = 5; i > 0; i--) {
    
    
        list2.add(new DTO(i));
    }
    Collections.sort(list2, comparator);
    log.info(JSON.toJSONString(list2));
}

Trier le résultat 1
Trier le résultat 2

La première sortie de tri va de petite à grande, le résultat est: [{"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id ”: 5}];
La deuxième sortie est exactement le contraire, le résultat est: [{" id ": 5}, {" id ": 4}, {" id ": 3}, {" id ": 2} , {"Id": 1}].
Les deux ci-dessus sont les méthodes de tri par Comparable et Comparateur respectivement, et TreeMap utilise également ce principe pour réaliser le tri des clés.

2: L'architecture globale de TreeMap

La structure de données sous-jacente de TreeMap est l'arbre rouge-noir, qui est identique à l'arborescence rouge-noir de HashMap.

La différence est que TreeMap tire parti de la nature de l'arbre rouge-noir: le nœud gauche est petit et le nœud droit est grand. Il trie en fonction de la clé, de sorte que chaque élément puisse être inséré dans la position appropriée de l'arbre rouge-noir, maintient la relation de taille de clé et convient à la clé Scènes qui doivent être triées.

Étant donné que la couche inférieure utilise une structure arborescente rouge-noire équilibrée, la complexité temporelle des méthodes telles que containsKey, get, put et remove sont toutes log (n).

2.1: Propriétés de TreeMap

Les attributs communs de TreeMap sont:

//比较器,如果外部有传进来 Comparator 比较器,首先用外部的
//如果外部比较器为空,则使用 key 自己实现的 Comparable#compareTo 方法
//比较手段和上面日常工作中的比较 demo 是一致的
private final Comparator<? super K> comparator;

//红黑树的根节点
private transient Entry<K,V> root;

//红黑树的已有元素大小
private transient int size = 0;

//树结构变化的版本号,用于迭代过程中的快速失败场景
private transient int modCount = 0;

//红黑树的节点
static final class Entry<K,V> implements Map.Entry<K,V> {
    
    }

2.2: Nouveau nœud

Les étapes d'ajout de nœuds à TreeMap sont les suivantes:

  1. Déterminez si le nœud de l'arbre rouge-noir est vide. S'il est vide, le nouveau nœud sera directement utilisé comme nœud racine. Le code est le suivant:
    Entry<K,V> t = root;
    //红黑树根节点为空,直接新建
    if (t == null) {
          
          
        // compare 方法限制了 key 不能为 null
        compare(key, key); // type (and possibly null) check
        // 成为根节点
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    
  2. Selon les caractéristiques de l'arbre rouge-noir, la gauche est petite, la droite est grande et le nœud parent du nouveau nœud doit être trouvé.
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
          
          
        //自旋找到 key 应该新增的位置,就是应该挂载那个节点的头上
        do {
          
          
            //一次循环结束时,parent 就是上次比过的对象
            parent = t;
            // 通过 compare 来比较 key 的大小
            cmp = cpr.compare(key, t.key);
            //key 小于 t,把 t 左边的值赋予 t,因为红黑树左边的值比较小,循环再比
            if (cmp < 0)
                t = t.left;
            //key 大于 t,把 t 右边的值赋予 t,因为红黑树右边的值比较大,循环再比
            else if (cmp > 0)
                t = t.right;
            //如果相等的话,直接覆盖原值
            else
                return t.setValue(value);
            // t 为空,说明已经到叶子节点了
        } while (t != null);
    }
    
  3. Insérez un nouveau nœud à gauche ou à droite du nœud parent, le code est le suivant:
    //cmp 代表最后一次对比的大小,小于 0 ,代表 e 在上一节点的左边
    if (cmp < 0)
        parent.left = e;
    //cmp 代表最后一次对比的大小,大于 0 ,代表 e 在上一节点的右边,相等的情况第二步已经处理了。
    else
        parent.right = e;
    
  4. La coloration tourne, atteint l'équilibre et se termine.

Nous pouvons voir à partir du code source ci-dessus:

  1. Lors de l'ajout d'un nouveau nœud, il utilise les caractéristiques de l'arbre rouge-noir selon lesquelles la gauche est petite et la droite est grande, et le nœud racine est continuellement recherché jusqu'à ce que le nœud soit trouvé comme étant nul. Si le nœud est nul, cela signifie que le nœud feuille est atteint;
  2. Pendant le processus de recherche, il est constaté que la valeur de clé existe déjà et elle est directement écrasée;
  3. TreeMap empêche la clé d'être une valeur nulle;

2.3: Résumé de TreeMap

TreeMap est relativement simple. Les arbres rouge-noir et HashMap sont similaires. La clé est de comparer la taille des clés par comparaison, puis d'utiliser les caractéristiques des arbres rouge-noir pour trouver leur propre position pour chaque clé à maintenir L'ordre de tri de la taille de la clé.

Trois: architecture globale de LinkedHashMap

LinkedHashMap lui-même hérite de HashMap, il possède donc toutes les fonctionnalités de HashMap, et sur cette base, il fournit également deux fonctionnalités majeures:

  1. Visite dans l'ordre d'insertion;
  2. Obtenez le moins d'accès et la première fonction de suppression, le but est de supprimer automatiquement les clés qui n'ont pas été accédées depuis longtemps;

3.1: Accès par ordre d'insertion

3.1.1: Structure de liste liée LinkedHashMap

Jetons un coup d'œil aux attributs ajoutés à LinkedHashMap pour obtenir la structure de la liste liée:

// 链表头
transient LinkedHashMap.Entry<K,V> head;

// 链表尾
transient LinkedHashMap.Entry<K,V> tail;

// 继承 Node,为数组的每个元素增加了 before 和 after 属性
static class Entry<K,V> extends HashMap.Node<K,V> {
    
    
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
    
    
        super(hash, key, value, next);
    }
}

// 控制两种访问模式的字段,默认 false
// true 按照访问顺序,会把经常访问的 key 放到队尾
// false 按照插入顺序提供访问
final boolean accessOrder;

Comme le montrent les nouveaux attributs de la carte ci-dessus, la structure de données de LinkedHashMap est très similaire au remplacement de chaque élément de LinkedList par le nœud de HashMap, comme une combinaison des deux. C'est précisément grâce à l'ajout de ces structures qu'il peut Les éléments de la carte sont connectés en série pour former une liste liée, et la liste liée peut garantir l'ordre, et l'ordre dans lequel les éléments sont insérés peut être conservé.

3.1.2: Comment ajouter dans l'ordre

Lorsque LinkedHashMap est initialisé, la valeur par défaut accessOrder est false, ce qui signifie que l'accès sera fourni dans l'ordre d'insertion. La méthode insert utilise la méthode put de la classe parent HashMap, mais écrase les méthodes newNode / newTreeNode et afterNodeAccess appelées lors de la méthode put.

La méthode newNode / newTreeNode contrôle l'ajout de nouveaux nœuds à la fin de la liste liée, de sorte que chaque fois qu'un nouveau nœud est ajouté à la fin, l'ordre d'insertion peut être garanti. Prenons le code source newNode comme exemple:

// 新增节点,并追加到链表的尾部
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    
    
    // 新增节点
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    // 追加到链表的尾部
    linkNodeLast(p);
    return p;
}
// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    
    
    LinkedHashMap.Entry<K,V> last = tail;
    // 新增节点等于位节点
    tail = p;
    // last 为空,说明链表为空,首尾节点相等
    if (last == null)
        head = p;
    // 链表有数据,直接建立新增节点和上个尾节点之间的前后关系即可
    else {
    
    
        p.before = last;
        last.after = p;
    }
}

LinkedHashMap ajoute des attributs avant et après à chaque nœud en ajoutant un nœud de tête et un nœud de queue. À chaque fois que le nœud est ajouté, le nœud est ajouté au nœud de queue. Lorsqu'il est ajouté, l'ordre d'insertion est conservé. Structure de liste liée.

3.1.2: Visite dans l'ordre

LinkedHashMap ne fournit qu'un accès unidirectionnel, c'est-à-dire que l'accès est effectué du début à la fin dans l'ordre d'insertion, et n'est pas accessible dans les deux sens comme LinkedList.

On y accède principalement par l'itérateur. Lorsque l'itérateur est initialisé, on y accède par défaut depuis le nœud principal. Pendant l'itération, il suffit d'accéder en continu au nœud arrière du nœud courant.

Map fournit une méthode itérative pour la clé, la valeur et l'entité (nœud). En supposant que nous avons besoin d'itérer l'entité, nous pouvons utiliser LinkedHashMap.entrySet (). Iterator () pour renvoyer directement LinkedHashIterator, LinkedHashIterator est un itérateur, nous appelons La méthode nextNode de l'itérateur peut obtenir le nœud suivant. Le code source de l'itérateur est le suivant:

// 初始化时,默认从头节点开始访问
LinkedHashIterator() {
    
    
    // 头节点作为第一个访问的节点
    next = head;
    expectedModCount = modCount;
    current = null;
}

final LinkedHashMap.Entry<K,V> nextNode() {
    
    
    LinkedHashMap.Entry<K,V> e = next;
    if (modCount != expectedModCount)// 校验
        throw new ConcurrentModificationException();
    if (e == null)
        throw new NoSuchElementException();
    current = e;
    next = e.after; // 通过链表的 after 结构,找到下一个迭代的节点
    return e;
}

Lors de l'ajout de nouveaux nœuds, nous avons déjà conservé l'ordre d'insertion entre les éléments, l'accès itératif est donc très simple, il suffit d'accéder en continu au nœud suivant du nœud actuel.

3.2: Accès à la stratégie de moindre suppression

3.2.1: Cas

Cette stratégie est également appelée LRU (le moins récemment utilisé, le moins récemment utilisé), ce qui signifie en gros que les éléments fréquemment consultés seront ajoutés à la fin de l'équipe, de sorte que les données qui ne sont pas fréquemment consultées seront naturellement proches du chef de l'équipe, et ensuite nous pouvons définir la stratégie de suppression , Par exemple, lorsque le nombre d'éléments de la carte est supérieur au nombre, supprimez le nœud principal, nous écrivons une démo pour démontrer.

@Test
public void testAccessOrder() {
    
    
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(4,0.75f,true) {
    
    
  {
    
    
    put(10, 10);
    put(9, 9);
    put(20, 20);
    put(1, 1);
  }

  @Override
  protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
    
    
    return size() > 3;
  }
};

log.info("初始化:{}",JSON.toJSONString(map));
Assert.assertNotNull(map.get(9));
log.info("map.get(9):{}",JSON.toJSONString(map));
Assert.assertNotNull(map.get(20));
log.info("map.get(20):{}",JSON.toJSONString(map));

}

Le résultat imprimé est le suivant: Comme
LRU
vous pouvez le voir, lorsque la carte est initialisée, nous mettons quatre éléments, mais le résultat est seulement trois éléments, 10 est manquant, c'est principalement parce que nous écrasons la méthode removeEldestEntry, nous avons réalisé si Lorsque le nombre d'éléments de la carte est supérieur à 3, nous supprimons l'élément en tête de l'équipe. Lorsque put (1, 1) est exécuté, le 10 en tête de l'équipe est supprimé. Cela signifie que lorsque la stratégie de suppression que nous avons définie est atteinte, il sera Supprimez automatiquement le nœud principal.

Lorsque nous appelons la méthode map.get (9), l'élément 9 est déplacé à la fin de la file d'attente, et lorsque la méthode map.get (20) est appelée, l'élément 20 est déplacé à la fin de la file d'attente. Cela signifie que le nœud fréquemment visité sera déplacé vers la file d'attente. queue.

Cet exemple est une bonne illustration de la stratégie de suppression de moindre accès. Ensuite, regardons le principe.

3.2.2: L'élément est transféré à la fin de la ligne

Voyons d'abord pourquoi l'élément sera déplacé à la fin de la file d'attente lors de l'obtention:

public V get(Object key) {
    
    
    Node<K,V> e;
    // 调用 HashMap  get 方法
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 如果设置了 LRU 策略
    if (accessOrder)
    // 这个方法把当前 key 移动到队尾
        afterNodeAccess(e);
    return e.value;
}

À partir du code source ci-dessus, on peut voir que le nœud d'accès actuel est déplacé à la fin de la file d'attente via la méthode afterNodeAccess. Ce n'est pas seulement la méthode get, mais aussi lorsque les méthodes getOrDefault, compute, computeIfAbsent, computeIfPresent et merge sont exécutées. Les nœuds fréquemment visités se déplacent à la fin de l'équipe, de sorte que les nœuds proches du chef d'équipe sont naturellement les éléments qui sont rarement visités.

3.2.3: Supprimer la stratégie

Dans le cas ci-dessus, lorsque nous avons exécuté la méthode put, nous avons constaté que l'élément head a été supprimé. LinkedHashMap lui-même n'est pas implémenté par la méthode put. Au lieu de cela, la méthode put de HashMap est appelée, mais LinkedHashMap implémente la méthode afterNodeInsertion dans la méthode put, qui est implémentée de cette manière Pour supprimer, regardons le code source:

// 删除很少被访问的元素,被 HashMap 的 put 方法所调用
void afterNodeInsertion(boolean evict) {
    
     
    // 得到元素头节点
    LinkedHashMap.Entry<K,V> first;
    // removeEldestEntry 来控制删除策略,如果队列不为空,并且删除策略允许删除的情况下,删除头节点
    if (evict && (first = head) != null && removeEldestEntry(first)) {
    
    
        K key = first.key;
        // removeNode 删除头节点
        removeNode(hash(key), key, null, false, true);
    }
}

Quatre: Résumé

Ci-dessus, nous avons principalement parlé de la structure de données de TreeMap et LinkedHashMap, et analysé le code source du contenu principal des deux. Nous avons constaté que les deux exploitent pleinement les caractéristiques de la structure de données sous-jacente. TreeMap utilise les caractéristiques des arbres rouge et noir pour trier. LinkedHashMap ajoute simplement une structure de liste chaînée sur la base de HashMap pour former l'ordre des nœuds, ce qui est très intelligent.

Je suppose que tu aimes

Origine blog.csdn.net/weixin_38478780/article/details/107904319
conseillé
Classement