Мне трудно переварить этот конкретный блок кода из метода java.util.PriorityQueue#initElementsFromCollection.
/**
* Initializes queue array with elements from the given Collection.
*
* @param c the collection
*/
private void initFromCollection(Collection<? extends E> c) {
initElementsFromCollection(c);
heapify();
}
private void initElementsFromCollection(Collection<? extends E> c) {
Object[] es = c.toArray();
int len = es.length;
if (c.getClass() != ArrayList.class)
es = Arrays.copyOf(es, len, Object[].class);
if (len == 1 || this.comparator != null)
for (Object e : es)
if (e == null)
throw new NullPointerException();
this.queue = ensureNonEmpty(es);
this.size = len;
}
Я могу понять, что код здесь пытается построить кучу из элементов коллекции, предоставленных через конструктор, но почему они проверяют тип класса аргумента Collection
против ArrayList
и снова копируют элементы, которые уже были скопированы в Object[] es
с помощью toArray
?
if (c.getClass() != ArrayList.class)
es = Arrays.copyOf(es, len, Object[].class);
В java.util.Arrays#copyOf(U[], int, java.lang.Class<? extends T[]>)
происходит что-то волшебное?
java-11
Конструктор (точнее его автор) не доверяет входящей коллекции. Метод коллекции toArray
может нарушить контракт и вернуть общий массив вместо создания нового. Таким образом, вызывающая сторона может получить доступ к внутренне используемому массиву созданного экземпляра PriorityQueue
.
Таким образом, конструктор делает еще одну защитную копию, за исключением случаев, когда входящая коллекция является именно ArrayList
, то есть даже не ее подклассом. Другими словами, он доверяет реализации ArrayList
toArray
соблюдения контракта, чтобы пропустить дополнительный шаг копирования в этом конкретном случае. Вот почему даже подкласс ArrayList
не будет принят, поскольку подкласс мог переопределить метод toArray
.
Аналогичное недоверие было показано в реализации Stream.toList()
по умолчанию, как обсуждалось в этот вопрос и разделе комментариев под этот вопрос.
Реализация по умолчанию была указана как
default List<T> toList() {
return (List<T>) Collections.unmodifiableList(new ArrayList<>(Arrays.asList(this.toArray())));
}
потому что он не доверяет реализации toArray()
сторонних реализаций Stream
(в любом случае реализация JDK переопределяет метод toList()
).
Штраф за эту паранойю выплачивается даже дважды. В прошлом конструктор ArrayList
доверял реализации toArray()
любой входящей коллекции, но сегодня это выглядит так:
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
Делая то же самое, что вы видели в конструкторе PriorityQueue
, доверяйте toArray()
только тогда, когда коллекция точно является ArrayList
.
Поскольку в случае toList()
входящая коллекция является результатом Arrays.asList(…)
, то есть не ArrayList
, метод toArray()
создаст копию, а затем конструктор ArrayList
создаст другую копию. Все это, хотя исходный массив в любом случае является новым массивом, для хорошо работающих Stream
реализаций…
Мы можем обобщить проблему. Идиоматическая инициализация коллекций, таких как new ArrayList<>(Arrays.asList(…))
или new ArrayList<>(List.of(…))
или аналогично для PriorityQueue
, получает штраф в виде защитной копии, несмотря на то, что используются только коллекции JDK, которые должны иметь правильную toArray
реализацию.
Я бы, вероятно, использовал c.getClass().getClassLoader() != null
вместо c.getClass() != ArrayList.class
, чтобы доверять всем встроенным коллекциям, реализация которых была загружена загрузчиком начальной загрузки. Но, возможно, этот тест окажется дороже ArrayList
теста¹ или слишком много ненадежных встроенных коллекций…
¹ Ключевым моментом является то, достаточно ли хорошо оптимизатор понимает конструкцию, чтобы предсказать ее результат.
Это была старая логика. Было легко проверить, неправильно ли toArray
возвращает массив, отличный от Object[]
. Но проверить, является ли массив неправильно общим массивом, было бы дорого. Таким образом, он просто предполагает, что им можно поделиться, если только не известно, что он связан с надежной реализацией. По иронии судьбы, в прошлом реализация, которая при определенных обстоятельствах неправильно возвращала массив, не являющийся Object[]
, была классом JDK, то есть списком, возвращаемым Array.asList(…)
.
Понятно. Цените свои усилия. Это как «Если c.toArray неправильно не возвращает Object[], скопируйте его».