Heim >Web-Frontend >js-Tutorial >Analyse der Java-Collection-Traversal-Methode (Implementierungsprinzip, Algorithmusleistung, anwendbare Anlässe)_Javascript-Kenntnisse
Übersicht
Die Java-Sprache bietet eine Reihe von Datenerfassungs-Frameworks, die einige abstrakte Datentypen wie List und Set definieren. Jeder abstrakte Datentyp verfügt über spezifische Implementierungen, und die unterste Ebene verwendet unterschiedliche Implementierungsmethoden wie ArrayList und LinkedList.
Darüber hinaus bietet Java auch verschiedene Möglichkeiten zum Durchlaufen von Datensammlungen. Entwickler müssen die Merkmale, anwendbaren Situationen und Leistung jeder Traversal-Methode bei verschiedenen zugrunde liegenden Implementierungen klar verstehen. Lassen Sie uns diesen Inhalt im Folgenden im Detail analysieren.
Wie werden Datenelemente im Speicher gespeichert?
Datenelemente werden im Speicher gespeichert, und es gibt zwei Hauptspeichermethoden:
1. Sequentielle Speicherung, Direktzugriff:
Auf diese Weise werden benachbarte Datenelemente in benachbarten Speicheradressen gespeichert und die gesamte Speicheradresse ist kontinuierlich. Die Speicheradresse kann anhand der Position des Elements direkt berechnet und direkt gelesen werden. Die durchschnittliche zeitliche Komplexität beim Lesen eines Elements an einer bestimmten Position beträgt O(1). Normalerweise verfügen nur auf Arrays basierende Sammlungen über diese Funktion. Java wird durch ArrayList dargestellt.
2. Kettenspeicher, sequenzieller Zugriff:
Auf diese Weise muss sich nicht jedes Datenelement an einer benachbarten Position im Speicher befinden. Jedes Datenelement enthält die Speicheradresse seines nächsten Elements. Die Speicheradresse kann nicht direkt anhand der Position der Elemente berechnet werden, die Elemente können nur nacheinander gelesen werden. Die durchschnittliche zeitliche Komplexität des Lesens eines Elements an einer bestimmten Position beträgt O(n). Hauptsächlich dargestellt durch verknüpfte Listen.
Dargestellt durch LinkedList in Java.
Welche Traversalmethoden werden in Java bereitgestellt?
1. Traditionell für Schleifendurchquerung, basierend auf dem Zähler:
Der Traverser verwaltet einen Zähler außerhalb der Sammlung, liest dann die Elemente an jeder Position der Reihe nach und stoppt, wenn das letzte Element gelesen wurde. Die Hauptsache ist, Elemente entsprechend ihrer Position zu lesen. Dies ist auch die primitivste Sammlungsdurchlaufmethode.
wird geschrieben als:
for (int i = 0; i < list.size(); i++) { list.get(i); }
2. Iteratordurchquerung, Iterator:
Iterator ist ursprünglich ein Entwurfsmuster von OO. Sein Hauptzweck besteht darin, die Eigenschaften verschiedener Datensammlungen abzuschirmen und die Schnittstelle zum Durchlaufen von Sammlungen zu vereinheitlichen. Als OO-Sprache unterstützt Java selbstverständlich den Iterator-Modus in Sammlungen.
wird geschrieben als:
Iterator iterator = list.iterator(); while (iterator.hasNext()) { iterator.next(); }
3. Foreach-Schleifendurchlauf:
Shields deklarierte explizit Iteratoren und Zähler.
Vorteile: Der Code ist prägnant und weniger fehleranfällig.
Nachteile: Es kann nur eine einfache Durchquerung durchgeführt werden und die Datenerfassung während des Durchquerungsprozesses kann nicht durchgeführt (löschen, ersetzen) werden.
wird geschrieben als:
for (ElementType element : list) { }
Was ist das Implementierungsprinzip jeder Traversalmethode?
1. Traditionell für Schleifendurchquerung, basierend auf dem Zähler:
Der Traverser verwaltet einen Zähler außerhalb der Sammlung, liest dann die Elemente an jeder Position der Reihe nach und stoppt, wenn das letzte Element gelesen wurde. Die Hauptsache ist, Elemente entsprechend ihrer Position zu lesen.
2. Iteratordurchquerung, Iterator:
Jede speziell implementierte Datenerfassung muss im Allgemeinen einen entsprechenden Iterator bereitstellen. Im Vergleich zur herkömmlichen for-Schleife eliminiert Iterator den expliziten Durchlaufzähler. Daher kann ein Iterator, der auf sequentiell gespeicherten Sammlungen basiert, direkt auf Daten nach Position zugreifen. Die normale Implementierung von Iterator basierend auf verketteten Speichersammlungen erfordert das Speichern der aktuell durchlaufenen Position. Bewegen Sie dann den Zeiger basierend auf der aktuellen Position vorwärts oder rückwärts.
3. Foreach-Schleifendurchlauf:
Anhand des dekompilierten Bytecodes kann festgestellt werden, dass foreach auch intern mit Iterator implementiert wird, der Java-Compiler diese Codes jedoch für uns generiert.
Wie hoch ist die Leistung jeder Traversalmethode für verschiedene Speichermethoden?
1. Traditionell für Schleifendurchquerung, basierend auf dem Zähler:
Da es auf der Position des Elements basiert, wird es nach Position gelesen. Wir können also wissen, dass für die sequentielle Speicherung die durchschnittliche zeitliche Komplexität des Lesens eines Elements an einer bestimmten Position O(1) und die durchschnittliche zeitliche Komplexität des Durchlaufens der gesamten Sammlung O(n) beträgt. Da bei verketteter Speicherung die durchschnittliche zeitliche Komplexität des Lesens eines Elements an einem bestimmten Ort O(n) beträgt, beträgt die durchschnittliche zeitliche Komplexität des Durchlaufens der gesamten Sammlung O(n2) (n im Quadrat).
Code zum Lesen der ArrayList nach Position: direkt nach Elementposition lesen.
transient Object[] elementData; public E get(int index) { rangeCheck(index); return elementData(index); } E elementData(int index) { return (E) elementData[index]; }
Code zum Lesen von LinkedList nach Position: Jedes Mal müssen Sie ab dem 0. Element rückwärts lesen. Tatsächlich wurden auch intern kleine Optimierungen vorgenommen.
transient int size = 0; transient Node<E> first; transient Node<E> last; public E get(int index) { checkElementIndex(index); return node(index).item; } Node<E> node(int index) { if (index < (size >> 1)) { //查询位置在链表前半部分,从链表头开始查找 Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { //查询位置在链表后半部分,从链表尾开始查找 Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
2、迭代器遍历,Iterator:
那么对于RandomAccess类型的集合来说,没有太多意义,反而因为一些额外的操作,还会增加额外的运行时间。但是对于Sequential Access的集合来说,就有很重大的意义了,因为Iterator内部维护了当前遍历的位置,所以每次遍历,读取下一个位置并不需要从集合的第一个元素开始查找,只要把指针向后移一位就行了,这样一来,遍历整个集合的时间复杂度就降低为O(n);
(这里只用LinkedList做例子)LinkedList的迭代器,内部实现,就是维护当前遍历的位置,然后操作指针移动就可以了:
代码:
public E next() { checkForComodification(); if (!hasNext()) throw new NoSuchElementException(); lastReturned = next; next = next.next; nextIndex++; return lastReturned.item; } public E previous() { checkForComodification(); if (!hasPrevious()) throw new NoSuchElementException(); lastReturned = next = (next == null) ? last : next.prev; nextIndex--; return lastReturned.item; }
3、foreach循环遍历:
分析Java字节码可知,foreach内部实现原理,也是通过Iterator实现的,只不过这个Iterator是Java编译器帮我们生成的,所以我们不需要再手动去编写。但是因为每次都要做类型转换检查,所以花费的时间比Iterator略长。时间复杂度和Iterator一样。
使用Iterator的字节码:
Code: new # // class java/util/ArrayList dup invokespecial # // Method java/util/ArrayList."<init>":()V astore_ aload_ invokeinterface #, // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator; astore_ goto aload_ invokeinterface #, // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object; pop aload_ invokeinterface #, // InterfaceMethod java/util/Iterator.hasNext:()Z ifne return
使用foreach的字节码:
Code: new # // class java/util/ArrayList dup invokespecial # // Method java/util/ArrayList."<init>":()V astore_ aload_ invokeinterface #, // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator; astore_ goto aload_ invokeinterface #, // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object; checkcast # // class loop/Model astore_ aload_ invokeinterface #, // InterfaceMethod java/util/Iterator.hasNext:()Z ifne return
各遍历方式的适用于什么场合?
1、传统的for循环遍历,基于计数器的:
顺序存储:读取性能比较高。适用于遍历顺序存储集合。
链式存储:时间复杂度太大,不适用于遍历链式存储的集合。
2、迭代器遍历,Iterator:
顺序存储:如果不是太在意时间,推荐选择此方式,毕竟代码更加简洁,也防止了Off-By-One的问题。
链式存储:意义就重大了,平均时间复杂度降为O(n),还是挺诱人的,所以推荐此种遍历方式。
3、foreach循环遍历:
foreach只是让代码更加简洁了,但是他有一些缺点,就是遍历过程中不能操作数据集合(删除等),所以有些场合不使用。而且它本身就是基于Iterator实现的,但是由于类型转换的问题,所以会比直接使用Iterator慢一点,但是还好,时间复杂度都是一样的。所以怎么选择,参考上面两种方式,做一个折中的选择。
Java的最佳实践是什么?
Java数据集合框架中,提供了一个RandomAccess接口,该接口没有方法,只是一个标记。通常被List接口的实现使用,用来标记该List的实现是否支持Random Access。
一个数据集合实现了该接口,就意味着它支持Random Access,按位置读取元素的平均时间复杂度为O(1)。比如ArrayList。
而没有实现该接口的,就表示不支持Random Access。比如LinkedList。
所以看来JDK开发者也是注意到这个问题的,那么推荐的做法就是,如果想要遍历一个List,那么先判断是否支持Random Access,也就是 list instanceof RandomAccess。
比如:
if (list instanceof RandomAccess) { //使用传统的for循环遍历。 } else { //使用Iterator或者foreach。 }
以上所述是小编给大家介绍的Java遍历集合方法分析(实现原理、算法性能、适用场合),希望对大家有所帮助!