.toArray (MyClass baru [0]) atau .toArray (MyClass baru [myList.size ()]))?

176

Dengan asumsi saya memiliki ArrayList

ArrayList<MyClass> myList;

Dan saya ingin memanggil toArray, apakah ada alasan kinerja untuk digunakan

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

lebih

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

?

Saya lebih suka gaya kedua, karena kurang verbose, dan saya berasumsi bahwa kompiler akan memastikan array kosong tidak benar-benar dibuat, tapi saya sudah bertanya-tanya apakah itu benar.

Tentu saja, dalam 99% kasus tidak ada bedanya dengan satu cara atau yang lain, tapi saya ingin menjaga gaya yang konsisten antara kode normal saya dan loop internal yang dioptimalkan ...

itsadok
sumber
6
Sepertinya pertanyaannya sekarang telah diselesaikan dalam posting blog baru oleh Aleksey Shipilёv, Array of Wisdom of the Ancients !
glts
6
Dari posting blog: "Intinya: toArray (T baru [0]) tampaknya lebih cepat, lebih aman, dan lebih bersih secara kontraktual, dan karenanya harus menjadi pilihan default sekarang."
DavidS

Jawaban:

109

Secara berlawanan, versi tercepat, di Hotspot 8, adalah:

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

Saya telah menjalankan tolok ukur mikro menggunakan jmh hasil dan kode di bawah ini, menunjukkan bahwa versi dengan array kosong secara konsisten mengungguli versi dengan array yang telah ditentukan. Perhatikan bahwa jika Anda dapat menggunakan kembali array yang ada dengan ukuran yang benar, hasilnya mungkin berbeda.

Hasil benchmark (skor dalam mikrodetik, lebih kecil = lebih baik):

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

Untuk referensi, kodenya:

@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]);
  }
}

Anda dapat menemukan hasil yang serupa, analisis lengkap, dan diskusi di posting blog Array of Wisdom of the Ancients . Untuk meringkas: kompiler JVM dan JIT berisi beberapa optimisasi yang memungkinkannya untuk membuat dan menginisialisasi array dengan ukuran baru dengan murah, dan optimisasi tersebut tidak dapat digunakan jika Anda membuat array sendiri.

assylias
sumber
2
Komentar yang sangat menarik Saya terkejut tidak ada yang berkomentar tentang ini. Saya kira itu karena bertentangan dengan jawaban lain di sini, sejauh kecepatan. Juga menarik untuk dicatat, reputasi orang ini hampir lebih tinggi dari gabungan semua jawaban lainnya.
Pimp Trizkit
Saya ngelantur. Saya juga ingin melihat tolok ukur untuk MyClass[] arr = myList.stream().toArray(MyClass[]::new);.. yang saya kira akan lebih lambat. Juga, saya ingin melihat tolok ukur untuk perbedaan dengan deklarasi array. Seperti dalam perbedaan antara: MyClass[] arr = new MyClass[myList.size()]; arr = myList.toArray(arr);dan MyClass[] arr = myList.toArray(new MyClass[myList.size()]);... atau haruskah tidak ada perbedaan? Saya kira keduanya adalah masalah yang berada di luar toArrayfungsi yang terjadi. Tapi hey! Saya tidak berpikir saya akan belajar tentang perbedaan rumit lainnya.
Pimp Trizkit
1
@PimpTrizkit Baru saja diperiksa: menggunakan variabel tambahan tidak membuat perbedaan seperti yang diharapkan, Menggunakan aliran membutuhkan waktu antara 60% dan 100% lebih banyak saat memanggil toArraylangsung (semakin kecil ukurannya, semakin besar overhead relatifnya)
assylias
Wow, itu respons yang cepat! Terima kasih! Ya, saya curiga. Konversi ke aliran tidak terdengar efisien. Tapi kamu tidak pernah tahu!
Pimp Trizkit
2
Kesimpulan yang sama ditemukan di sini: shipilev.net/blog/2016/arrays-wisdom-ancients
user167019
122

Pada ArrayList di Java 5 , array akan diisi sudah jika memiliki ukuran yang tepat (atau lebih besar). Karena itu

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

akan membuat satu objek array, mengisinya dan mengembalikannya ke "arr". Di samping itu

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

akan membuat dua array. Yang kedua adalah array dari MyClass dengan panjang 0. Jadi ada penciptaan objek untuk objek yang akan dibuang segera. Sejauh kode sumber menunjukkan compiler / JIT tidak dapat mengoptimalkan yang ini sehingga tidak dibuat. Selain itu, dengan menggunakan objek zero-length menghasilkan casting dalam metode toArray () -.

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

