.toArray (neue MyClass [0]) oder .toArray (neue MyClass [myList.size ()])?


176

Angenommen, ich habe eine ArrayList

ArrayList<MyClass> myList;

Und ich möchte bei Array anrufen, gibt es einen Leistungsgrund zu verwenden

MyClass[] arr = myList.toArray(new MyClass[myList.size()]);

Über

MyClass[] arr = myList.toArray(new MyClass[0]);

?

Ich bevorzuge den zweiten Stil, da er weniger ausführlich ist, und ich nahm an, dass der Compiler sicherstellen wird, dass das leere Array nicht wirklich erstellt wird, aber ich habe mich gefragt, ob das stimmt.

Natürlich macht es in 99% der Fälle keinen Unterschied auf die eine oder andere Weise, aber ich möchte einen konsistenten Stil zwischen meinem normalen Code und meinen optimierten inneren Schleifen beibehalten ...


6
Es sieht so aus, als ob die Frage jetzt in einem neuen Blog-Beitrag von Aleksey Shipilёv, Arrays of Wisdom of the Ancients, geklärt wurde !
glitzert

6
Aus dem Blog-Beitrag: "Fazit: toArray (neues T [0]) scheint schneller, sicherer und vertraglich sauberer zu sein und sollte daher jetzt die Standardauswahl sein."
DavidS

Antworten:


109

Gegenintuitiv ist die schnellste Version auf Hotspot 8:

MyClass[] arr = myList.toArray(new MyClass[0]);

Ich habe einen Mikro-Benchmark mit jmh durchgeführt. Die Ergebnisse und der Code sind unten aufgeführt. Dies zeigt, dass die Version mit einem leeren Array die Version mit einem vordefinierten Array durchweg übertrifft. Beachten Sie, dass das Ergebnis möglicherweise anders ausfällt, wenn Sie ein vorhandenes Array mit der richtigen Größe wiederverwenden können.

Benchmark-Ergebnisse (Punktzahl in Mikrosekunden, kleiner = besser):

Benchmark                      (n)  Mode  Samples    Score   Error  Units
c.a.p.SO29378922.preSize         1  avgt       30    0.025  0.001  us/op
c.a.p.SO29378922.preSize       100  avgt       30    0.155  0.004  us/op
c.a.p.SO29378922.preSize      1000  avgt       30    1.512  0.031  us/op
c.a.p.SO29378922.preSize      5000  avgt       30    6.884  0.130  us/op
c.a.p.SO29378922.preSize     10000  avgt       30   13.147  0.199  us/op
c.a.p.SO29378922.preSize    100000  avgt       30  159.977  5.292  us/op
c.a.p.SO29378922.resize          1  avgt       30    0.019  0.000  us/op
c.a.p.SO29378922.resize        100  avgt       30    0.133  0.003  us/op
c.a.p.SO29378922.resize       1000  avgt       30    1.075  0.022  us/op
c.a.p.SO29378922.resize       5000  avgt       30    5.318  0.121  us/op
c.a.p.SO29378922.resize      10000  avgt       30   10.652  0.227  us/op
c.a.p.SO29378922.resize     100000  avgt       30  139.692  8.957  us/op

Als Referenz der Code:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
public class SO29378922 {
  @Param({"1", "100", "1000", "5000", "10000", "100000"}) int n;
  private final List<Integer> list = new ArrayList<>();
  @Setup public void populateList() {
    for (int i = 0; i < n; i++) list.add(0);
  }
  @Benchmark public Integer[] preSize() {
    return list.toArray(new Integer[n]);
  }
  @Benchmark public Integer[] resize() {
    return list.toArray(new Integer[0]);
  }
}

Ähnliche Ergebnisse, vollständige Analysen und Diskussionen finden Sie im Blog-Beitrag Arrays of Wisdom of the Ancients . Zusammenfassend lässt sich sagen, dass der JVM- und JIT-Compiler mehrere Optimierungen enthält, mit denen er kostengünstig ein neues Array mit der richtigen Größe erstellen und initialisieren kann. Diese Optimierungen können nicht verwendet werden, wenn Sie das Array selbst erstellen.


