Навіщо запускати ArrayList з початковою ємністю?


149

Звичайний конструктор ArrayList:

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

Але є також перевантажений конструктор з параметром для його початкової потужності:

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

Чому корисно створити ArrayListпочаткову ємність, коли ми можемо додати її до свого бажання?


17
Ви намагалися побачити вихідний код ArrayList?
AmitG

@Joachim Sauer: Колись ми ретельно читаємо джерело. Я спробував, чи прочитав він джерело. Я зрозумів ваш аспект. Дякую.
AmitG

ArrayList погано працює, чому ви хочете використовувати таку структуру
PositiveGuy

Відповіді:


196

Якщо ви заздалегідь знаєте, який розмір ArrayListбуде, то більш ефективно визначити початкову ємність. Якщо цього не зробити, внутрішній масив доведеться неодноразово перерозподіляти, коли список зростає.

Чим більше кінцевий список, тим більше часу ви економите, уникаючи перерозподілу.

Однак, навіть без попереднього розміщення, вставлення nелементів на задній панелі ArrayListгарантовано займе загальний O(n)час. Іншими словами, додавання елемента - це амортизована операція постійного часу. Це досягається шляхом кожного перерозподілу збільшення розміру масиву експоненціально, як правило, на коефіцієнт 1.5. При такому підході може бути показанаO(n) загальна кількість операцій .


5
Хоча попередньо виділити відомі розміри - це гарна ідея, але робити це, як правило, не страшно: вам знадобиться перерозподіл журналу (n) для списку з кінцевим розміром n , якого не багато.
Йоахім Зауер

2
@PeterOlson O(n log n)би робив log nробочий nчас. Це важка завищення (хоча технічно правильне з великим O, оскільки воно є верхньою межею). Він копіює s + s * 1,5 + s * 1,5 ^ 2 + ... + s * 1,5 ^ m (такий, що s * 1,5 ^ m <n <s * 1,5 ^ (m + 1)) елементів. Я не добрий в сумах, тому не можу дати точну математику вгорі голови (для зміни фактора 2, це 2n, так що це може бути 1,5n дати або взяти невелику константу), але це не ' t занадто сильно ковзаючи, щоб побачити, що ця сума є щонайменше постійним коефіцієнтом більше n. Отже, вона займає копії O (k * n), що, звичайно, є O (n).

1
@delnan: Не можу з цим посперечатися! ;) До речі, мені дуже сподобався твій примружений аргумент; додасть це до мого репертуару трюків.
NPE

6
Простіше зробити аргумент з подвоєнням. Припустимо, ви подвоюєтесь у повному обсязі, починаючи з одного елемента. Припустимо, ви хочете вставити 8 елементів. Вставте один (вартість: 1). Вставте два - подвійне, скопіюйте один елемент і вставте два (вартість: 2). Вставити три - подвійне, скопіювати два елементи, вставити три (вартість: 3). Вставте чотири (вартість: 1). Вставте п'ять - подвійно, скопіюйте чотири елементи, вставте п’ять (вартість: 5). Вставте шість, сім і вісім (вартість: 3). Загальна вартість: 1 + 2 + 3 + 1 + 5 + 3 = 16, що вдвічі перевищує кількість вставлених елементів. З цього ескізу ви можете довести, що середня вартість загалом - дві за вкладиш .
Ерік Ліпперт

9
Ось вартість у часі . Ви також можете побачити, що кількість витраченого простору з часом змінювалася, будучи 0% деякий час і близько 100% деякого часу. Зміна коефіцієнта від 2 до 1,5 або 4 або 100 або що завгодно змінює середню кількість витраченого простору та середню кількість витраченого часу на копіювання, але складність часу залишається лінійною в середньому незалежно від того, який це коефіцієнт.
Ерік Ліпперт

41

Оскільки ArrayListце динамічно змінюється структура даних масиву , це означає, що вона реалізована у вигляді масиву з початковим (за замовчуванням) фіксованим розміром. Коли це заповниться, масив буде розширено до подвійного розміру. Ця операція коштує дорого, тому вам потрібно якомога менше.