Gunakan metode pertama sehingga hanya satu objek yang dibuat dan menghindari coran (implisit tapi tetap mahal).

Georgi
sumber
1
Dua komentar, mungkin menarik bagi seseorang: 1) LinkedList.toArray (T [] a) bahkan lebih lambat (menggunakan refleksi: Array.newInstance) dan lebih kompleks; 2) Di sisi lain, dalam rilis JDK7, saya sangat terkejut mengetahui, Array yang biasanya sangat lambat. Performa baru melakukan hampir secepat pembuatan array yang biasa!
java.is.for.desktop
1
@ktaria size adalah anggota pribadi ArrayList, yang menentukan **** mengejutkan **** ukurannya. Lihat ArrayList SourceCode
MyPasswordIsLasercats
3
Menebak kinerja tanpa tolok ukur hanya berfungsi dalam kasus sepele. Sebenarnya, new Myclass[0]lebih cepat: shipilev.net/blog/2016/arrays-wisdom-ancients
Karol S
Ini bukan lagi jawaban yang valid pada JDK6 +
Антон Антонов
28

Dari inspeksi JetBrains Intellij Idea:

Ada dua gaya untuk mengonversi koleksi menjadi array: baik menggunakan array ukuran awal (seperti c.toArray (String baru [c.size ()]) )) atau menggunakan array kosong (seperti c.toArray (new String [ 0]) .

Dalam versi Java yang lebih lama menggunakan array ukuran pra-rekomendasikan, karena panggilan refleksi yang diperlukan untuk membuat array dengan ukuran yang tepat sangat lambat. Namun karena pembaruan OpenJDK 6 yang terlambat, panggilan ini terintinsifikasi, menjadikan kinerja versi array kosong sama dan terkadang bahkan lebih baik, dibandingkan dengan versi pra-ukuran. Melewati array pra-ukuran berbahaya untuk koleksi bersamaan atau disinkronkan karena perlombaan data dimungkinkan antara ukuran dan panggilan toArray yang dapat menghasilkan nulls ekstra di akhir array, jika koleksi secara bersamaan menyusut selama operasi.

Inspeksi ini memungkinkan untuk mengikuti gaya seragam: baik menggunakan array kosong (yang direkomendasikan di Jawa modern) atau menggunakan array pra-ukuran (yang mungkin lebih cepat di versi Java yang lebih lama atau JVM berbasis non-HotSpot).

Антон Антонов
sumber
Jika semua ini disalin / dikutip teks, dapatkah kita memformatnya dan juga memberikan tautan ke sumbernya? Saya benar-benar datang ke sini karena inspeksi IntelliJ dan saya sangat tertarik pada tautan untuk mencari semua inspeksi mereka dan alasan di baliknya.
Tim Büthe
3
Di sini Anda dapat memeriksa teks-teks inspeksi: github.com/JetBrains/intellij-community/tree/master/plugins/…
Антон Антонов
17

JVM modern mengoptimalkan konstruksi array reflektif dalam hal ini, sehingga perbedaan kinerja sangat kecil. Menamai koleksi dua kali dalam kode boilerplate seperti itu bukan ide bagus, jadi saya akan menghindari metode pertama. Keuntungan lain dari yang kedua adalah bahwa ia bekerja dengan koleksi yang disinkronkan dan bersamaan. Jika Anda ingin membuat optimasi, gunakan kembali array kosong (array kosong tidak dapat diubah dan dapat dibagikan), atau gunakan profiler (!).

Tom Hawtin - tackline
sumber
2
Upvoting 'menggunakan kembali array kosong', karena kompromi antara keterbacaan dan kinerja potensial yang layak dipertimbangkan. Melewati argumen dinyatakan private static final MyClass[] EMPTY_MY_CLASS_ARRAY = new MyClass[0]tidak mencegah array kembali dari sedang dibangun oleh refleksi, tetapi tidak mencegah array tambahan yang dibangun masing-masing setiap kali.
Michael Scheper
Machael benar, jika Anda menggunakan array nol panjang tidak ada jalan lain: (T []) java.lang.reflect.Array.newInstance (a.getClass (). GetComponentType (), size); yang akan berlebihan jika ukurannya>> actualSize (JDK7)
Alex
Jika Anda dapat memberikan kutipan untuk "JVM modern mengoptimalkan konstruksi array reflektif dalam kasus ini", saya dengan senang hati akan meningkatkan jawaban ini.
Tom Panning
Saya sedang belajar di sini. Jika sebaliknya saya menggunakan: MyClass[] arr = myList.stream().toArray(MyClass[]::new);Apakah akan membantu atau terluka dengan koleksi yang disinkronkan dan bersamaan. Dan mengapa? Silahkan.
Pimp Trizkit
3

toArray memeriksa apakah array yang dikirimkan memiliki ukuran yang tepat (yaitu, cukup besar untuk memuat elemen dari daftar Anda) dan jika demikian, gunakan itu. Akibatnya jika ukuran array yang disediakan lebih kecil dari yang dibutuhkan, array baru akan dibuat secara refleksif.

Dalam kasus Anda, array dengan ukuran nol, tidak dapat diubah, sehingga dapat dengan aman dinaikkan ke variabel final statis, yang mungkin membuat kode Anda sedikit lebih bersih, yang menghindari membuat array pada setiap permintaan. Larik baru akan dibuat di dalam metode, jadi itu adalah pembacaan optimalisasi.

Mungkin versi yang lebih cepat adalah untuk melewatkan array dengan ukuran yang benar, tetapi kecuali Anda dapat membuktikan bahwa kode ini adalah hambatan kinerja, lebih suka keterbacaan untuk kinerja runtime sampai terbukti sebaliknya.

Dave Cheney
sumber
2

Kasus pertama lebih efisien.

Itu karena dalam kasus kedua:

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

runtime sebenarnya membuat array kosong (dengan ukuran nol) dan kemudian di dalam metode toArray membuat array lain agar sesuai dengan data aktual. Pembuatan ini dilakukan dengan menggunakan refleksi menggunakan kode berikut (diambil dari 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;
}

Dengan menggunakan formulir pertama, Anda menghindari pembuatan array kedua dan juga menghindari kode refleksi.

Panagiotis Korros
sumber
toArray () tidak menggunakan refleksi. Setidaknya selama Anda tidak menghitung "casting" untuk refleksi, tetap ;-).
Georgi
toArray (T []) tidak. Perlu membuat larik tipe yang sesuai. JVM modern mengoptimalkan refleksi semacam itu dengan kecepatan yang sama dengan versi non-reflektif.
Tom Hawtin - tackline
Saya pikir itu memang menggunakan refleksi. JDK 1.5.0_10 tidak pasti dan refleksi adalah satu-satunya cara saya tahu untuk membuat array tipe yang Anda tidak tahu pada waktu kompilasi.
Panagiotis Korros
Kemudian salah satu kode sumber mencontohkannya (yang di atas atau milik saya) sudah kedaluwarsa. Sayangnya, saya tidak menemukan nomor sub-versi yang benar untuk nomor saya.
Georgi
1
Georgi, kode Anda berasal dari JDK 1.6 dan jika Anda melihat implementasi metode Arrays.copyTo Anda akan melihat bahwa implementasi menggunakan refleksi.
Panagiotis Korros
-1

