Análisis del código fuente de la colección Java LinkedList

Análisis del código fuente de la colección Java LinkedList

Baiyu IT Haha
LinkedList también implementa la interfaz List como ArrayList, pero es más eficiente que ArrayList al insertar y eliminar operaciones, ya que se basa en una lista vinculada. Según la lista enlazada, también determina que es un poco inferior a ArrayList en términos de acceso aleatorio.

Además, LinkedList también proporciona algunos métodos que se pueden usar como pilas, colas y deques. Algunos de estos métodos son solo nombres que se diferencian entre sí para que estos nombres sean más apropiados en un contexto específico.

Primero mire la definición de la clase LinkedList.


public class LinkedList<E>
   extends AbstractSequentialList<E>
   implements List<E>, Deque<E>, Cloneable, java.io.Serializable

LinkedList hereda de AbstractSequenceList e implementa las interfaces List y Deque. De hecho, AbstractSequenceList ha implementado la interfaz de Lista, y es más claro marcar la Lista aquí. AbstractSequenceList proporciona una implementación principal de la interfaz List para reducir la complejidad de implementar la interfaz List. La interfaz Deque define el funcionamiento del deque.

En LinkedList se definen dos atributos:


private transient Entry<E> header = new Entry<E>(null, null, null);
private transient int size = 0;

El tamaño debe ser el número de elementos almacenados en el objeto LinkedList. Dado que LinkedList se implementa en función de una lista vinculada, este encabezado debe ser el nodo principal de la lista vinculada y Entry es el objeto de nodo. El siguiente es el código de la clase Entry.


private static class Entry<E> {
     E element;
     Entry<E> next;
     Entry<E> previous; 
     Entry(E element, Entry<E> next, Entry<E> previous) {
         this.element = element;
         this.next = next;
         this.previous = previous;
     }
 }

Sólo se definen el elemento almacenado, el elemento anterior y el siguiente elemento, esta es la definición de los nodos de la lista doblemente enlazada, cada nodo sólo conoce su nodo anterior y el siguiente.
Mira el método de construcción de LinkedList.


public LinkedList() {
     header.next = header.previous = header;
}
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

LinkedList proporciona dos métodos de construcción. El primer método de construcción no acepta parámetros, pero establece tanto el nodo anterior como el siguiente nodo del nodo de encabezado para sí mismo (tenga en cuenta que esta es una lista enlazada circular de dos vías, si no es una lista enlazada circular, el caso de una lista enlazada vacía debe estar antes del nodo de encabezado Un nodo y el siguiente nodo son ambos nulos), por lo que toda la lista vinculada en realidad solo tiene un nodo de encabezado, que se utiliza para representar una lista vinculada vacía. El segundo método de construcción recibe un parámetro de Colección c, llama al primer método de construcción para construir una lista vinculada vacía y luego agrega todos los elementos en c a la lista vinculada a través de addAll. Mira el contenido de addAll.


public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}
 // index参数指定collection中插入的第一个元素的位置
public boolean addAll(int index, Collection<? extends E> c) {
    // 插入位置超过了链表的长度或小于0,报IndexOutOfBoundsException异常
    if (index < 0 || index > size)
        throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+size);
    Object[] a = c.toArray();
    int numNew = a.length;
    // 若需要插入的节点个数为0则返回false,表示没有插入元素
    if (numNew==0)
        return false;
    modCount++;
    // 保存index处的节点。插入位置如果是size,则在头结点前面插入,否则获取index处的节点
    Entry<E> successor = (index==size ? header : entry(index));
    // 获取前一个节点,插入时需要修改这个节点的next引用
    Entry<E> predecessor = successor.previous;
    // 按顺序将a数组中的第一个元素插入到index处,将之后的元素插在这个元素后面
    for (int i=0; i<numNew; i++) {
    // 结合Entry的构造方法,这条语句是插入操作,相当于C语言中链表中插入节点并修改指针
        Entry<E> e = new Entry<E>((E)a[i], successor, predecessor);
        // 插入节点后将前一节点的next指向当前节点,相当于修改前一节点的next指针
        predecessor.next = e;
        // 相当于C语言中成功插入元素后将指针向后移动一个位置以实现循环的功能
        predecessor = e;
}
    // 插入元素前index处的元素链接到插入的Collection的最后一个节点
    successor.previous = predecessor;
   // 修改size
    size += numNew;
    return true;
}

