[Java] Wenn ArrayList und LinkedList falsch verwendet werden, ist der Leistungsunterschied so groß!

Vorwort

Bei Vorstellungsgesprächen werden uns oft mehrere Fragen gestellt:

  • Ich glaube, die meisten Freunde können den Unterschied zwischen ArrayList und LinkedList beantworten:

    • ArrayList wird basierend auf Arrays implementiert, und LinkedList wird basierend auf verknüpften Listen implementiert.
    • ArrayList ist effizienter als LinkedList, wenn zufällig auf Listen usw. zugegriffen wird.
  • Auf die Frage nach den Verwendungsszenarien von ArrayList und LinkedList könnten die meisten Freunde antworten:

    • Beim Hinzufügen und Löschen von Elementen zwischen ArrayList und LinkedList ist LinkedList effizienter als ArrayList, und beim Durchlaufen ist ArrayList effizienter als LinkedList.

Ist diese Antwort korrekt? Lasst es uns heute studieren!
Analysieren Sie mehrere Fallstricke von ArrayList.subList aus der Quellcode-Perspektive.
Lassen Sie uns zunächst kurz die prinzipielle Implementierung von ArrayList und LinkedList vorstellen!

Quellcode-Analyse

Anordnungsliste

Implementierungsklasse

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList implementiert die List-Schnittstelle und erbt die abstrakte Klasse AbstractList. Die unterste Ebene wird durch ein Array implementiert und implementiert eine sich selbst erhöhende Array-Größe.

ArrayList implementiert auch die Cloneable-Schnittstelle und die Serializable-Schnittstelle, sodass Klonen und Serialisierung implementiert werden können.

ArrayList implementiert auch die RandomAccess-Schnittstelle, bei der es sich um eine Flag-Schnittstelle handelt, die darauf hinweist, dass „solange die List-Klasse, die diese Schnittstelle implementiert, einen schnellen Direktzugriff erreichen kann“.

Grundeigenschaften

Das ArrayList-Attribut besteht hauptsächlich aus der Array-Längengröße, den Elementdaten des Objektarrays, der Initialisierungskapazität default_capacity usw. Die Standardgröße der Initialisierungskapazität beträgt 10.

//默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
//对象数组
transient Object[] elementData; 
//数组长度
private int size;

Dem ArrayList-Attribut nach zu urteilen, wird elementData durch das Schlüsselwort transient geändert. Wenn Sie das Feld mit dem Schlüsselwort transient ändern, bedeutet dies, dass das Attribut nicht serialisiert wird.

ArrayList implementiert jedoch tatsächlich die Serialisierungsschnittstelle. Warum ist das so?

Da das Array von ArrayList auf dynamischer Erweiterung basiert, speichert nicht der gesamte zugewiesene Speicherplatz Daten.
Wenn die externe Serialisierungsmethode zum Serialisieren des Arrays verwendet wird, wird das gesamte Array serialisiert. Um zu verhindern, dass diese Speicherbereiche, in denen keine Daten gespeichert werden, serialisiert werden, bietet ArrayList zwei private Methoden: writeObject und readObject führt die Serialisierung und Deserialisierung selbst durch und spart so Platz und Zeit beim Serialisieren und Deserialisieren von Arrays.
Daher verhindert die Verwendung von transient zum Ändern des Arrays, dass das Objektarray durch andere externe Methoden serialisiert wird.
Die benutzerdefinierte Serialisierungsmethode von ArrayList lautet wie folgt:
Fügen Sie hier eine Bildbeschreibung ein

Initialisierung

Es gibt drei Initialisierungsmethoden: direkte Initialisierung ohne Parameter, Initialisierung mit angegebener Größe und Initialisierung mit angegebenen Anfangsdaten. Der Quellcode lautet wie folgt:
Fügen Sie hier eine Bildbeschreibung ein