Отже, якщо ви знаєте, що ваша верхня межа - 20 елементів, то створити масив з початковою довжиною 20 краще, ніж використовувати за замовчуванням, скажімо, 15, а потім змінити його розмір 15*2 = 30і використовувати лише 20, витрачаючи цикли на розширення.

PS - Як каже AmitG, коефіцієнт розширення є специфічним для впровадження (в даному випадку (oldCapacity * 3)/2 + 1)


9
це насправдіint newCapacity = (oldCapacity * 3)/2 + 1;
AmitG

25

Розмір Arraylist за замовчуванням - 10 .

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

Отже, якщо ви збираєтеся додати 100 або більше записів, ви можете побачити накладні перерозподіл пам'яті.

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

Тож якщо ви маєте уявлення про кількість елементів, які зберігатимуться в Arraylist, краще створити Arraylist такого розміру, а не починати з 10, а потім продовжувати його збільшувати.


Немає гарантії, що в майбутньому ємність за замовчуванням завжди буде 10 для версій JDK -private static final int DEFAULT_CAPACITY = 10
vikingsteve

17

Я фактично написав повідомлення в блозі на цю тему 2 місяці тому. Стаття призначена для C #, List<T>але Java ArrayListмає дуже схожу реалізацію. Оскільки ArrayListреалізується за допомогою динамічного масиву, він збільшується в розмірі на вимогу. Тож причина конструктора ємності полягає в оптимізаційних цілях.

Коли відбувається одна з цих операцій зміни розміру, ArrayList копіює вміст масиву в новий масив, що в два рази перевищує ємність старого. Ця операція виконується в O (n) час.

Приклад

Ось приклад того, як ArrayListзбільшився б розмір:

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

Отже, список починається з ємності 10, коли додається 11-й елемент, він збільшується 50% + 1на 16. На 17-му пункті ArrayListзнову збільшується до 25тощо. Тепер розглянемо приклад, коли ми створюємо список, де потрібна ємність вже відома як 1000000. Створення ArrayListконструктора без розміру буде викликати ArrayList.add 1000000час, який нормально приймає O (1) або O (n) .

1000000 + 16 + 25 + ... + 670205 + 1005308 = 4015851 операції

Порівняйте це за допомогою конструктора та виклику, ArrayList.addякий гарантовано працює в O (1) .

1000000 + 1000000 = 2000000 операцій

Java проти C #

Java, як вище, починається з 10і збільшує кожен розмір на 50% + 1. C # починається 4і збільшується набагато агресивніше, подвоюючись при кожному зміні розміру. Приклад 1000000додавання зверху для C # використовує 3097084операції.

Список літератури


9

Встановлення початкового розміру ArrayList, наприклад, до ArrayList<>(100), зменшує кількість разів, коли відбудеться перерозподіл внутрішньої пам'яті.

Приклад:

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. 

Як ви бачите у наведеному вище прикладі - ArrayListпри необхідності можна розширити розширення. Це вам не показує, що розмір Arraylist зазвичай подвоюється (хоча зауважте, що новий розмір залежить від вашої реалізації). З Oracle цитується наступне :

"Кожен екземпляр ArrayList має ємність. Ємність - це розмір масиву, який використовується для зберігання елементів у списку. Він завжди принаймні такий, як розмір списку. Оскільки елементи додаються до ArrayList, його ємність зростає автоматично. Деталі політики зростання не визначені, крім того, що додавання елемента має постійну амортизовану вартість часу ".

Очевидно, якщо ви не маєте уявлення про те, який діапазон ви будете тримати, встановлення розміру, ймовірно, не буде гарною ідеєю - однак, якщо ви маєте на увазі конкретний діапазон, встановлення початкової ємності підвищить ефективність пам'яті .


3

ArrayList може містити багато значень, і, роблячи великі початкові вставки, ви можете сказати ArrayList виділити більшу пам’ять для початку, щоб не витрачати цикли процесора, коли він намагається виділити більше місця для наступного елемента. Таким чином, виділяти деякий простір на початку є більш доцільним.


3

Це дозволяє уникнути можливих зусиль для перерозподілу кожного об'єкта.

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