En el método de construcción, se llama al método addAll (Collection <? Extiende E> c), y en el método addAll (Collection <? Extiende E> c), solo se usa el tamaño como parámetro de índice para llamar a addAll (int index, Collection <? Extiende E> c) Método.


private Entry<E> entry(int index) {
      if (index < 0 || index >= size)
           throw new IndexOutOfBoundsException("Index: "+index+
                                               ", Size: "+size);
        Entry<E> e = header;
        // 根据这个判断决定从哪个方向遍历这个链表
       if (index < (size >> 1)) {
           for (int i = 0; i <= index; i++)
               e = e.next;
       } else {
           // 可以通过header节点向前遍历,说明这个一个循环双向链表,header的previous指向链表的最后一个节点,这也验证了构造方法中对于header节点的前后节点均指向自己的解释
           for (int i = size; i > index; i--)
                e = e.previous;
        }
         return e;
    }

Combinando los comentarios en el código anterior y el conocimiento de la lista enlazada circular bidireccional, debería ser fácil entender el contenido involucrado en el método de construcción LinkedList. Empecemos analizando otros métodos de LinkedList.

agregar (E y)


public boolean add(E e) {
   addBefore(e, header);
   return true;
}

Como se puede ver en el código anterior, el método add (E e) simplemente llama al método addBefore (E e, Entry <E> entry) y devuelve verdadero.

addBefore (E e, Entrada <E> entrada)


private Entry<E> addBefore(E e, Entry<E> entry) {
   Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
   newEntry.previous.next = newEntry;
   newEntry.next.previous = newEntry;
   size++;
   modCount++;
   return newEntry;
}

El método addBefore (E e, Entry <E> entry) es un método privado, por lo que no se puede llamar desde un programa externo (por supuesto, esta es una situación general, aún puede llamarlo mediante reflexión).

addBefore (E e, Entry <E> entry) primero crea el nodo newEntry de e mediante el método de construcción Entry (incluida la operación de establecer su siguiente nodo en entry y el nodo anterior en entry.previous, que es equivalente a modificar newEntry "Pointer"), y luego modifique la siguiente referencia del nodo anterior de newEntry y la referencia anterior del siguiente nodo después de la posición de inserción, para que la relación de referencia entre los nodos de la lista enlazada permanezca correcta. Luego modifique el tamaño y registre el modCount, y luego regrese al nodo recién insertado.

En resumen, addBefore (E e, Entrada <E> entrada) implementa la inserción de un nuevo nodo construido por e antes de la entrada. Y add (E e) se da cuenta de insertar un nuevo nodo construido por e antes del nodo de encabezado.

añadir (índice int, E e)


public void add(int index, E element) {
    addBefore(element, (index==size ? header : entry(index)));
}

También se llama al método addBefore (E e, Entry <E> entry), pero el nodo de entrada está determinado por el valor de index.

Método de construcción, los métodos addAll (Collection <? Extiende E> c), add (E e), addBefor (E e, Entrada <E> entrada) pueden construir una lista vinculada e insertar un nodo en una posición específica. Para facilitar la comprensión, la inserción se da a continuación Diagrama esquemático del nodo.
Análisis del código fuente de la colección Java LinkedList
addFirst (E e)


public void addFirst(E e) {
   addBefore(e, header.next);
}

addLast (E e)


public void addLast(E e) {
    addBefore(e, header);
}

