Mengapa memulai ArrayList dengan kapasitas awal?

149

Konstruktor biasa ArrayListadalah:

ArrayList<?> list = new ArrayList<>();

Tetapi ada juga konstruktor yang kelebihan muatan dengan parameter untuk kapasitas awalnya:

ArrayList<?> list = new ArrayList<>(20);

Mengapa berguna untuk membuat ArrayListdengan kapasitas awal ketika kita dapat menambahkannya sesuka kita?

rampok
sumber
17
Sudahkah Anda mencoba melihat kode sumber ArrayList?
AmitG
@ Joachim Sauer: Suatu saat kita mendapatkan kesadaran ketika kita membaca sumber dengan cermat. Saya mencoba jika dia sudah membaca sumbernya. Saya mengerti aspek Anda. Terima kasih.
AmitG
ArrayList adalah periode berkinerja buruk, mengapa Anda ingin menggunakan struktur seperti itu
PositiveGuy

Jawaban:

196

Jika Anda tahu sebelumnya apa ukuran yang ArrayListakan terjadi, akan lebih efisien untuk menentukan kapasitas awal. Jika Anda tidak melakukan ini, array internal harus berulang kali dialokasikan kembali seiring bertambahnya daftar.

Semakin besar daftar akhir, semakin banyak waktu yang Anda hemat dengan menghindari realokasi.

Yang mengatakan, bahkan tanpa pra-alokasi, memasukkan nelemen di belakang sebuah ArrayListdijamin akan memakan O(n)waktu total . Dengan kata lain, menambahkan elemen adalah operasi waktu konstan yang diamortisasi. Ini dicapai dengan meminta setiap realokasi meningkatkan ukuran array secara eksponensial, biasanya dengan faktor 1.5. Dengan pendekatan ini, jumlah total operasi dapat ditunjukkanO(n) .

NPE
sumber
5
Meskipun pra-alokasi ukuran yang diketahui adalah ide yang baik, tidak melakukannya biasanya tidak buruk: Anda akan membutuhkan log (n) alokasi ulang untuk daftar dengan ukuran akhir n , yang tidak banyak.
Joachim Sauer
2
@PeterOlson O(n log n)akan melakukan waktu log nkerja n. Itu perkiraan terlalu tinggi (meskipun secara teknis benar dengan O besar karena itu menjadi batas atas). Ini menyalin s + s * 1,5 + s * 1,5 ^ 2 + ... + s * 1,5 ^ m (sedemikian sehingga s * 1,5 ^ m <n <s * 1,5 ^ (m + 1)) elemen secara total. Saya tidak pandai dalam jumlah jadi saya tidak bisa memberi Anda matematika yang tepat dari atas kepala saya (untuk mengubah ukuran faktor 2, ini 2n, jadi mungkin 1,5n memberi atau mengambil konstanta kecil), tetapi itu tidak Jangan terlalu menyipitkan mata untuk melihat bahwa jumlah ini paling banyak merupakan faktor konstan yang lebih besar dari n. Jadi dibutuhkan O (k * n) salinan, yang tentu saja O (n).
1
@ Darnan: Tidak bisa berdebat dengan itu! ;) BTW, saya sangat menyukai argumen menyipit Anda; akan menambahkannya ke daftar trik saya.
NPE
6
Lebih mudah melakukan argumen dengan menggandakan. Misalkan Anda menggandakan ketika penuh, dimulai dengan satu elemen. Misalkan Anda ingin memasukkan 8 elemen. Masukkan satu (biaya: 1). Masukkan dua - dua kali lipat, salin satu elemen dan masukkan dua (biaya: 2). Masukkan tiga - dobel, salin dua elemen, masukkan tiga (biaya: 3). Masukkan empat (biaya: 1). Masukkan lima - ganda, salin empat elemen, masukkan lima (biaya: 5). Masukkan enam, tujuh dan delapan (biaya: 3). Total biaya: 1 + 2 + 3 + 1 + 5 + 3 = 16, yang merupakan dua kali jumlah elemen yang dimasukkan. Dari sketsa ini, Anda dapat membuktikan bahwa biaya rata - rata adalah dua per sisipan pada umumnya.
Eric Lippert
9
Itulah biaya dalam waktu . Anda juga dapat melihat bahwa jumlah ruang yang terbuang berubah seiring waktu, menjadi 0% beberapa waktu dan mendekati 100% beberapa waktu. Mengubah faktor dari 2 menjadi 1,5 atau 4 atau 100 atau apa pun mengubah jumlah rata-rata ruang yang terbuang dan jumlah rata-rata waktu yang dihabiskan untuk menyalin, tetapi kompleksitas waktu tetap linier rata-rata, apa pun faktornya.
Eric Lippert
41

