Зачем запускать ArrayList с начальной емкостью?

обычный конструктор ArrayList - это:

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

но есть также перегруженный конструктор с параметром для его начальной емкости:

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

Почему полезно создать ArrayList с начальной емкостью, когда мы можем добавить к этому, как нам заблагорассудится?

11 ответов


если вы заранее знаете, какие размеры ArrayList будет, более эффективно указать начальную емкость. Если вы этого не сделаете, внутренний массив придется повторно перераспределять по мере роста списка.

большой окончательный список, тем больше времени вы экономите, избегая перераспределения.

что сказал, даже без предварительного выделения, вставляя n элементы в задней части ArrayList гарантированно принимает total O(n) времени. В других другими словами, добавление элемента является амортизированной операцией постоянного времени. Это достигается за счет того, что каждое перераспределение увеличивает размер массива экспоненциально, как правило, в1.5. При таком подходе общее количество операций может быть O(n).


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

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

P.S.-Как говорит AmitG, фактор расширения специфичен для реализации (в этом случае (oldCapacity * 3)/2 + 1)


размер по умолчанию 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, а затем увеличивать его.


я на самом деле написал блоге по теме 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 to 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 vs C#

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

ссылки


установка начального размера 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, его емкость растет автоматически. Подробности роста политики не указываются, кроме того факта, что добавление элемента имеет постоянная амортизируемая стоимость времени."

очевидно, если вы понятия не имеете, как какой диапазон вы будете держать, установка размера, вероятно, не будет хорошей идеей - однако, если у вас есть определенный диапазон в виду, установка начальной емкости повысит эффективность памяти.


это делается для того, чтобы избежать возможных усилий по перераспределению для каждого отдельного объекта.

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

внутри это.
JVM требует усилий для создания new Object[] при добавлении элемента в arraylist. Если у вас нет выше кода(любой algo вы думаете) для перераспределения, то каждый раз, когда вы вызываете arraylist.add() затем new Object[] должно быть создано, что бессмысленно, и мы теряем время для увеличения размера на 1 для каждого добавляемого объекта. Так что лучше увеличить размер Object[] С помощью следующей формулы.
(JSL использовал формулу forcasting, приведенную ниже, для динамически растущего arraylist вместо роста на 1 каждый раз. Потому что для роста требуется усилие со стороны JVM)

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

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


ArrayList может содержать много значений, и при выполнении больших начальных вставок вы можете сказать ArrayList выделить большее хранилище для начала, чтобы не тратить циклы процессора, когда он пытается выделить больше места для следующего элемента. Таким образом, выделить некоторое пространство в начале более эффективно.


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

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


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


Я тестировал 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