Mirando el diagrama anterior, combinado con el método addBefore (E e, Entrada <E> entrada), es fácil entender que addFrist (E e) solo necesita insertarse antes del siguiente elemento del elemento de encabezado, es decir, antes del número uno en el diagrama. addLast (E e) solo necesita implementarse antes del nodo del encabezado (porque es una lista enlazada circular, por lo que el nodo anterior del encabezado es el último nodo de la lista enlazada) inserte el nodo (después de la inserción es después del segundo nodo).

claro()


public void clear() {
 Entry<E> e = header.next;
 // e可以理解为一个移动的“指针”,因为是循环链表,所以回到header的时候说明已经没有节点了
 while (e != header) {
     // 保留e的下一个节点的引用
         Entry<E> next = e.next;
         // 接触节点e对前后节点的引用
         e.next = e.previous = null;
         // 将节点e的内容置空
         e.element = null;
         // 将e移动到下一个节点
         e = next;
 }
 // 将header构造成一个循环链表,同构造方法构造一个空的LinkedList
 header.next = header.previous = header;
 // 修改size
     size = 0;
     modCount++;
 }

Los comentarios en el código anterior son suficientes para explicar la lógica de este código. Cabe señalar que el "puntero" mencionado es solo una analogía conceptual. Java no tiene el concepto de "puntero", sino solo referencias. Para facilitar la comprensión, la parte Explique que se utilizan "punteros".

contiene (Objeto o)


public boolean contains(Object o) {
    return indexOf(o) != -1;
}

Simplemente juzgue el índice de o en la lista vinculada. Primero observe el método indexOf (Object o).


public int indexOf(Object o) {
    int index = 0;
    if (o==null) {
        for (Entry e = header.next; e != header; e = e.next) {
            if (e.element==null)
               return index;
           index++;
        }
    } else {
        for (Entry e = header.next; e != header; e = e.next) {
             if (o.equals(e.element))
                return index;
            index++;
         }
    }
    return -1;
}

indexOf (Object o) juzga si el elemento del nodo en la o lista vinculada es igual a o, si son iguales, devuelve la posición de índice del nodo en la lista vinculada, y si no existe, devuelve -1.

El método contains (Object o) juzga si el objeto o está contenido en la lista vinculada al juzgar si el valor devuelto por el método indexOf (Object o) es -1.

elemento()


public E element() {
    return getFirst();
}

getFirst ()


public E getFirst() {
    if (size==0)
        throw new NoSuchElementException();
    return header.next.element;
}

El método element () llama a getFirst () para devolver el elemento del primer nodo de la lista vinculada. ¿Por qué necesita proporcionar dos métodos con la misma función, como si el nombre estuviera envuelto? De hecho, esto es solo para poder llamar a nombres de métodos más apropiados en diferentes contextos.

obtener (índice int)


public E get(int index) {
   return entry(index).element;
}

El método get (int index) se utiliza para obtener el elemento del nodo en la posición de índice especificada. Obtiene el nodo a través del método de entrada (índice int). El método de entrada (int index) atraviesa la lista enlazada y obtiene el nodo, se explicó anteriormente y no se volverá a indicar.

set (índice int, elemento E)


conjunto E público (índice int, elemento E) {
Entrada <E> e = entrada (índice);
E oldVal = e.element;
e.element = elemento;
return oldVal;
}

Primero obtenga el nodo del índice especificado, luego conserve el elemento original, luego reemplácelo con el elemento y luego devuelva el elemento original.

obtener ultimo()


public E getLast()  {
   if (size==0)
       throw new NoSuchElementException();
    return header.previous.element;
}

El método getLast () es similar al método getFirst (), excepto que obtiene el elemento del nodo anterior del nodo de encabezado. Debido a que es una lista vinculada circular, el nodo anterior del nodo de encabezado es el último nodo de la lista vinculada.

lastIndexOf (Objeto o)


public int lastIndexOf(Object o) {
    int index = size;
    if (o==null) {
        for (Entry e = header.previous; e != header; e = e.previous) {
           index--;
           if (e.element==null)
               return index;
       }
    } else {
        for (Entry e = header.previous; e != header; e = e.previous) {
            index--;
           if (o.equals(e.element))
                return index;
        }
    }
    return -1;

}

