Зачем запускать 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
Хотя предварительное распределение известных размеров является хорошей идеей, не делать это обычно не страшно: вам понадобится перераспределение log (n) для списка с конечным размером n , что не так уж много.
Иоахим Зауэр

2
@PeterOlson O(n log n)будет log nработать nраз. Это грубая переоценка (хотя технически правильная с большим О, потому что это верхняя граница). Он копирует 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, чтобы дать или взять небольшую константу), но это не так. не нужно слишком щуриться, чтобы увидеть, что эта сумма не более чем постоянный фактор, превышающий 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. 

Как вы видите в приведенном выше примере - an 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

@ А, я отвечаю, что случилось, если бы у нас не было процесса перераспределения в ArrayList. Так и ответ. Попробуйте прочитать дух ответа :-). Я лучше знаю, что в ArrayList амортизированное перераспределение происходит в любом случае с любым значением начальной емкости.
AmitG

2

Я думаю, что каждый ArrayList создан со значением емкости инициализации "10". Так или иначе, если вы создадите ArrayList без установки емкости в конструкторе, он будет создан со значением по умолчанию.


2

Я бы сказал, что это оптимизация. ArrayList без начальной емкости будет иметь ~ 10 пустых строк и будет расширяться при добавлении.

Чтобы получить список с точным количеством элементов, вам нужно вызвать trimToSize ()


0

Согласно моему опыту ArrayList, предоставление начальной емкости - хороший способ избежать затрат на перераспределение. Но это несет оговорку. Все предложения, упомянутые выше, говорят о том, что исходную емкость следует указывать только тогда, когда известна приблизительная оценка количества элементов. Но когда мы пытаемся дать начальную емкость без какой-либо идеи, объем зарезервированной и неиспользованной памяти будет пустой тратой, поскольку она может никогда не потребоваться после заполнения списка требуемым количеством элементов. Я хочу сказать, что в начале мы можем прагматично распределять емкость, а затем находить разумный способ узнать требуемую минимальную емкость во время выполнения. ArrayList предоставляет метод с именем ensureCapacity(int minCapacity). Но тогда нужно найти умный способ ...


0

Я протестировал ArrayList с и без initialCapacity и получил удивительный результат
Когда я установил для LOOP_NUMBER значение 100 000 или меньше, результатом является то, что установка initialCapacity эффективна.

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.