Análisis del código fuente principal de TreeMap y LinkedHashMap

Análisis del código fuente principal de TreeMap y LinkedHashMap

Después de familiarizarnos con HashMap, ahora echemos un vistazo a TreeMap y LinkedHashMap para ver cómo se ordena TreeMap según las claves, y cómo se accede a LinkedHashMap utilizando dos estrategias.

Uno: conocimientos básicos

Antes de entender TreeMap, veamos las dos formas de ordenar en el trabajo diario.Como reserva básica para nuestro aprendizaje, las dos formas son:

  1. Implementar la interfaz Comparable;
  2. Utilice el comparador clasificador externo para clasificar;

Ahora veamos la implementación del código de estos dos métodos de clasificación:

@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));
}

Ordenar resultado 1
Ordenar resultado 2

La primera salida de clasificación resulta de pequeña a grande, el resultado es: [{"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id ”: 5}];
La segunda salida es todo lo contrario, el resultado es: [{" id ": 5}, {" id ": 4}, {" id ": 3}, {" id ": 2} , {"Id": 1}].
Los dos anteriores son los métodos de clasificación por Comparable y Comparator respectivamente, y TreeMap también usa este principio para realizar la clasificación de claves. Echemos un vistazo juntos.

2: La arquitectura general de TreeMap

La estructura de datos subyacente de TreeMap es el árbol rojo-negro, que es el mismo que la estructura de árbol rojo-negro de HashMap.

La diferencia es que TreeMap aprovecha la naturaleza del árbol rojo-negro que el nodo izquierdo es pequeño y el nodo derecho es grande. Se clasifica de acuerdo con la clave, de modo que cada elemento se puede insertar en la posición apropiada del árbol rojo-negro, mantiene la relación de tamaño de clave y es adecuado para clave Escenas que deben ordenarse.

Debido a que la capa inferior usa una estructura de árbol balanceada rojo-negro, la complejidad temporal de métodos como containsKey, get, put y remove son todos log (n).

2.1: Propiedades de TreeMap

Los atributos comunes de TreeMap son:

//比较器,如果外部有传进来 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: Nuevo nodo

Los pasos para agregar nodos a TreeMap son los siguientes:

  1. Determine si el nodo del árbol rojo-negro está vacío. Si está vacío, el nuevo nodo se utilizará directamente como nodo raíz. El código es el siguiente:
    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. Según las características del árbol rojo-negro, la izquierda es pequeña y la derecha es grande, juzgue y encuentre el nodo padre del nuevo nodo. El código es el siguiente:
    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. Inserte un nuevo nodo a la izquierda o derecha del nodo principal, el código es el siguiente:
    //cmp 代表最后一次对比的大小,小于 0 ,代表 e 在上一节点的左边
    if (cmp < 0)
        parent.left = e;
    //cmp 代表最后一次对比的大小,大于 0 ,代表 e 在上一节点的右边,相等的情况第二步已经处理了。
    else
        parent.right = e;
    
  4. La coloración gira, se equilibra y termina.

Podemos ver en el código fuente anterior:

  1. Al agregar un nuevo nodo, utiliza las características del árbol rojo-negro de que la izquierda es pequeña y la derecha es grande, y el nodo raíz se busca continuamente hacia abajo hasta que se encuentra que el nodo es nulo. Si el nodo es nulo, significa que se alcanzó el nodo hoja;
  2. Durante el proceso de búsqueda, se encuentra que el valor de la clave ya existe y se sobrescribe directamente;
  3. TreeMap prohíbe que la clave sea un valor nulo;

2.3: Resumen de TreeMap

TreeMap es relativamente simple. Los árboles rojo-negro y HashMap son similares. La clave es comparar el tamaño de las claves mediante la comparación, y luego usar las características de los árboles rojo-negro para encontrar su propia posición para cada clave a mantener El orden de clasificación del tamaño de la clave.

Tres: Arquitectura general de LinkedHashMap

LinkedHashMap en sí mismo hereda HashMap, por lo que tiene todas las características de HashMap, y sobre esta base, también proporciona dos características principales:

  1. Visita en el orden de inserción;
  2. Logre la función de menor acceso y primera eliminación, el propósito es eliminar automáticamente las claves a las que no se ha accedido durante mucho tiempo;

3.1: Acceso por orden de inserción

3.1.1: Estructura de lista enlazada LinkedHashMap

Echemos un vistazo a los atributos que se agregan a LinkedHashMap para lograr la estructura de la lista vinculada:

// 链表头
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;

Como se puede ver en los nuevos atributos del Mapa anterior, la estructura de datos de LinkedHashMap es muy similar a reemplazar cada elemento de LinkedList con el Nodo de HashMap, como una combinación de los dos. Es precisamente por la adición de estas estructuras que puede Los elementos del Mapa están conectados en serie para formar una lista enlazada, y la lista enlazada puede garantizar el orden, y se puede mantener el orden en el que se insertan los elementos.

3.1.2: Cómo agregar en orden

Cuando se inicializa LinkedHashMap, el accessOrder predeterminado es falso, lo que significa que el acceso se proporcionará en el orden de inserción. El método insert usa el método put de la clase padre HashMap, pero sobrescribe los métodos newNode / newTreeNode y afterNodeAccess llamados durante el método put.

El método newNode / newTreeNode controla la adición de nuevos nodos al final de la lista vinculada, de modo que cada vez que se agrega un nuevo nodo al final, se puede garantizar el orden de inserción. Tomemos el código fuente de newNode como ejemplo:

// 新增节点,并追加到链表的尾部
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 agrega atributos antes y después a cada nodo agregando un nodo principal y un nodo final. Cada vez que se agrega el nodo, el nodo se agrega al nodo final. Cuando se agrega, se mantiene el orden de inserción. Estructura de lista vinculada.

3.1.2: Visita en orden

LinkedHashMap solo proporciona acceso unidireccional, es decir, el acceso se realiza de principio a fin en el orden de inserción y no se puede acceder en ambas direcciones como LinkedList.

Principalmente accedemos a él a través del iterador, cuando el iterador se inicializa, se accede desde el nodo principal por defecto, durante la iteración basta con acceder continuamente al nodo posterior del nodo actual.

Map proporciona un método iterativo para clave, valor y entidad (nodo). Asumiendo que necesitamos iterar la entidad, podemos usar LinkedHashMap.entrySet (). Iterator () para devolver directamente LinkedHashIterator, LinkedHashIterator es un iterador, llamamos El método nextNode del iterador puede obtener el siguiente nodo. El código fuente del iterador es el siguiente:

// 初始化时,默认从头节点开始访问
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;
}

Al agregar nuevos nodos, ya hemos mantenido el orden de inserción entre elementos, por lo que el acceso iterativo es muy sencillo, solo es necesario visitar constantemente el siguiente nodo del nodo actual.

3.2: Acceso a la estrategia de menor eliminación

3.2.1: Caso

Esta estrategia también se llama LRU (Usado menos recientemente, usado menos recientemente), lo que aproximadamente significa que los elementos a los que se accede con frecuencia se agregarán al final del equipo, de modo que los datos a los que no se accede con frecuencia estarán naturalmente cerca del jefe del equipo, y luego podemos establecer la estrategia de eliminación Por ejemplo, cuando la cantidad de elementos del mapa es mayor que la cantidad, elimine el nodo principal, escribimos una demostración para demostrar.

@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));

}

