Análisis de código fuente HashSet, TreeSet

Análisis de código fuente HashSet, TreeSet

Las dos clases HashSet y TreeSet se ensamblan sobre la base de Map. El enfoque de nuestro estudio es cómo Set utiliza las funciones existentes de Map para lograr sus propios objetivos, es decir, cómo basarse en las Innove la función y luego vea si vale la pena aprender algunos pequeños detalles del cambio.

一 : HashSet

1.1, anotación de clase HashSet

Mirando el código fuente, primero debemos mirar los comentarios de la clase. Echemos un vistazo a lo que dicen los comentarios de la clase. Algunas capturas de pantalla se muestran en la siguiente figura: Los
Inserte la descripción de la imagen aquí
comentarios de la clase hablan principalmente sobre los siguientes cuatro puntos

  1. La implementación subyacente se basa en HashMap, por lo que no se garantiza que se repita en el orden de inserción u otro orden durante la iteración;
  2. El lento rendimiento de agregar, eliminar, contener, tamaño y otros métodos no aumentará con el aumento de la cantidad de datos. Esto está principalmente relacionado con la estructura de datos de matriz subyacente del HashMap, independientemente de la cantidad de datos, independientemente del conflicto de hash. , La complejidad del tiempo es O (1) *;
  3. Si el hilo no es seguro, ciérrelo usted mismo si necesita seguridad o use Collections.synchronizedSet;
  4. Durante el proceso de iteración, si se cambia la estructura de datos, fallará rápidamente y se lanzará ConcurrentModificationException;

También hemos visto las anotaciones de clase de List y Map antes, y descubrimos que los puntos 2, 3 y 4 se mencionan en las anotaciones de clase. Entonces, si alguien pregunta acerca de las similitudes de List, Map y Set, entonces puede decir 2. , 3, 4 tres puntos.

1.2 ¿Cómo combina HashSet HashMap?

Acabo de ver en la anotación de clase 1 que la implementación de HashSet se basa en HashMap. En Java, hay dos formas de implementar una implementación innovadora basada en clases básicas:

  1. Heredar la clase básica y anular el método de la clase básica, por ejemplo, heredar el HashMap y anular el método add;
  2. Combine las clases básicas y reutilice las capacidades de las clases básicas llamando a los métodos de las clases básicas;

HashSet utiliza un HashMap combinado y sus ventajas son las siguientes:

  1. La herencia significa que las clases principal y secundaria son lo mismo, y Set and Map originalmente quería expresar dos cosas, por lo que la herencia no es apropiada y las restricciones de sintaxis de Java, la clase secundaria solo puede heredar una clase principal y la expansión posterior es difícil.
  2. La combinación es más flexible, las clases básicas existentes se pueden combinar arbitrariamente y los métodos se pueden ampliar y organizar sobre la base de los métodos de las clases básicas, y los nombres de los métodos se pueden nombrar arbitrariamente, sin la necesidad de ser coherentes con los nombres de los métodos de las clases básicas;

En nuestro trabajo, si encontramos problemas similares, nuestro principio es utilizar la composición tanto como sea posible y utilizar menos herencia.

La combinación es tratar HashMap como una de sus propias variables locales. A continuación, se muestra la implementación combinada de HashSet:

// 把 HashMap 组合进来,key 是 Hashset 的 key,value 是下面的 PRESENT
private transient HashMap<E,Object> map;
// HashMap 中的 value
private static final Object PRESENT = new Object();

De estas dos líneas de código, podemos ver dos cosas:

  1. Cuando usamos HashSet, como el método add, solo hay un parámetro de entrada, pero el método add del Map combinado tiene dos parámetros de entrada, clave y valor. La clave correspondiente al Map es el parámetro de entrada de nuestro add, y el valor es la segunda línea. PRESENTE en el código, el diseño aquí es muy inteligente, reemplace el valor del mapa con un valor predeterminado PRESENTE;
  2. Si se comparte HashSet, habrá problemas de seguridad de subprocesos cuando varios subprocesos accedan a él, porque en todas las operaciones posteriores, no hay bloqueo;

Cuando se implementa HashSet basado en HashMap, primero elija el método de combinación y luego use el valor predeterminado para reemplazar el valor del valor en el mapa. El diseño es muy inteligente, brinda al usuario una buena experiencia y es simple y conveniente de usar. También puede aprender de esta idea en su trabajo, puede envolver las complejas implementaciones subyacentes, algunas implementaciones predeterminadas las puede comer usted mismo, de modo que la interfaz de escupir sea lo más simple y fácil de usar posible.

1.2.1, inicialización