2
Sehr interessanter Kommentar. Ich bin überrascht, dass niemand dies kommentiert hat. Ich denke, das liegt daran, dass es den anderen Antworten hier in Bezug auf die Geschwindigkeit widerspricht. Interessant ist auch, dass der Ruf dieses Mannes fast höher ist als alle anderen Antworten (er) zusammen.
Pimp Trizkit

Ich schweife ab. Ich würde auch gerne Benchmarks sehen, für MyClass[] arr = myList.stream().toArray(MyClass[]::new);die ich denke, dass sie langsamer sind. Außerdem würde ich gerne Benchmarks für den Unterschied zur Array-Deklaration sehen. Wie im Unterschied zwischen: MyClass[] arr = new MyClass[myList.size()]; arr = myList.toArray(arr);und MyClass[] arr = myList.toArray(new MyClass[myList.size()]);... oder sollte es keinen Unterschied geben? Ich denke, diese beiden sind ein Problem, das außerhalb der toArrayFunktionsereignisse liegt. Aber hey! Ich hätte nicht gedacht, dass ich etwas über die anderen komplizierten Unterschiede lernen würde.
Pimp Trizkit

1
@PimpTrizkit Nur überprüft: Die Verwendung einer zusätzlichen Variablen macht erwartungsgemäß keinen Unterschied. Die Verwendung eines Streams dauert zwischen 60% und 100% länger als der toArraydirekte Aufruf (je kleiner die Größe,
desto

Wow, das war eine schnelle Antwort! Vielen Dank! Ja, das habe ich vermutet. Die Konvertierung in einen Stream klang nicht effizient. Aber du weißt nie!
Pimp Trizkit


122

Ab ArrayList in Java 5 wird das Array bereits gefüllt, wenn es die richtige Größe hat (oder größer ist). Folglich

MyClass[] arr = myList.toArray(new MyClass[myList.size()]);

erstellt ein Array-Objekt, füllt es und gibt es an "arr" zurück. Andererseits

MyClass[] arr = myList.toArray(new MyClass[0]);

erstellt zwei Arrays. Das zweite ist ein Array von MyClass mit der Länge 0. Es wird also ein Objekt für ein Objekt erstellt, das sofort weggeworfen wird. Soweit der Quellcode dies vorschlägt, kann der Compiler / JIT diesen nicht so optimieren, dass er nicht erstellt wird. Darüber hinaus führt die Verwendung des Objekts mit der Länge Null zu Castings innerhalb der toArray () - Methode.

Siehe die Quelle von ArrayList.toArray ():

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

Verwenden Sie die erste Methode, damit nur ein Objekt erstellt wird, und vermeiden Sie (implizite, aber dennoch teure) Gussteile.


1
Zwei Kommentare könnten für jemanden von Interesse sein: 1) LinkedList.toArray (T [] a) ist noch langsamer (verwendet Reflection: Array.newInstance) und komplexer; 2) Andererseits war ich in der JDK7-Version sehr überrascht, dass das normalerweise schmerzhaft langsame Array.newInstance fast so schnell wie die übliche Array-Erstellung ist!
java.is.for.desktop

1
@ktaria size ist ein privates Mitglied von ArrayList und gibt **** Überraschung **** die Größe an. Siehe ArrayList SourceCode
MyPasswordIsLasercats

3
Das Erraten der Leistung ohne Benchmarks funktioniert nur in trivialen Fällen. Eigentlich new Myclass[0]ist schneller: shipilev.net/blog/2016/arrays-wisdom-ancients
Karol S

Dies ist keine gültige Antwort mehr ab JDK6 +
Антонов

28

Von JetBrains Intellij Idea Inspection:

Es gibt zwei Stile, um eine Sammlung in ein Array zu konvertieren: entweder mithilfe eines vorgroßen Arrays (wie c.toArray (neuer String [c.size ()]) ) oder mithilfe eines leeren Arrays (wie c.toArray (neuer String [) 0]) .