El resultado impreso es el siguiente: Como
LRU
puede ver, cuando se inicializa el mapa, colocamos cuatro elementos, pero el resultado es solo tres elementos, falta 10, esto se debe principalmente a que sobrescribimos el método removeEldestEntry, nos dimos cuenta de que si Cuando el número de elementos en el mapa es mayor a 3, eliminamos el elemento que está al frente del equipo. Cuando se ejecuta put (1, 1), se elimina el 10 al frente del equipo. Esto refleja que cuando se alcanza la estrategia de eliminación que establecimos, se Elimina automáticamente el nodo principal.

Cuando llamamos al método map.get (9), el elemento 9 se mueve al final de la cola, y cuando se llama al método map.get (20), el elemento 20 se mueve al final de la cola. Esto refleja que el nodo visitado con frecuencia se moverá a la cola. cola.

Este ejemplo es una buena ilustración de la estrategia de eliminación de acceso mínimo. A continuación, veamos el principio.

3.2.2: El elemento se transfiere al final de la línea.

Primero echemos un vistazo a por qué el elemento se moverá al final de la cola al obtener:

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;
}

En el código fuente anterior, se puede ver que el nodo de acceso actual se mueve al final de la cola a través del método afterNodeAccess. No es solo el método get, sino también cuando se ejecutan los métodos getOrDefault, compute, computeIfAbsent, computeIfPresent y merge. Los nodos visitados con frecuencia se mueven al final del equipo, por lo que los nodos cerca de la cabeza del equipo son naturalmente los elementos que rara vez se visitan.

3.2.3: Eliminar estrategia

En el caso anterior, cuando ejecutamos el método put, encontramos que el elemento head fue eliminado. LinkedHashMap en sí no es implementado por el método put. En su lugar, se llama al método put de HashMap, pero LinkedHashMap implementa el método afterNodeInsertion en el método put, que se implementa de esta manera Para eliminar, veamos el código fuente:

// 删除很少被访问的元素,被 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);
    }
}

Cuatro: Resumen

Anteriormente hablamos principalmente sobre la estructura de datos de TreeMap y LinkedHashMap, y analizamos el código fuente del contenido principal de los dos. Descubrimos que ambos hacen un uso completo de las características de la estructura de datos subyacente. TreeMap usa las características de los árboles rojo y negro para ordenar. LinkedHashMap simplemente agrega una estructura de lista vinculada sobre la base de HashMap para formar el orden de los nodos, lo cual es muy inteligente.

Supongo que te gusta

Origin blog.csdn.net/weixin_38478780/article/details/107904319
Recomendado
Clasificación