Karena ArrayListadalah struktur data array yang mengubah ukuran secara dinamis , yang berarti diimplementasikan sebagai array dengan ukuran tetap awal (default). Ketika ini terisi, array akan diperpanjang menjadi satu ukuran ganda. Operasi ini mahal, jadi Anda ingin sesedikit mungkin.

Jadi, jika Anda tahu batas atas Anda adalah 20 item, maka menciptakan array dengan panjang awal 20 lebih baik daripada menggunakan default, katakanlah, 15 dan kemudian ubah ukurannya 15*2 = 30dan gunakan hanya 20 saat membuang-buang siklus ekspansi.

PS - Seperti yang dikatakan AmitG, faktor ekspansi adalah implementasi spesifik (dalam hal ini (oldCapacity * 3)/2 + 1)

Iulius Curt
sumber
9
sebenarnyaint newCapacity = (oldCapacity * 3)/2 + 1;
AmitG
25

Ukuran standar Arraylist adalah 10 .

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
    this(10);
    } 

Jadi jika Anda akan menambah 100 atau lebih catatan, Anda dapat melihat overhead realokasi memori.

ArrayList<?> list = new ArrayList<>();    
// same as  new ArrayList<>(10);      

Jadi jika Anda memiliki gagasan tentang jumlah elemen yang akan disimpan di Arraylist lebih baik untuk membuat Arraylist dengan ukuran itu daripada mulai dengan 10 dan kemudian meningkatkannya.

xyz
sumber
Tidak ada jaminan bahwa kapasitas default akan selalu 10 untuk versi JDK di masa depan -private static final int DEFAULT_CAPACITY = 10
vikingsteve
17

Saya sebenarnya menulis posting blog pada topik 2 bulan yang lalu. Artikel ini untuk C # List<T>tetapi Java ArrayListmemiliki implementasi yang sangat mirip. Karena ArrayListdiimplementasikan menggunakan array dinamis, ukurannya bertambah sesuai permintaan. Jadi alasan konstruktor kapasitas adalah untuk keperluan optimasi.

Ketika salah satu dari operasi resizings ini terjadi, ArrayList menyalin isi dari array ke dalam array baru yang dua kali kapasitas dari yang lama. Operasi ini berjalan dalam waktu O (n) .

Contoh

Berikut adalah contoh bagaimana ArrayListpeningkatan ukuran:

10
16
25
38
58
... 17 resizes ...
198578
297868
446803
670205
1005308

Jadi daftar dimulai dengan kapasitas 10, ketika item ke-11 ditambahkan itu meningkat 50% + 1hingga 16. Pada item ke-17 ArrayListmeningkat lagi ke 25dan seterusnya. Sekarang perhatikan contoh di mana kami membuat daftar di mana kapasitas yang diinginkan sudah dikenal sebagai 1000000. Membuat ArrayListkonstruktor tanpa ukuran akan memanggil ArrayList.add 1000000waktu yang membutuhkan O (1) secara normal atau O (n) pada pengubahan ukuran.

1000000 + 16 + 25 + ... + 670205 + 1005308 = 4015851 operasi

Bandingkan ini menggunakan konstruktor dan kemudian panggilan ArrayList.addyang dijamin berjalan di O (1) .

1000000 + 1000000 = 2000000 operasi

Java vs C #

Java adalah seperti di atas, mulai 10dan meningkatkan setiap ukuran di 50% + 1. C # mulai 4dan meningkat jauh lebih agresif, dua kali lipat pada setiap ukuran. Contoh 1000000menambahkan dari atas untuk C # menggunakan 3097084operasi.