La inicialización de HashSet es relativamente simple, solo use el nuevo HashMap directamente. Lo que es más interesante es que cuando se inicializan los datos de la colección original, se calculará la capacidad inicial de HashMap. El código fuente es el siguiente:

// 对 HashMap 的容量进行了计算
public HashSet(Collection<? extends E> c) {
    
    
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}

En cuanto a los otros métodos de HashSet, es relativamente simple, es decir, algunos paquetes de la api de Map, se implementa el siguiente método de adición:

public boolean add(E e) {
    
    
    // 直接使用 HashMap 的 put 方法,进行一些简单的逻辑判断
    return map.put(e, PRESENT)==null;
}

Desde el método add, podemos ver los beneficios de la combinación. Los parámetros de entrada, el nombre y el valor de retorno del método se pueden personalizar. Si se hereda, no funcionará.

1.2.2 Resumen

La implementación específica de HashSet merece nuestra referencia principalmente de la siguiente manera, cuando normalmente escribimos código, podemos referirnos a él:

  1. Análisis y comprensión de combinación o herencia;
  2. Realice algunos paquetes de lógica compleja para hacer que la interfaz que escupe sea lo más simple y fácil de usar posible;
  3. Al combinar otras api, intente aprender más sobre la api combinada, para que pueda utilizar mejor la api;

Dos: TreeSet

La estructura general de TreeSet es similar a HashSet. La combinación subyacente es TreeMap, por lo que hereda la función de clasificación de claves de TreeMap. Al iterar, también puede iterar de acuerdo con el orden de clasificación de las claves. Principalmente, nos fijamos en la reutilización de TreeMap. Dos ideas:

2.1, la idea de reutilizar TreeMap

Escenario 1: método add de TreeSet, veamos su código fuente:

public boolean add(E e) {
    
    
    return m.put(e, PRESENT)==null;
}

Como puede ver, la capa inferior usa directamente la capacidad put de HashMap, solo úsela directamente.

2.2, la segunda idea de reutilizar TreeMap

Escenario 2: Necesidad de iterar los elementos en el TreeSet, debería ser como agregar, usar directamente la capacidad iterativa existente de HashMap, por ejemplo, como el siguiente:

// 模仿思路一的方式实现
public Iterator<E> descendingIterator() {
    
    
    // 直接使用 HashMap.keySet 的迭代能力
    return m.keySet().iterator();
}

Esta es la realización de la idea uno. TreeSet combina TreeMap y selecciona directamente las capacidades subyacentes de TreeMap para el empaquetado, pero la implementación real de TreeSet es completamente opuesta. Veamos el código fuente:

// NavigableSet 接口,定义了迭代的一些规范,和一些取值的特殊方法
// TreeSet 实现了该方法,也就是说 TreeSet 本身已经定义了迭代的规范
public interface NavigableSet<E> extends SortedSet<E> {
    
    
    Iterator<E> iterator();
    E lower(E e);
}  
// m.navigableKeySet() 是 TreeMap 写了一个子类实现了 NavigableSet
// 接口,实现了 TreeSet 定义的迭代规范
public Iterator<E> iterator() {
    
    
    return m.navigableKeySet().iterator();
}

El código fuente de implementación de la interfaz NavigableSet en TreeMap es el siguiente:

// TreeMap 为了满足 Set 的功能,实现了 Set 定义的 NavigableSet 的接口
static final class KeySet<E> extends AbstractSet<E> implements NavigableSet<E> {
    
    
    private final NavigableMap<E, ?> m;
    KeySet(NavigableMap<E,?> map) {
    
     m = map; }

    public Iterator<E> iterator() {
    
    
        if (m instanceof TreeMap)
            return ((TreeMap<E,?>)m).keyIterator();
        else
            return ((TreeMap.NavigableSubMap<E,?>)m).keyIterator();
    }

    public Iterator<E> descendingIterator() {
    
    
        if (m instanceof TreeMap)
            return ((TreeMap<E,?>)m).descendingKeyIterator();
        else
            return ((TreeMap.NavigableSubMap<E,?>)m).descendingKeyIterator();
    }