Dado que la búsqueda es el último índice, es decir, la posición de la última aparición, se adopta un método de recorrido hacia atrás. Debido a que se usa el recorrido de ida y vuelta, al índice se le asigna el tamaño y la operación de resta se realiza cuando se ejecuta el cuerpo del bucle. Hay dos casos para determinar si existe, a saber, nulo y no nulo.

oferta (E e)


public boolean offer(E e) {
    return add(e);
}

Inserte un elemento al final de la lista vinculada.

oferta primero (E e)


public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}

Inserte un elemento al principio de la lista vinculada.
ofertaÚltima (E e)


public boolean offerLast(E e) {
    addLast(e);
    return true;
}

Inserte un elemento al final de la lista vinculada.
Los tres métodos anteriores simplemente llaman al método add correspondiente y también proporcionan diferentes nombres de métodos para usar en diferentes contextos.

ojeada()


public E peek() {
    if (size==0)
        return null;
    return getFirst();
}

peekFirst ()


public E peekFirst() {
    if (size==0)
        return null;
    return getFirst();
}

peekLast ()


public E peekLast() {
    if (size==0)
        return null;
    return getLast();
}

Los tres métodos anteriores también son muy simples, simplemente llame al método get correspondiente.

encuesta()


public E poll() {
    if (size==0)
        return null;
    return removeFirst();
}

pollFirst ()


public E pollFirst() {
    if (size==0)
        return null;
    return removeFirst();
}

pollLast ()


public E pollLast() {
    if (size==0)
        return null;
    return removeLast();
}

Los métodos relacionados con la encuesta son obtener y eliminar un elemento. Todos están relacionados con la operación de eliminación.

popular()


public E pop() {
    return removeFirst();
}

empujar (E y)


public void push(E e) {
    addFirst(e);
}

Estos dos métodos corresponden a la operación de la pila, es decir, sacar un elemento y empujar un elemento, simplemente llamando a los métodos removeFirst () y addFirst ().
Lo siguiente se centra en el método de eliminar operaciones relacionadas.

eliminar()


public E remove() {
    return removeFirst();
}

**remove(int index)**

public E remove(int index) {
    return remove(entry(index));
}

**remove(Object o)**

public boolean remove(Object o) {
    if (o==null) {
        for (Entry<E> e = header.next; e != header; e = e.next) {
            if (e.element==null) {
                remove(e);
                return true;
            }
        }
    } else {
        for (Entry<E> e = header.next; e != header; e = e.next) {
            if (o.equals(e.element)) {
                remove(e);
                return true;
            }
        }
    }
    return false;
}

**removeFirst()**

public E removeFirst() {
    return remove(header.next);
}

**removeLast()**

public E removeLast() {
    return remove(header.previous);
}

**removeFirstOccurrence()**

public boolean removeFirstOccurrence(Object o) {
   return remove(o);
}

**removeLastOccurence()**

public boolean removeLastOccurrence(Object o) {
    if (o==null) {
        for (Entry<E> e = header.previous; e != header; e = e.previous) {
            if (e.element==null) {
                remove(e);
                return true;
            }
        }
    } else {
        for (Entry<E> e = header.previous; e != header; e = e.previous) {
            if (o.equals(e.element)) {
                remove(e);
                return true;
            }
        }
    }
    return false;
}

Varios métodos remove eventualmente llaman a un método privado: remove (Entrada <E> e), que es solo una simple diferencia lógica. El siguiente análisis elimina el método (Entrada <E> e).


private E remove(Entry<E> e) {
    if (e == header)
        throw new NoSuchElementException();
    // 保留将被移除的节点e的内容
 E result = e.element;
 // 将前一节点的next引用赋值为e的下一节点
     e.previous.next = e.next;
     // 将e的下一节点的previous赋值为e的上一节点
     e.next.previous = e.previous;
     // 上面两条语句的执行已经导致了无法在链表中访问到e节点,而下面解除了e节点对前后节点的引用
 e.next = e.previous = null;
 // 将被移除的节点的内容设为null
 e.element = null;
 // 修改size大小
     size--;
     modCount++;
     // 返回移除节点e的内容
     return result;
}