Referensi

Daniel Imms
sumber
9

Mengatur ukuran awal ArrayList, misalnya untuk ArrayList<>(100), mengurangi berapa kali alokasi ulang memori internal harus terjadi.

Contoh:

ArrayList example = new ArrayList<Integer>(3);
example.add(1); // size() == 1
example.add(2); // size() == 2, 
example.add(2); // size() == 3, example has been 'filled'
example.add(3); // size() == 4, example has been 'expanded' so that the fourth element can be added. 

Seperti yang Anda lihat dalam contoh di atas - suatu ArrayListdapat diperluas jika perlu. Apa ini tidak menunjukkan kepada Anda adalah bahwa ukuran Arraylist biasanya berlipat ganda (walaupun perhatikan bahwa ukuran baru tergantung pada implementasi Anda). Berikut ini dikutip dari Oracle :

"Setiap instance ArrayList memiliki kapasitas. Kapasitas adalah ukuran array yang digunakan untuk menyimpan elemen dalam daftar. Itu selalu setidaknya sebesar ukuran daftar. Ketika elemen ditambahkan ke ArrayList, kapasitasnya tumbuh secara otomatis. Rincian kebijakan pertumbuhan tidak ditentukan di luar fakta bahwa menambahkan elemen memiliki biaya waktu diamortisasi konstan. "

Jelas, jika Anda tidak tahu kisaran apa yang akan Anda pegang, mengatur ukuran mungkin tidak akan menjadi ide yang baik - namun, jika Anda memiliki kisaran tertentu dalam pikiran, pengaturan kapasitas awal akan meningkatkan efisiensi memori .

dsgriffin
sumber
3

ArrayList dapat berisi banyak nilai dan ketika melakukan penyisipan awal yang besar Anda dapat memberitahu ArrayList untuk mengalokasikan penyimpanan yang lebih besar untuk memulai dengan agar tidak membuang siklus CPU ketika mencoba mengalokasikan lebih banyak ruang untuk item berikutnya. Dengan demikian untuk mengalokasikan beberapa ruang di awal lebih efisien.

Sanober Malik
sumber
3

Ini untuk menghindari upaya yang mungkin untuk realokasi untuk setiap objek tunggal.

int newCapacity = (oldCapacity * 3)/2 + 1;

internal new Object[]dibuat.
JVM perlu upaya untuk membuat new Object[]ketika Anda menambahkan elemen dalam daftar array. Jika Anda tidak memiliki kode diatas (setiap algo Anda berpikir) untuk realokasi maka setiap kali ketika Anda menjalankan arraylist.add()kemudian new Object[]harus dibuat yang sia-sia dan kami kehilangan waktu untuk meningkatkan ukuran oleh 1 untuk setiap objek yang akan ditambahkan. Jadi lebih baik menambah ukuran Object[]dengan formula berikut.
(JSL telah menggunakan rumus forcasting yang diberikan di bawah ini untuk daftar array yang tumbuh secara dinamis alih-alih bertambah 1 setiap kali. Karena untuk tumbuh dibutuhkan upaya oleh JVM)

int newCapacity = (oldCapacity * 3)/2 + 1;
AmitG
sumber
ArrayList tidak akan melakukan realokasi untuk setiap single add- sudah menggunakan beberapa formula pertumbuhan secara internal. Karena itu pertanyaannya tidak dijawab.
AH
@AH Jawaban saya adalah untuk pengujian negatif . Silakan baca yang tersirat. Saya berkata "Jika Anda tidak memiliki kode di atas (algo menurut Anda) untuk realokasi maka setiap kali ketika Anda memanggil arraylist.add () maka Obyek baru [] harus dibuat yang tidak ada gunanya dan kami kehilangan waktu." dan kode adalah int newCapacity = (oldCapacity * 3)/2 + 1;yang hadir dalam kelas ArrayList. Apakah Anda masih berpikir itu belum terjawab?
AmitG
1
Saya masih berpikir itu tidak dijawab: Dalam ArrayListrealokasi diamortisasi berlangsung di setiap kasus dengan setiap nilai untuk kapasitas awal. Dan pertanyaannya adalah tentang: Mengapa menggunakan nilai non-standar untuk kapasitas awal? Selain itu: "membaca yang tersirat" bukanlah sesuatu yang diinginkan dalam jawaban teknis. ;-)
AH
@AH Saya menjawab seperti, apa yang terjadi jika kita tidak memiliki proses realokasi di ArrayList. Jadi jawabannya. Coba baca semangat jawabannya :-). Saya lebih baik tahu Di ArrayList realokasi diamortisasi terjadi dalam hal apapun dengan nilai untuk kapasitas awal.
AmitG
2