In älteren Java-Versionen wurde die Verwendung eines Arrays mit vorgefertigter Größe empfohlen, da der Reflection-Aufruf, der zum Erstellen eines Arrays mit der richtigen Größe erforderlich ist, recht langsam war. Seit den späten Updates von OpenJDK 6 war dieser Aufruf jedoch nicht mehr gültig, sodass die Leistung der leeren Array-Version im Vergleich zur vorgroßen Version gleich und manchmal sogar noch besser war. Das Übergeben eines Arrays mit vorgroßer Größe ist für eine gleichzeitige oder synchronisierte Sammlung gefährlich, da ein Datenrennen zwischen der Größe und dem Aufruf von toArray möglich ist , was zu zusätzlichen Nullen am Ende des Arrays führen kann, wenn die Sammlung während des Vorgangs gleichzeitig verkleinert wurde.

Diese Überprüfung ermöglicht es, dem einheitlichen Stil zu folgen: entweder mit einem leeren Array (was in modernem Java empfohlen wird) oder mit einem Array mit Vorgröße (das in älteren Java-Versionen oder nicht auf HotSpot basierenden JVMs möglicherweise schneller ist).


Wenn all dies kopierter / zitierter Text ist, können wir ihn dann entsprechend formatieren und auch einen Link zur Quelle bereitstellen? Ich bin wegen der IntelliJ-Inspektion hierher gekommen und ich bin sehr interessiert an dem Link, über den alle Inspektionen und die Gründe dafür nachgeschlagen werden können.
Tim Büthe



17

Moderne JVMs optimieren in diesem Fall die Konstruktion reflektierender Arrays, sodass der Leistungsunterschied gering ist. Es ist keine gute Idee, die Sammlung zweimal in einem solchen Code zu benennen, daher würde ich die erste Methode vermeiden. Ein weiterer Vorteil der zweiten ist, dass sie mit synchronisierten und gleichzeitigen Sammlungen funktioniert. Wenn Sie eine Optimierung vornehmen möchten, verwenden Sie das leere Array erneut (leere Arrays sind unveränderlich und können gemeinsam genutzt werden) oder verwenden Sie einen Profiler (!).


2
Upvoting "Wiederverwendung des leeren Arrays", da ein Kompromiss zwischen Lesbarkeit und potenzieller Leistung in Betracht gezogen werden sollte. Das Übergeben eines deklarierten Arguments private static final MyClass[] EMPTY_MY_CLASS_ARRAY = new MyClass[0]verhindert nicht, dass das zurückgegebene Array durch Reflektion erstellt wird, verhindert jedoch, dass jedes Mal ein zusätzliches Array erstellt wird.
Michael Scheper

Machael hat recht, wenn Sie ein Array mit der Länge Null verwenden, führt kein Weg daran vorbei: (T []) java.lang.reflect.Array.newInstance (a.getClass (). GetComponentType (), size); Das wäre überflüssig, wenn die Größe> = actualSize (JDK7) wäre
Alex

Wenn Sie ein Zitat für "moderne JVMs optimieren in diesem Fall die Konstruktion reflektierender Arrays optimieren" geben können, werde ich diese Antwort gerne positiv bewerten.
Tom Panning

Ich lerne hier. Wenn ich stattdessen verwende: MyClass[] arr = myList.stream().toArray(MyClass[]::new);Würde es bei synchronisierten und gleichzeitigen Sammlungen helfen oder schaden? Und warum? Bitte.
Pimp Trizkit

3

toArray überprüft, ob das übergebene Array die richtige Größe hat (dh groß genug, um zu den Elementen aus Ihrer Liste zu passen), und verwendet dies in diesem Fall. Wenn die Größe des Arrays kleiner als erforderlich ist, wird folglich reflexartig ein neues Array erstellt.

