Análise de código-fonte central TreeMap e LinkedHashMap

Análise de código-fonte central TreeMap e LinkedHashMap

Depois de nos familiarizar com o HashMap, vamos agora dar uma olhada em TreeMap e LinkedHashMap para ver como TreeMap é classificado de acordo com as chaves e como LinkedHashMap é acessado usando duas estratégias.

Um: conhecimento básico

Antes de entender o TreeMap, vamos examinar as duas maneiras de classificar no trabalho diário. Como reserva básica para nosso aprendizado, as duas maneiras são:

  1. Implementar a interface Comparable;
  2. Use o comparador de classificador externo para classificar;

Agora vamos dar uma olhada na implementação de código desses dois métodos de classificação:

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

Classificar resultado 1
Classificar resultado 2

A primeira saída de classificação resulta de pequeno para grande, o resultado é: [{"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id ”: 5}];
A segunda saída é exatamente o oposto, o resultado é: [{" id ": 5}, {" id ": 4}, {" id ": 3}, {" id ": 2} , {"Id": 1}].
Os dois acima são os métodos de classificação por Comparable e Comparator respectivamente, e TreeMap também usa esse princípio para realizar a classificação de chaves. Vamos dar uma olhada juntos.

2: A arquitetura geral do TreeMap

A estrutura de dados subjacente do TreeMap é a árvore vermelho-preto, que é igual à estrutura da árvore vermelho-preto do HashMap.

A diferença é que TreeMap tira proveito da natureza da árvore vermelho-preto de que o nó esquerdo é pequeno e o nó direito é grande. Ele classifica de acordo com a chave, de modo que cada elemento possa ser inserido na posição apropriada da árvore vermelho-preto, mantém a relação de tamanho da chave e é adequado para chave Cenas que precisam ser classificadas.

Como a camada inferior usa uma estrutura de árvore vermelha e preta balanceada, a complexidade de tempo dos métodos como containsKey, get, put e remove são todos log (n).

2.1: Propriedades do TreeMap

Os atributos comuns de TreeMap são:

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

As etapas para adicionar nós ao TreeMap são as seguintes:

  1. Determine se o nó da árvore vermelho-preto está vazio. Se estiver vazio, o novo nó será usado diretamente como o nó raiz. O código é o seguinte:
    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. De acordo com as características da árvore vermelho-preto, a esquerda é pequena e a direita é grande, julgue e encontre o nó pai do novo nó. O código é o seguinte:
    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. Insira um novo nó à esquerda ou direita do nó pai, o código é o seguinte:
    //cmp 代表最后一次对比的大小,小于 0 ,代表 e 在上一节点的左边
    if (cmp < 0)
        parent.left = e;
    //cmp 代表最后一次对比的大小,大于 0 ,代表 e 在上一节点的右边,相等的情况第二步已经处理了。
    else
        parent.right = e;
    
  4. A coloração gira, atinge o equilíbrio e termina.

Podemos ver no código-fonte acima:

  1. Ao adicionar um novo nó, ele usa as características da árvore rubro-negra de que a esquerda é pequena e a direita grande, e o nó raiz é continuamente pesquisado até que o nó seja considerado nulo.Se o nó for nulo, significa que o nó folha foi atingido;
  2. Durante o processo de busca, verifica-se que o valor da chave já existe e é sobrescrito diretamente;
  3. TreeMap proíbe a chave de ser um valor nulo;

2.3: Resumo do TreeMap

TreeMap é relativamente simples. Árvores vermelho-pretas e HashMap são semelhantes. A chave é comparar o tamanho das chaves por meio de comparação e, em seguida, usar as características das árvores vermelho-pretas para encontrar sua própria posição para cada chave para manter A ordem de classificação do tamanho da chave.

Três: arquitetura geral LinkedHashMap

O próprio LinkedHashMap herda o HashMap, portanto, possui todos os recursos do HashMap e, com base nisso, também fornece dois recursos principais:

  1. Visita por ordem de inserção;
  2. Obter a menor função de acesso e primeiro excluir, o objetivo é excluir automaticamente as chaves que não foram acessadas por um longo tempo;

3.1: Acesso por ordem de inserção

3.1.1: Estrutura de lista vinculada LinkedHashMap

Vamos dar uma olhada em quais atributos são adicionados a LinkedHashMap para obter a estrutura da 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 pode ser visto a partir dos novos atributos do Mapa acima, a estrutura de dados de LinkedHashMap é muito semelhante à substituição de cada elemento de LinkedList pelo Node de HashMap, como uma combinação dos dois. É precisamente por causa da adição dessas estruturas que ela pode Os elementos do Mapa são conectados em série para formar uma lista vinculada, e a lista vinculada pode garantir a ordem, e a ordem em que os elementos são inseridos pode ser mantida.

3.1.2: Como adicionar em ordem

Quando LinkedHashMap é inicializado, o accessOrder padrão é false, o que significa que o acesso será fornecido na ordem de inserção. O método insert usa o método put da classe pai HashMap, mas sobrescreve os métodos newNode / newTreeNode e afterNodeAccess chamados durante o método put.

O método newNode / newTreeNode controla o acréscimo de novos nós ao final da lista vinculada, de modo que toda vez que um novo nó é anexado ao final, a ordem de inserção pode ser garantida. Vamos tomar o código-fonte newNode como exemplo:

// 新增节点,并追加到链表的尾部
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 adiciona atributos antes e depois de cada nó adicionando um nó principal e um nó final. Cada vez que o nó é adicionado, o nó é anexado ao nó final. Quando é adicionado, a ordem de inserção foi mantida. Estrutura de lista vinculada.

3.1.2: Visita em ordem

LinkedHashMap fornece acesso unilateral, ou seja, o acesso é realizado do início ao fim na ordem de inserção e não pode ser acessado em ambas as direções, como LinkedList.

Acessamos principalmente através do iterador. Quando o iterador é inicializado, ele é acessado do nó principal por padrão. Durante a iteração, é suficiente acessar continuamente o nó posterior do nó atual.

O mapa fornece um método iterativo para chave, valor e entidade (nó). Assumindo que precisamos iterar a entidade, podemos usar LinkedHashMap.entrySet (). Iterator () para retornar diretamente LinkedHashIterator, LinkedHashIterator é um iterador, chamamos O método nextNode do iterador pode obter o próximo nó. O código-fonte do iterador é o seguinte:

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

Ao adicionar novos nós, já mantemos a ordem de inserção entre os elementos, então o acesso iterativo é muito simples, basta acessar continuamente o próximo nó do nó atual.

3.2: Acesso à estratégia de menos exclusão

3.2.1: Caixa

Essa estratégia também é chamada de LRU (menos usado recentemente, menos usado recentemente), o que significa que os elementos acessados ​​com frequência serão anexados ao final da equipe, de modo que os dados não acessados ​​com frequência estarão naturalmente próximos ao chefe da equipe, e então podemos definir a estratégia de exclusão , Por exemplo, quando o número de elementos do mapa é maior do que quantos, exclua o nó principal, escrevemos uma demonstração para demonstrar.

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

}

O resultado impresso é o seguinte: Como
LRU
você pode ver, quando o mapa é inicializado, colocamos quatro elementos, mas o resultado são apenas três elementos, 10 está faltando, principalmente porque sobrescrevemos o método removeEldestEntry, percebemos que se Quando o número de elementos no mapa é maior que 3, excluímos o elemento na cabeça da equipe. Quando put (1, 1) é executado, o 10 na cabeça da equipe é excluído. Isso reflete que quando a estratégia de exclusão que definimos for alcançada, Exclua automaticamente o nó principal.

Quando chamamos o método map.get (9), o elemento 9 é movido para o final da fila, e quando o método map.get (20) é chamado, o elemento 20 é movido para o final da fila. Isso reflete que o nó frequentemente visitado será movido para a fila. rabo.

Este exemplo é uma boa ilustração da estratégia de exclusão de menor acesso. A seguir, vamos examinar o princípio.

3.2.2: O elemento é transferido para o final da linha

Vamos primeiro dar uma olhada em por que o elemento será movido para o final da fila ao obter:

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

A partir do código-fonte acima, pode-se ver que o nó de acesso atual é movido para o final da fila por meio do método afterNodeAccess. Não é apenas o método get, mas também quando os métodos getOrDefault, compute, computeIfAbsent, computeIfPresent e merge são executados. Os nós visitados com frequência movem-se para o final da equipe, de modo que os nós próximos ao chefe da equipe são naturalmente os elementos raramente visitados.

3.2.3: Excluir estratégia

No caso acima, quando executamos o método put, descobrimos que o elemento head foi excluído. LinkedHashMap em si não é implementado pelo método put. Em vez disso, o método put de HashMap é chamado, mas LinkedHashMap implementa o método afterNodeInsertion no método put, que é implementado desta forma Para excluir, vamos dar uma olhada no código-fonte:

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

Quatro: Resumo

Acima, falamos principalmente sobre a estrutura de dados de TreeMap e LinkedHashMap e analisamos o código-fonte do conteúdo principal de ambos. Descobrimos que ambos fazem uso total das características da estrutura de dados subjacente. TreeMap usa as características das árvores vermelhas e pretas para classificar. LinkedHashMap simplesmente adiciona uma estrutura de lista vinculada com base no HashMap para formar a ordem dos nós, o que é muito inteligente.

Acho que você gosta

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