JavaコレクションLinkedListのソースコードの分析

JavaコレクションLinkedListのソースコードの分析


Baiyu IT Haha LinkedListもArrayListのようなListインターフェイスを実装していますが、リンクリストに基づいているため、操作の挿入と削除の際にArrayListよりも効率的です。リンクされたリストに基づいて、ランダムアクセスの点でArrayListよりも少し劣っていると判断します。

さらに、LinkedListは、スタック、キュー、およびキューとして使用できるいくつかのメソッドも提供します。これらのメソッドの一部は、特定のコンテキストでこれらの名前をより適切にするために互いに異なる名前です。

まず、LinkedListクラスの定義を見てください。


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

LinkedListは、AbstractSequenceListを継承し、ListインターフェイスとDequeインターフェイスを実装します。実際、AbstractSequenceListはListインターフェイスを実装しており、ここでListをマークする方が明確です。AbstractSequenceListは、Listインターフェイスのバックボーン実装を提供して、Listインターフェイスの実装の複雑さを軽減します。Dequeインターフェースは、dequeの操作を定義します。

LinkedListには、次の2つの属性が定義されています。


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

サイズは、LinkedListオブジェクトに格納されている要素の数である必要があります。LinkedListはリンクリストに基づいて実装されるため、このヘッダーはリンクリストのヘッドノードである必要があり、Entryはノードオブジェクトです。以下は、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;
     }
 }

保存された要素、前の要素、次の要素のみが定義されます。これは、二重にリンクされたリストのノードの定義です。各ノードは、前のノードと次のノードのみを認識します。
LinkedListの構築方法を見てください。


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

LinkedListは、2つの構築メソッドを提供します。最初の構築方法はパラメータを受け入れませんが、ヘッダーノードの前のノードと次のノードの両方をそれ自体に設定します(これは双方向の循環リンクリストであることに注意してください。循環リンクリストでない場合は、空のリンクリストの場合はヘッダーノードの前にある必要があります1つのノードと次のノードは両方ともnull)であるため、リンクリスト全体には、実際には空のリンクリストを表すために使用されるヘッダーノードのみがあります。2番目の構築メソッドはCollectionパラメーターcを受け取り、最初の構築メソッドを呼び出して空のリンクリストを構築し、次にaddAllを介してcのすべての要素をリンクリストに追加します。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;
}

構築メソッドでは、addAll(Collection <?extends E> c)メソッドが呼び出され、addAll(Collection <?extends E> c)メソッドでは、サイズのみがaddAll(int index、Collection <?extends)を呼び出すためのインデックスパラメーターとして使用されます。 E> c)方法。


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

上記のコードのコメントと双方向の循環リンクリストの知識を組み合わせると、LinkedListの構築方法に関連するコンテンツを簡単に理解できるはずです。LinkedListの他のメソッドの分析を始めましょう。

追加(Eおよび)


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

上記のコードからわかるように、add(E e)メソッドはaddBefore(E e、Entry <E> entry)メソッドを呼び出すだけで、trueを返します。

addBefore(E e、Entry <E> entry)


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

addBefore(E e、Entry <E> entry)メソッドはプライベートメソッドであるため、外部プログラムから呼び出すことはできません(もちろん、これは一般的な状況であり、リフレクションを介して呼び出すことができます)。

addBefore(E e、Entry <E> entry)は、最初にEntry構築メソッドを介してeのノードnewEntryを作成します(次のノードをentryに設定し、前のノードをentry.previousに設定する操作を含みます。これは、newEntryの変更と同等です。 「ポインタ」)、次に、newEntryの前のノードの次の参照と、挿入位置の後の次のノードの前の参照を変更して、リンクされたリストノード間の参照関係が正しいままになるようにします。次に、サイズを変更してmodCountを記録し、新しく挿入されたノードに戻ります。

要約すると、addBefore(E e、Entry <E> entry)は、エントリの前にeによって構築された新しいノードの挿入を実装します。そして、add(E e)は、ヘッダーノードの前にeによって構築された新しいノードを挿入することを実現します。

add(int index、E e)


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

addBefore(E e、Entry <E> entry)メソッドも呼び出されますが、エントリノードはindexの値によって決定されます。

構築メソッド、addAll(Collection <?extends E> c)、add(E e)、addBefor(E e、Entry <E> entry)メソッドは、リンクリストを構築し、指定された位置にノードを挿入できます。理解を容易にするために、挿入を以下に示します。ノードの概略図。
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);
}

上記の図をaddBefore(E e、Entry <E> entry)メソッドと組み合わせると、addFrist(E e)をヘッダー要素の次の要素の前、つまり図の番号1の前に挿入するだけでよいことが簡単に理解できます。addLast(E e)は、ヘッダーノードの前にのみ実装する必要があります(循環リンクリストであるため、ヘッダーの前のノードがリンクリストの最後のノードであるため)ノードを挿入します(挿入後は2番目のノードの後に​​あります)。

晴れ()


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

上記のコードのコメントは、このコードのロジックを説明するのに十分です。言及されている「ポインタ」は概念的な類推にすぎないことに注意してください。Javaには「ポインタ」の概念はなく、参照のみがあります。理解を容易にするために、 「ポインタ」が使用されていることを説明します。

contains(オブジェクトo)


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

リンクリストでoのインデックスを判断するだけです。まず、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)は、oリンクリスト内のノードの要素がoと等しいかどうかを判断し、等しい場合はリンクリスト内のノードのインデックス位置を返し、存在しない場合は-1を返します。

contains(Object o)メソッドは、indexOf(Object o)メソッドによって返される値が-1であるかどうかを判断することにより、オブジェクトoがリンクリストに含まれているかどうかを判断します。