	// 这些都是 NavigableSet 接口里面的方法
    public int size() {
    
     return m.size(); }
    public boolean isEmpty() {
    
     return m.isEmpty(); }
    public boolean contains(Object o) {
    
     return m.containsKey(o); }
    public void clear() {
    
     m.clear(); }
    public E lower(E e) {
    
     return m.lowerKey(e); }
    public E floor(E e) {
    
     return m.floorKey(e); }
    public E ceiling(E e) {
    
     return m.ceilingKey(e); }
    public E higher(E e) {
    
     return m.higherKey(e); }
    public E first() {
    
     return m.firstKey(); }
    public E last() {
    
     return m.lastKey(); }
    public Comparator<? super E> comparator() {
    
     return m.comparator(); }
    public E pollFirst() {
    
    
        Map.Entry<E,?> e = m.pollFirstEntry();
        return (e == null) ? null : e.getKey();
    }
    public E pollLast() {
    
    
        Map.Entry<E,?> e = m.pollLastEntry();
        return (e == null) ? null : e.getKey();
    }
    public boolean remove(Object o) {
    
    
        int oldSize = size();
        m.remove(o);
        return size() != oldSize;
    }
    public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                                  E toElement,   boolean toInclusive) {
    
    
        return new KeySet<>(m.subMap(fromElement, fromInclusive,
                                      toElement,   toInclusive));
    }
    public NavigableSet<E> headSet(E toElement, boolean inclusive) {
    
    
        return new KeySet<>(m.headMap(toElement, inclusive));
    }
    public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
    
    
        return new KeySet<>(m.tailMap(fromElement, inclusive));
    }
    public SortedSet<E> subSet(E fromElement, E toElement) {
    
    
        return subSet(fromElement, true, toElement, false);
    }
    public SortedSet<E> headSet(E toElement) {
    
    
        return headSet(toElement, false);
    }
    public SortedSet<E> tailSet(E fromElement) {
    
    
        return tailSet(fromElement, true);
    }
    public NavigableSet<E> descendingSet() {
    
    
        return new KeySet<>(m.descendingMap());
    }

    public Spliterator<E> spliterator() {
    
    
        return keySpliteratorFor(m);
    }
}

Desde el código fuente de TreeMap, podemos ver que TreeMap implementa varios métodos especiales definidos por TreeSet.

Podemos ver que esta idea es que TreeSet define la especificación de la interfaz y TreeMap se encarga de la implementación, la idea de realización y la idea son opuestas.

Resumimos las dos ideas para implementar TreeSet y TreeMap:

  1. TreeSet usa directamente algunas funciones de TreeMap y se envuelve en una nueva API;
  2. TreeSet define la API que desea, define la especificación de la interfaz usted mismo y permite que TreeMap la implemente;

La relación de llamada de los esquemas 1 y 2 es que TreeSet llama a TreeMap, pero la relación de realización de la función es completamente opuesta. La primera es que la definición y realización de la función están en TreeMap, y TreeSet es solo una llamada simple. El segundo TreeSet define la interfaz. Después de eso, deje que TreeMap implemente la lógica interna, TreeSet es responsable de la definición de la interfaz y TreeMap es responsable de la implementación específica. En este caso, debido a que la interfaz está definida por TreeSet, la implementación debe ser lo que más desea TreeSet. TreeSet ni siquiera necesita ajustarse, y usted puede devolver el valor directamente. Puedes vomitarlo.

Pensemos en las razones de estas dos ideas de reutilización:

  1. Para métodos simples como agregar, usamos directamente la Idea 1, principalmente porque los métodos de agregar son relativamente simples de implementar sin una lógica complicada, por lo que TreeSet es relativamente simple de implementar por sí mismo;
  2. La Idea 2 es adecuada principalmente para escenarios complejos, como escenarios iterativos. TreeSet tiene escenarios complejos. Por ejemplo, necesita poder iterar desde cero, por ejemplo, para poder tomar el primer valor, por ejemplo, para poder tomar el último valor, además de que la estructura subyacente de TreeMap es más complicada, TreeSet Es posible que no esté clara la lógica compleja subyacente de TreeMap. En este momento, si TreeSet se utiliza para implementar una lógica de escena tan compleja, TreeSet no podrá manejarla. Es mejor dejar que TreeSet defina la interfaz y dejar que TreeMap sea responsable de la implementación. TreeMap es muy claro sobre la estructura compleja subyacente. Es preciso y sencillo de implementar;

2.3 Resumen

Las dos ideas de reutilización diferentes de TreeSet y TreeMap son muy importantes. A menudo se encuentran en el trabajo, especialmente la segunda idea, como la llamada de generalización de dubbo, la inversión de dependencia en DDD, etc., los principios son el segundo tipo de TreeSet La reutilización de ideas.

Tres: Resumen

Vale la pena aprender la comprensión profunda y el diseño de HashSet del valor umbral de la expansión combinada de la clase HashMap. Vale la pena aprender las dos ideas de reutilización de TreeSet para TreeMap, especialmente la segunda idea de reutilización.

Supongo que te gusta

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