In Ihrem Fall ist ein Array der Größe Null unveränderlich und kann daher sicher zu einer statischen Endvariablen erhoben werden, wodurch Ihr Code möglicherweise ein wenig sauberer wird und das Array nicht bei jedem Aufruf erstellt wird. Innerhalb der Methode wird ohnehin ein neues Array erstellt, sodass die Lesbarkeit optimiert wird.

Die schnellere Version besteht wahrscheinlich darin, das Array mit der richtigen Größe zu übergeben. Wenn Sie jedoch nicht nachweisen können, dass dieser Code ein Leistungsengpass darstellt, ziehen Sie die Lesbarkeit der Laufzeitleistung vor, bis das Gegenteil bewiesen ist.


2

Der erste Fall ist effizienter.

Das liegt daran, dass im zweiten Fall:

MyClass[] arr = myList.toArray(new MyClass[0]);

Die Laufzeit erstellt tatsächlich ein leeres Array (mit der Größe Null) und erstellt dann innerhalb der toArray-Methode ein weiteres Array, das den tatsächlichen Daten entspricht. Diese Erstellung erfolgt mithilfe von Reflection mit dem folgenden Code (entnommen aus jdk1.5.0_10):

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        a = (T[])java.lang.reflect.Array.
    newInstance(a.getClass().getComponentType(), size);
System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

Durch die Verwendung des ersten Formulars vermeiden Sie die Erstellung eines zweiten Arrays und den Reflektionscode.


toArray () verwendet keine Reflexion. Zumindest solange man "Casting" sowieso nicht zum Nachdenken zählt ;-).
Georgi

toArray (T []) tut es. Es muss ein Array des entsprechenden Typs erstellt werden. Moderne JVMs optimieren diese Art der Reflexion so, dass sie ungefähr der Geschwindigkeit der nicht reflektierenden Version entspricht.
Tom Hawtin - Tackline

Ich denke, dass es Reflexion verwendet. Das JDK 1.5.0_10 funktioniert mit Sicherheit, und nur durch Reflektion kann ich ein Array eines Typs erstellen, den Sie zur Kompilierungszeit nicht kennen.
Panagiotis Korros

Dann ist eines der Quellcodebeispiele (das obige oder meins) veraltet. Leider habe ich keine korrekte Subversionsnummer für meine gefunden.
Georgi

1
Georgi, Ihr Code stammt aus JDK 1.6. Wenn Sie die Implementierung der Arrays.copyTo-Methode sehen, werden Sie feststellen, dass die Implementierung Reflection verwendet.
Panagiotis Korros

-1

Der zweite ist geringfügig besser lesbar, aber es gibt so wenig Verbesserungen, dass es sich nicht lohnt. Die erste Methode ist schneller, ohne Nachteile zur Laufzeit, also verwende ich sie. Aber ich schreibe es auf die zweite Art, weil es schneller zu tippen ist. Dann kennzeichnet meine IDE es als Warnung und bietet an, es zu beheben. Mit einem einzigen Tastendruck wird der Code vom zweiten in den ersten Typ konvertiert.


-2

Die Verwendung von 'toArray' mit dem Array der richtigen Größe ist besser, da die Alternative zuerst das Array mit der Größe Null und dann das Array mit der richtigen Größe erstellt. Wie Sie sagen, ist der Unterschied jedoch wahrscheinlich vernachlässigbar.

Beachten Sie außerdem, dass der Javac-Compiler keine Optimierung durchführt. Heutzutage werden alle Optimierungen zur Laufzeit von den JIT / HotSpot-Compilern durchgeführt. Mir sind keine Optimierungen in Bezug auf 'toArray' in JVMs bekannt.

Die Antwort auf Ihre Frage ist also weitgehend eine Frage des Stils, sollte jedoch aus Gründen der Konsistenz Teil aller Codierungsstandards sein, die Sie einhalten (ob dokumentiert oder anderweitig).


OTOH: Wenn der Standard ein Array mit der Länge Null verwenden soll, implizieren abweichende Fälle, dass die Leistung ein Problem darstellt.
Michael Scheper

-5

Beispielcode für Ganzzahl:

Integer[] arr = myList.toArray(new integer[0]);
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.