Почему ArrayList создается с массивом пустых элементов, но HashSet с нулевой таблицей?

#java #c#

#java #c#

Вопрос:

Может быть, немного философский вопрос.

Глядя на реализацию Java в ArrayList, я заметил, что при создании нового экземпляра внутренний массив «elementData» (который содержит элементы) создается как новый пустой массив:

 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
  

Однако HashSet (который основан на HashMap) создается с таблицей, а entreySet просто остается null;

 transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
  

Это заставило меня задуматься, поэтому я пошел и посмотрел список C # и HashSet:
https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,61f6a8d9f0c40f6e
https://referencesource.microsoft.com/#System.Core/System/Collections/Generic/HashSet.cs,2d265edc718b158b

Список:

 static readonly T[]  _emptyArray = new T[0]; 

public List() {
        _items = _emptyArray;
}
  

HashSet:

 private int[] m_buckets;

public HashSet()
        : this(EqualityComparer<T>.Default) { }

public HashSet(IEqualityComparer<T> comparer) {
    if (comparer == null) {
        comparer = EqualityComparer<T>.Defau<
    }

    this.m_comparer = comparer;
    m_lastIndex = 0;
    m_count = 0;
    m_freeList = -1;
    m_version = 0;
}
  

Итак, есть веская причина, по которой оба языка выбрали empty для list и null для set / map?

Они оба использовали «единый экземпляр» для трюка с пустым массивом, что неплохо, но почему бы просто не иметь нулевой массив?

Ответ №1:

Отвечаю с точки зрения C #.

Для пустой ArrayList вы обнаружите, что вся логика (получение, добавление, рост, …) работает «как есть», если у вас есть пустой массив в качестве резервного хранилища. Нет необходимости в дополнительном коде для обработки неинициализированного регистра, это делает всю реализацию более аккуратной. И поскольку пустой массив кэшируется, это не приводит к дополнительному выделению кучи, поэтому вы получаете более чистый код без дополнительных затрат.

Для HashSet это невозможно, поскольку доступ к корзине осуществляется через формулу hashCode % m_buckets.Length . Попытка вычисления % 0 рассматривается как деление на 0 и, следовательно, недопустима. Это означает, что вам нужно конкретно обработать случай «не инициализированный», поэтому вы ничего не получите от предварительного присвоения полю пустого массива.

Ответ №2:

Инициализация elementData пустого массива в ArrayList позволяет избежать null проверки в grow(int minCapacity) методе, который вызывает:

 elementData = Arrays.copyOf(elementData, newCapacity);
  

для увеличения емкости резервного массива. При первом вызове этого метода этот оператор «скопирует» пустой массив в начало нового массива (на самом деле он ничего не скопирует).

В HashMap подобная стратегия не была бы полезной, поскольку при изменении размера массива сегментов вы не копируете исходный массив в начало нового массива, вам приходится перебирать все записи и находить новую ячейку для каждой записи. Поэтому инициализация массива buckets с пустым массивом вместо сохранения его нулевым значением потребует от вас проверки, равна ли длина массива == 0, вместо проверки, равно ли оно нулю. Замена одного условия другим не была бы полезной.