Wenn ein neues Element zu ArrayList hinzugefügt wird und das gespeicherte Element seine vorhandene Größe überschreitet, berechnet es die Elementgröße und erweitert sie dann dynamisch. Die Erweiterung des Arrays führt zu einer Speicherkopie des gesamten Arrays.

Wenn wir ArrayList initialisieren, können wir daher die Anfangsgröße des Arrays sinnvollerweise über den ersten Konstruktor angeben, was dazu beiträgt, die Anzahl der Array-Erweiterungen zu reduzieren und somit die Systemleistung zu verbessern.

wichtiger Punkt:

Wenn der Parameterlose ArrayList-Konstruktor initialisiert wird, ist die Standardgröße ein leeres Array, nicht die 10, die jeder oft sagt. 10 ist der Array-Wert, der beim ersten Hinzufügen erweitert wird.

Neues Element hinzufügen

Es gibt zwei Möglichkeiten, Elemente zu ArrayList hinzuzufügen: Eine besteht darin, Elemente direkt am Ende des Arrays hinzuzufügen, und die andere darin, Elemente an einer beliebigen Position hinzuzufügen.

public boolean add(E e) {
    
    
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    public void add(int index, E element) {
    
    
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

Dasselbe zwischen den beiden Methoden besteht darin, dass vor dem Hinzufügen von Elementen zunächst die Kapazität bestätigt wird. Wenn die Kapazität groß genug ist, besteht keine Notwendigkeit, sie zu erweitern. Wenn die Kapazität nicht groß genug ist, wird sie erweitert entsprechend der 1,5-fachen Größe des ursprünglichen Arrays erweitert werden. Nach der Erweiterung muss das Array an die neu zugewiesene Speicheradresse kopiert werden.
Das Folgende ist der spezifische Quellcode:
Fügen Sie hier eine Bildbeschreibung ein

Es gibt auch Unterschiede zwischen diesen beiden Methoden. Das Hinzufügen eines Elements an einer beliebigen Position führt dazu, dass alle Elemente nach dieser Position neu angeordnet werden. Das Hinzufügen eines Elements am Ende des Arrays verursacht jedoch keine Probleme, es sei denn, es erfolgt eine Erweiterung. Sortiervorgang für Elementkopien .

Daher ist die Effizienz von ArrayList in Szenarien, in denen viele neue Elemente hinzugefügt werden, nicht unbedingt sehr langsam.

Wenn wir uns über die Größe der gespeicherten Daten während der Initialisierung im Klaren sind, können wir beim Initialisieren von ArrayList die Array-Kapazität angeben und beim Hinzufügen von Elementen nur Elemente am Ende des Arrays hinzufügen. Dann ist die Leistung von ArrayList in Szenarien schlecht, in denen a Es werden viele neue Elemente hinzugefügt. Es wird nicht schlechter, aber die Leistung ist besser als bei anderen Listensammlungen.

Element löschen

ArrayList Es gibt viele Möglichkeiten, Elemente zu löschen, z. B. das Löschen basierend auf dem Array-Index, das Löschen basierend auf dem Wert oder das Löschen in Stapeln usw. Die Prinzipien und Ideen sind ähnlich.
ArrayList muss das Array nach jedem effektiven Elementlöschvorgang neu organisieren. Je früher das gelöschte Element liegt, desto höher sind die Kosten für die Array-Reorganisation.
Wir wählen die wertbasierte Löschmethode für die Quellcodebeschreibung:
Fügen Sie hier eine Bildbeschreibung ein

Elemente durchqueren

Da ArrayList auf Array-Basis implementiert ist, können Elemente sehr schnell abgerufen werden.

public E get(int index) {
    
    
        rangeCheck(index);
        return elementData(index);
    }
    E elementData(int index) {
    
    
        return (E) elementData[index];
    }

LinkedList

LinkedList wird basierend auf der Datenstruktur der doppelt verknüpften Liste implementiert.
In dieser doppelt verknüpften Listenstruktur kann jeder Knoten in der verknüpften Liste vorwärts oder rückwärts verfolgt werden. Es gibt mehrere Konzepte wie folgt:

  • Jeder Knoten in der verknüpften Liste wird als Knoten bezeichnet. Der Knoten verfügt über ein prev-Attribut, das die Position des vorherigen Knotens darstellt, und ein next-Attribut, das die Position des nächsten Knotens darstellt.
  • Der erste ist der Kopfknoten der doppelt verknüpften Liste, und sein vorheriger Knoten ist null.
  • last ist der Endknoten der doppelt verknüpften Liste und der Knoten danach ist null;
    Wenn die verknüpfte Liste keine Daten enthält, sind first und last derselbe Knoten , und sowohl der vordere als auch der hintere Punkt sind null. ;
  • Da es sich um eine doppelt verknüpfte Liste handelt, gibt es keine Größenbeschränkung, solange der Maschinenspeicher leistungsstark genug ist.

Die Knotenstruktur besteht aus drei Teilen: Elementinhaltselement, vorderer Zeiger prev und hinterer Zeiger next. Der Code lautet wie folgt.

private static class Node<E> {
    
    
    E item;// 节点值
    Node<E> next; // 指向的下一个节点
    Node<E> prev; // 指向的前一个节点
    // 初始化参数顺序分别是:前一个节点、本身节点值、后一个节点
    Node(Node<E> prev, E element, Node<E> next) {
    
    
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

LinkedList ist eine doppelt verknüpfte Liste, die durch Knotenstrukturobjekte verbunden ist.

Implementierungsklasse

Die LinkedList-Klasse implementiert die List-Schnittstelle und die Deque-Schnittstelle und erbt die abstrakte Klasse AbstractSequentialList. LinkedList implementiert die Merkmale sowohl des List-Typs als auch des Queue-Typs; LinkedList implementiert auch die Cloneable- und Serializable-Schnittstellen. Wie ArrayList Es kann Klonen und Serialisieren implementiert werden.
Da die Speicheradressen, an denen LinkedList Daten speichert, diskontinuierlich sind und diskontinuierliche Adressen über Zeiger lokalisiert werden, unterstützt LinkedList keinen wahlfreien Schnellzugriff und LinkedList kann die RandomAccess-Schnittstelle nicht implementieren.

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

Grundeigenschaften

transient int size = 0;
transient Node first;
transient Node last;

Wir können sehen, dass diese drei Attribute durch transient geändert wurden. Der Grund ist sehr einfach: Bei der Serialisierung serialisieren wir nicht nur Kopf und Ende, sodass LinkedList auch readObject und writeObject für die Serialisierung und Deserialisierung implementiert.
Das Folgende ist die benutzerdefinierte Serialisierungsmethode von LinkedList.
Fügen Sie hier eine Bildbeschreibung ein

Knotenabfrage

Das Abfragen eines Knotens in einer verknüpften Liste ist relativ langsam und erfordert eine Schleife, um einen nach dem anderen zu durchsuchen. Schauen wir uns an, wie der Quellcode von LinkedList nach Knoten sucht:
Fügen Sie hier eine Bildbeschreibung ein

LinkedList verwendet nicht den Ansatz einer Schleife vom Anfang bis zum Ende, sondern folgt einer einfachen Dichotomie. Schauen Sie sich zunächst an, ob sich der Index in der ersten oder zweiten Hälfte der verknüpften Liste befindet.
Wenn es sich um die erste Hälfte handelt, suchen Sie von Anfang an und umgekehrt. Dadurch wird die Anzahl der Schleifen um mindestens die Hälfte reduziert und die Suchleistung verbessert.

Neues Element hinzufügen

Die Implementierung des Hinzufügens von Elementen zu LinkedList ist sehr einfach, es gibt jedoch viele Möglichkeiten, Elemente hinzuzufügen.
Die standardmäßige Add-Methode (Ee) besteht darin, das hinzugefügte Element am Ende der Warteschlange hinzuzufügen. Zuerst wird das letzte Element in eine temporäre Variable ersetzt, ein neues Node-Knotenobjekt generiert und dann zeigt den letzten Verweis auf Das neue Knotenobjekt, der vorherige Zeiger des vorherigen letzten Objekts zeigt auf das neue Knotenobjekt.
Fügen Sie hier eine Bildbeschreibung ein

LinkedList verfügt auch über eine Methode zum Hinzufügen von Elementen an einer beliebigen Position. Wenn wir ein Element an der mittleren Position von zwei beliebigen Elementen hinzufügen, ändert der Vorgang zum Hinzufügen von Elementen nur die vorderen und hinteren Zeiger des vorherigen und nächsten Elements. Die Zeiger zeigen auf die hinzugefügten neuen Elemente, also im Vergleich zu ArrayList Für den Add-Vorgang bietet LinkedList offensichtliche Leistungsvorteile.
Fügen Sie hier eine Bildbeschreibung ein

Element löschen

Bei der Operation zum Löschen von Elementen in LinkedList müssen wir zunächst das zu löschende Element durch Schleifen finden. Wenn sich die zu löschende Position in der ersten Hälfte der Liste befindet, suchen Sie von vorne nach hinten; wenn ja Position ist in der zweiten Hälfte, dann suchen Sie von hinten nach vorne.
Auf diese Weise ist es sehr effizient, ob Sie die früheren oder späteren Elemente löschen möchten. Wenn die Liste jedoch eine große Anzahl von Elementen enthält und sich die entfernten Elemente in der Mitte der Liste befinden , dann Der Wirkungsgrad wird relativ gering sein.

Elemente durchqueren

Die Elementerfassungsoperation von LinkedList ähnelt im Wesentlichen der Elementlöschoperation von LinkedList. Die entsprechenden Elemente werden durch Durchlaufen der vorderen und hinteren Hälfte gefunden. Das Abfragen von Elementen auf diese Weise ist jedoch sehr ineffizient, insbesondere im Fall von for Bei der Schleifendurchquerung wird jedes Mal die Hälfte der Liste durchlaufen.
Wenn wir also die LinkedList in einer Schleife durchlaufen, können wir den Iteratormodus verwenden, um die Schleife zu iterieren und unsere Elemente direkt abzurufen, ohne durch die Schleife nach der Liste suchen zu müssen.
Analysetest
Neuer Elementoperationsleistungstest
Testfall-Quellcode:

ArrayList:paste.ubuntu.com/p/gktBvjgMG…
LinkedList:paste.ubuntu.com/p/3jQrY2XMP…

Analysetest

Fügen Sie hier eine Bildbeschreibung ein
Testergebnisse
Fügen Sie hier eine Bildbeschreibung ein

Die Operation benötigt Zeit, um ein Element von der Kopfposition der Sammlung (ArrayList) hinzuzufügen 550, um ein Element von der Kopfposition der Sammlung (LinkedList) hinzuzufügen 34, um ein Element von der Mittelposition der Sammlung hinzuzufügen (ArrayList) 32 zum Hinzufügen eines Elements von der mittleren Position der Sammlung (LinkedList) ) 58746 Hinzufügen von Elementen von der Endposition der Sammlung (ArrayList) 29 Hinzufügen von Elementen von der Endposition der Sammlung (LinkedList) 31
Durch diese Reihe von Tests können wir feststellen, dass die Effizienz des Hinzufügens von Elementen zu LinkedList nicht unbedingt höher ist als die von ArrayList.

Element aus Sammlungskopfposition hinzufügen

Da ArrayList als Array implementiert ist, müssen beim Hinzufügen von Elementen zum Kopf des Arrays die Daten nach dem Kopf kopiert und neu angeordnet werden, sodass die Effizienz sehr gering ist;
LinkedList basiert auf der Implementierung einer verknüpften Liste. Beim Hinzufügen eines Elements wird die Position des hinzugefügten Elements zunächst durch eine Schleife ermittelt. Wenn sich die hinzuzufügende Position in der ersten Hälfte der Liste befindet, wird von vorne nach hinten gesucht. Wenn sich die Position in der zweiten Hälfte befindet, suchen Sie von hinten. Suchen Sie vorher, sodass das Hinzufügen von Elementen zum Kopf von LinkedList sehr effizient ist.

Fügen Sie ein Element aus der Mitte der Sammlung hinzu

Wenn ArrayList Elemente in der Mitte des Arrays hinzufügt, müssen auch einige Daten kopiert und neu angeordnet werden, und die Effizienz ist nicht sehr hoch.
LinkedList fügt Elemente in der mittleren Position hinzu Dies ist der niedrigste Wert zum Hinzufügen von Elementen. Effizient, da nahe der Mittelposition die Schleifensuche vor dem Hinzufügen von Elementen die Operation ist, die die meisten Elemente durchläuft.

Fügen Sie Elemente vom Ende der Sammlung hinzu

Beim Hinzufügen von Elementen zum Schwanz ohne Erweiterung ist die Effizienz von ArrayList höher als die von LinkedList.
Dies liegt daran, dass ArrayList beim Hinzufügen von Elementen am Ende keine Daten kopieren und neu anordnen muss, was sehr effizient ist.
Obwohl LinkedList keine Schleife durchlaufen muss, um Elemente zu finden, verfügt LinkedList über neue Objekte und den Prozess, Zeiger so zu ändern, dass sie auf Objekte zeigen, sodass die Effizienz geringer ist als bei ArrayList.

Hinweis: Dies ist ein Test, der ohne dynamische Erweiterung der Array-Kapazität durchgeführt wird. Bei einer dynamischen Erweiterung wird auch die Effizienz von ArrayList verringert.

Leistungstest zum Löschen von Elementoperationen
Die Ergebnisse des ArrayList- und LinkedList-Löschelement-Operationstests kommen den Ergebnissen des Element-Hinzufügen-Operationstests sehr nahe!
Fazit: Wenn Sie am Kopf der Liste eine große Anzahl von Einfüge- und Löschvorgängen ausführen müssen, wählen Sie direkt LinkedList. Andernfalls reicht ArrayList aus.
Leistungstest der Traversal-Element-Operation
Quellcode des Testfalls:

Fügen Sie hier eine Bildbeschreibung ein

Testergebnisse:

Fügen Sie hier eine Bildbeschreibung ein

Die Operation benötigt Zeit für Schleife (ArrayList) 3 für Schleife (LinkedList) 17557 Iteratorschleife (ArrayList) 4 Iteratorschleife (LinkedList) 4
Wir können sehen, dass die for-Schleife von Die Leistung der LinkedList-Schleife ist am schlechtesten, während die Leistung der For-Schleife von ArrayList am besten ist.
Dies liegt daran, dass LinkedList basierend auf einer verknüpften Liste implementiert wird. Bei Verwendung einer for-Schleife durchläuft jede for-Schleife die Hälfte der Liste, was die Effizienz des Durchlaufs erheblich beeinträchtigt. ArrayList wird basierend implementiert auf einem Array. und implementiert das Schnittstellenflag RandomAccess, was bedeutet, dass ArrayList einen schnellen Direktzugriff erreichen kann, sodass die Effizienz der for-Schleife sehr hoch ist.
Die iterative Schleifendurchlaufleistung von LinkedList ähnelt der von ArrayList und ist nicht schlecht. Daher sollten wir beim Durchlaufen von LinkedList die Verwendung von for-Schleifendurchlauf vermeiden.

Guess you like

Origin blog.csdn.net/u011397981/article/details/134483514