素子()


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

getFirst()


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

element()メソッドはgetFirst()を呼び出して、リンクリストの最初のノードの要素を返します。名前がラップされているかのように、同じ機能を持つ2つのメソッドを提供する必要があるのはなぜですか?実際、これは、さまざまなコンテキストでより適切なメソッド名を呼び出すことができるようにするためだけのものです。

get(int index)


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

get(int index)メソッドは、指定されたインデックス位置にあるノードの要素を取得するために使用されます。entry(int index)メソッドを介してノードを取得します。entry(int index)メソッドは、リンクされたリストをトラバースしてノードを取得します。これは上記で説明されているため、再度説明することはありません。

set(int index、E element)


public E set(int index、E element){
Entry <E> e = entry(index);
E oldVal = e.element;
e.element =要素;
oldValを返します。
}

最初に指定されたインデックスのノードを取得し、次に元の要素を保持し、次にそれを要素に置き換えてから、元の要素を返します。

getLast()


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

getLast()メソッドはgetFirst()メソッドに似ていますが、ヘッダーノードの前のノードの要素を取得する点が異なります。循環リンクリストであるため、ヘッダーノードの前のノードがリンクリストの最後のノードになります。

lastIndexOf(Object 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;

}

検索は最後のインデックス、つまり最後に発生した位置であるため、後方トラバーサル方式が採用されています。バックツーフォワードトラバーサルが使用されるため、インデックスにサイズが割り当てられ、ループ本体の実行時に減算操作が実行されます。それが存在するかどうかを判断する2つのケース、つまりnullとnullではない場合があります。

オファー(E e)


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

リンクリストの最後に要素を挿入します。

オファーファースト(E e)


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

リンクリストの先頭に要素を挿入します。
オファーラスト(E e)


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

リンクリストの最後に要素を挿入します。
上記の3つのメソッドは、対応するaddメソッドを呼び出すだけでなく、さまざまなコンテキストで使用されるさまざまなメソッド名を提供するだけです。

ピーク()


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

上記の3つのメソッドも非常に単純で、対応するgetメソッドを呼び出すだけです。

poll()


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

ポーリング関連のメソッドは、要素を取得および削除することです。これらはすべて、削除操作に関連しています。

ポップ()


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

プッシュ(Eおよび)


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

これらの2つのメソッドは、スタックの操作に対応します。つまり、removeFirst()メソッドとaddFirst()メソッドを呼び出すだけで、要素をポップしてプッシュします。
以下は、関連する操作を削除する方法に焦点を当てています。

削除する()


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

いくつかのremoveメソッドは、最終的にプライベートメソッドremove(Entry <E> e)を呼び出します。これは、単純な論理的な違いです。次の分析は、(Entry <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;
}

クローン()


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

親クラスのclone()メソッドを呼び出して、オブジェクトリンクリストクローンを初期化し、クローンを空の双方向循環リンクリストに構築してから、ヘッダーの次のノードを1つずつクローンに追加し始めます。最後に、クローンオブジェクトが返されます。

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

LinkedListと同じサイズの配列結果を作成し、リンクリストをトラバースし、各ノードの要素要素を配列にコピーして、配列を返します。

toArray(T [] a)

JavaコレクションLinkedListのソースコードの分析

まず、着信アレイと発信アレイaのサイズが十分かどうかを判断し、サイズが十分でない場合は拡張します。ここでは起動メソッドが使用され、サイズの配列が再インスタンス化されます。次に、配列aを配列結果に割り当て、リンクリストから結果に追加された要素をトラバースします。最後に、配列aの長さがsizeより大きいかどうかが判断され、大きい場合は、size位置の内容がnullに設定されます。を返します。

コードから、配列aの長さがサイズ以下の場合、aのすべての要素がカバーされ、拡張スペースに格納されているコンテンツがnullであることがわかります。配列aの長さの長さがサイズより大きい場合、0からサイズ-位置1のコンテンツは上書きされ、サイズの要素はnullに設定され、サイズの後の要素は変更されません。

配列aを直接操作し、結果配列にaを割り当てた後、結果配列を操作してみませんか?Entryに加えて、
LinkedListのIterator
には内部クラスListItrがあります。
ListItrはListIteratorインターフェースを実装しており、LinkedListをトラバースおよび変更できるイテレーターであることがわかります。
ListItrオブジェクトを取得するメソッドは、LinkedList:listIterator(int index)で提供されています。


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

このメソッドは、単にListItrオブジェクトを返します。
LinkedListへの統合によって取得されたlistIterator()メソッドもあります。このメソッドは、listIterator(int index)を呼び出し、0を渡します。
以下でListItrを詳しく分析してみましょう。

JavaコレクションLinkedListのソースコードの分析
JavaコレクションLinkedListのソースコードの分析

JavaコレクションLinkedListのソースコードの分析
JavaコレクションLinkedListのソースコードの分析
以下は、ListItrの使用例です。
JavaコレクションLinkedListのソースコードの分析
結果:
JavaコレクションLinkedListのソースコードの分析

LinkedListには、Iteratorを提供するメソッドdescendingIterator()もあります。このメソッドは、DescendingIteratorオブジェクトを返します。DescendingIteratorは、LinkedListの内部クラスです。


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

次の分析では、DescendingIteratorクラスを詳細に分析します。
JavaコレクションLinkedListのソースコードの分析

クラス名と上記のコードから、これは逆イテレーターであることがわかります。コードは非常に単純で、すべてListItrクラスのメソッドと呼ばれます。

おすすめ

転載: blog.51cto.com/15061944/2593723