Yang kedua sedikit lebih mudah dibaca, tetapi ada sedikit perbaikan sehingga tidak layak. Metode pertama lebih cepat, tanpa kerugian saat runtime, jadi itulah yang saya gunakan. Tapi saya menulisnya dengan cara kedua, karena lebih cepat untuk mengetik. Kemudian IDE saya menandainya sebagai peringatan dan menawarkan untuk memperbaikinya. Dengan satu penekanan tombol, itu mengubah kode dari tipe kedua ke yang pertama.

MiguelMunoz
sumber
-2

Menggunakan 'toArray' dengan array dengan ukuran yang benar akan berkinerja lebih baik karena alternatif akan membuat pertama array berukuran nol kemudian array dengan ukuran yang benar. Namun, seperti yang Anda katakan, perbedaannya mungkin diabaikan.

Juga, perhatikan bahwa kompiler javac tidak melakukan optimasi apa pun. Hari-hari ini semua optimasi dilakukan oleh kompiler JIT / HotSpot saat runtime. Saya tidak mengetahui adanya optimasi di sekitar 'toArray' di JVM.

Maka, jawaban atas pertanyaan Anda sebagian besar adalah masalah gaya tetapi demi konsistensi harus menjadi bagian dari standar pengkodean apa pun yang Anda patuhi (apakah didokumentasikan atau tidak).

Matthew Murdoch
sumber
OTOH, jika standar adalah untuk menggunakan array panjang-nol, maka kasus-kasus yang menyimpang menyiratkan bahwa kinerja adalah masalah.
Michael Scheper
-5

kode sampel untuk integer:

Integer[] arr = myList.toArray(new integer[0]);
Rasol
sumber