Saya pikir setiap ArrayList dibuat dengan nilai kapasitas init "10". Jadi, jika Anda membuat ArrayList tanpa menetapkan kapasitas dalam konstruktor, itu akan dibuat dengan nilai default.

sk2212
sumber
2

Saya akan mengatakan ini sebuah optimasi. ArrayList tanpa kapasitas awal akan memiliki ~ 10 baris kosong dan akan diperluas ketika Anda melakukan add.

Untuk memiliki daftar dengan jumlah item yang Anda butuhkan untuk memanggil trimToSize ()

Daniel Magnusson
sumber
0

Sesuai pengalaman saya ArrayList, memberikan kapasitas awal adalah cara yang baik untuk menghindari biaya realokasi. Tapi itu menjadi peringatan. Semua saran yang disebutkan di atas mengatakan bahwa seseorang harus menyediakan kapasitas awal hanya ketika perkiraan kasar jumlah elemen diketahui. Tetapi ketika kami mencoba untuk memberikan kapasitas awal tanpa ide, jumlah memori yang dicadangkan dan tidak digunakan akan sia-sia karena mungkin tidak pernah diperlukan setelah daftar diisi ke sejumlah elemen yang diperlukan. Apa yang saya katakan adalah, kita bisa pragmatis di awal sambil mengalokasikan kapasitas, dan kemudian menemukan cara cerdas untuk mengetahui kapasitas minimal yang diperlukan saat runtime. ArrayList menyediakan metode yang disebut ensureCapacity(int minCapacity). Tapi kemudian, seseorang telah menemukan cara yang cerdas ...

Tushar Patidar
sumber
0

Saya telah menguji ArrayList dengan dan tanpa initialCapacity dan saya mendapat hasil yang mengejutkan.
Ketika saya mengatur LOOP_NUMBER menjadi 100.000 atau kurang hasilnya adalah bahwa pengaturan initialCapacity lebih efisien.

list1Sttop-list1Start = 14
list2Sttop-list2Start = 10


Tetapi ketika saya mengatur LOOP_NUMBER menjadi 1.000.000 hasilnya berubah menjadi:

list1Stop-list1Start = 40
list2Stop-list2Start = 66


Akhirnya, saya tidak tahu bagaimana cara kerjanya ?!
Kode sampel:

 public static final int LOOP_NUMBER = 100000;

public static void main(String[] args) {

    long list1Start = System.currentTimeMillis();
    List<Integer> list1 = new ArrayList();
    for (int i = 0; i < LOOP_NUMBER; i++) {
        list1.add(i);
    }
    long list1Stop = System.currentTimeMillis();
    System.out.println("list1Stop-list1Start = " + String.valueOf(list1Stop - list1Start));

    long list2Start = System.currentTimeMillis();
    List<Integer> list2 = new ArrayList(LOOP_NUMBER);
    for (int i = 0; i < LOOP_NUMBER; i++) {
        list2.add(i);
    }
    long list2Stop = System.currentTimeMillis();
    System.out.println("list2Stop-list2Start = " + String.valueOf(list2Stop - list2Start));
}

Saya telah menguji pada windows8.1 dan jdk1.7.0_80

Hamedz
sumber
1
hai, sayangnya toleransi currentTimeMillis adalah hingga seratus milidetik (tergantung), artinya hasilnya hampir tidak dapat diandalkan. Saya menyarankan untuk menggunakan beberapa perpustakaan khusus untuk melakukannya dengan benar.
Bogdan