new Object[]створюється внутрішньо .
JVM потребує зусиль для створення, new Object[]коли ви додаєте елемент до масиву. Якщо у вас немає вищевказаного коду (будь-якого альго, який ви думаєте) для перерозподілу, то кожен раз, коли ви викликаєте, arraylist.add()тоді new Object[]потрібно створювати, що безглуздо, і ми втрачаємо час на збільшення розміру на 1 для кожного об'єкта, який потрібно додати. Тож краще збільшити розмір Object[]за наступною формулою.
(JSL використовує формулу мовлення, наведену нижче, для динамічно зростаючого масиву масивів, а не зростаючи на 1 раз. Тому що для зростання потрібно JVM)

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

ArrayList не виконуватиме перерозподіл для кожного синглу add- він вже використовує якусь формулу росту внутрішньо. Звідси на запитання не відповіли.
AH

@AH Моя відповідь - за негативне тестування . Ласкаво читайте між рядків. Я сказав: "Якщо у вас немає вищевказаного коду (будь-якого альго, який ви думаєте) для перерозподілу, то кожного разу, коли ви викликаєте arraylist.add (), тоді слід створювати новий Object [], який є безглуздим, і ми втрачаємо час". і код , int newCapacity = (oldCapacity * 3)/2 + 1;який присутній у класі ArrayList. Ви все ще вважаєте, що це без відповіді?
AmitG

1
Я все ще думаю, що на нього не відповідають: В ArrayListамортизованому перерозподілі відбувається в будь-якому випадку з будь-яким значенням для початкової ємності. І питання полягає в тому: навіщо взагалі використовувати нестандартне значення для початкової ємності? Крім цього: "читання між рядками" не є чимось бажаним у технічній відповіді. ;-)
AH

@AH Я відповідаю як, що сталося, якби у нас не було процесу перерозподілу в ArrayList. Так і є відповідь. Спробуйте прочитати дух відповіді :-). Я краще знаю У ArrayList амортизоване перерозподіл відбувається в будь-якому випадку з будь-яким значенням для початкової ємності.
AmitG

2

Я думаю, що кожен ArrayList створений зі значенням init ємності "10". Так що все одно, якщо ви створите ArrayList без встановлення ємності в конструкторі, він буде створений зі значенням за замовчуванням.


2

Я б сказав, що це оптимізація. ArrayList без початкової ємності матиме ~ 10 порожніх рядків і розшириться, коли ви робите додавання.

Щоб мати список з точною кількістю елементів, потрібно зателефонувати trimToSize ()


0

Згідно з моїм досвідом ArrayList, надання початкової спроможності - це хороший спосіб уникнути витрат на перерозподіл. Але він містить застереження. Усі вищезгадані пропозиції говорять про те, що початкову здатність слід забезпечувати лише тоді, коли буде відома приблизна оцінка кількості елементів. Але коли ми намагаємось дати початкову ємність без будь-якої ідеї, обсяг пам'яті, зарезервованої та невикористаної, буде марною, тому що вона ніколи не буде потрібна, коли список заповниться необхідною кількістю елементів. Що я говорю, ми можемо бути прагматичними на початку, розподіляючи потужність, а потім знаходимо розумний спосіб знати необхідну мінімальну потужність під час виконання. ArrayList забезпечує метод, який називається ensureCapacity(int minCapacity). Але тоді треба знайти розумний шлях ...


0

Я перевірив ArrayList з і без InitiCapacity, і я отримав результат здивування
Коли я встановив LOOP_NUMBER 100 000 або менше, результат полягає в тому, що встановлення InitiCapacity є ефективним.

list1Sttop-list1Start = 14
list2Sttop-list2Start = 10


Але коли я встановив LOOP_NUMBER на 1 000 000, результат змінюється на:

list1Stop-list1Start = 40
list2Stop-list2Start = 66


Нарешті, я не міг зрозуміти, як це працює ?!
Приклад коду:

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

Я протестував на windows8.1 та jdk1.7.0_80


1
привіт, на жаль, толерантність currentTimeMillis становить до ста мілісекунд (залежно), це означає, що результат навряд чи надійний. Я б запропонував використовувати якусь власну бібліотеку, щоб зробити це правильно.
Богдан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.