clon()


public Object clone() {
    LinkedList<E> clone = null;
    try {
        clone = (LinkedList<E>) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new InternalError();
    }
     clone.header = new Entry<E>(null, null, null);
     clone.header.next = clone.header.previous = clone.header;
     clone.size = 0;
     clone.modCount = 0;
     for (Entry<E> e = header.next; e != header; e = e.next)
         clone.add(e.element);
     return clone;
 }

Llame al método clone () de la clase principal para inicializar el clon de la lista vinculada de objetos, construya el clon en una lista vinculada circular bidireccional vacía y luego comience a agregar el siguiente nodo del encabezado al clon uno por uno. Finalmente, se devuelve el objeto clonado.

toArray ()


public Object[] toArray() {
    Object[] result = new Object[size];
    int i = 0;
    for (Entry<E> e = header.next; e != header; e = e.next)
        result[i++] = e.element;
     return result;
 }

Cree un resultado de matriz con el mismo tamaño que la LinkedList, recorra la lista vinculada, copie el elemento de cada nodo en la matriz y devuelva la matriz.

toArray (T [] a)

Análisis del código fuente de la colección Java LinkedList

Primero juzgue si el tamaño de la matriz entrante y saliente a es suficiente y amplíe si el tamaño no es suficiente. Aquí se utiliza el método de lanzamiento y se vuelve a crear una instancia de una matriz de tamaño. Luego asigne la matriz a al resultado de la matriz y recorra los elementos agregados al resultado de la lista vinculada. Finalmente, se juzga si la longitud de la matriz a es mayor que el tamaño y, si es mayor, el contenido de la posición del tamaño se establece en nulo. Regresar a.

Se puede ver en el código que cuando la longitud de la matriz a es menor o igual al tamaño, todos los elementos de a están cubiertos y el contenido almacenado en el espacio expandido es nulo; si la longitud de la matriz a es mayor que el tamaño, entonces 0 a tamaño- El contenido en la posición 1 se sobrescribe, el elemento en tamaño se establece en nulo y el elemento después de tamaño permanece sin cambios.

¿Por qué no operar en la matriz a directamente y operar en la matriz de resultados después de asignar a la matriz de resultados? Además de Entry,
el Iterador de LinkedList
tiene una clase interna: ListItr.
ListItr implementa la interfaz ListIterator, se puede ver que es un iterador, a través del cual se puede atravesar y modificar LinkedList.
En LinkedList: listIterator (int index) se proporciona un método para obtener el objeto ListItr.


public ListIterator<E> listIterator(int index) {
    return new ListItr(index);
}

Este método simplemente devuelve un objeto ListItr.
También hay un método listIterator () obtenido a través de la integración en LinkedList, que simplemente llama a listIterator (int index) y pasa 0.
Analicemos ListItr en detalle a continuación.

Análisis del código fuente de la colección Java LinkedList
Análisis del código fuente de la colección Java LinkedList

Análisis del código fuente de la colección Java LinkedList
Análisis del código fuente de la colección Java LinkedList
El siguiente es un ejemplo del uso de ListItr.
Análisis del código fuente de la colección Java LinkedList
resultado:
Análisis del código fuente de la colección Java LinkedList

LinkedList también tiene un método para proporcionar Iterator: descendingIterator (). Este método devuelve un objeto DescendingIterator. DescendingIterator es una clase interna de LinkedList.


public Iterator<E> descendingIterator() {
    return new DescendingIterator();
}

El siguiente análisis analiza la clase DescendingIterator en detalle.
Análisis del código fuente de la colección Java LinkedList

Se puede ver en el nombre de la clase y en el código anterior que este es un iterador inverso, el código es muy simple, todos se denominan métodos en la clase ListItr.

Supongo que te gusta

Origin blog.51cto.com/15061944/2593723
Recomendado
Clasificación