Метод Java PriorityQueue initElementsFromCollection

Мне трудно переварить этот конкретный блок кода из метода 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

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
3
0
40
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

Таким образом, конструктор делает еще одну защитную копию, за исключением случаев, когда входящая коллекция является именно ArrayList, то есть даже не ее подклассом. Другими словами, он доверяет реализации ArrayListtoArray соблюдения контракта, чтобы пропустить дополнительный шаг копирования в этом конкретном случае. Вот почему даже подкласс 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 теста¹ или слишком много ненадежных встроенных коллекций…

¹ Ключевым моментом является то, достаточно ли хорошо оптимизатор понимает конструкцию, чтобы предсказать ее результат.

Понятно. Цените свои усилия. Это как «Если c.toArray неправильно не возвращает Object[], скопируйте его».

Kumar Ashutosh 23.03.2022 16:29

Это была старая логика. Было легко проверить, неправильно ли toArray возвращает массив, отличный от Object[]. Но проверить, является ли массив неправильно общим массивом, было бы дорого. Таким образом, он просто предполагает, что им можно поделиться, если только не известно, что он связан с надежной реализацией. По иронии судьбы, в прошлом реализация, которая при определенных обстоятельствах неправильно возвращала массив, не являющийся Object[], была классом JDK, то есть списком, возвращаемым Array.asList(…).

Holger 23.03.2022 16:44

Другие вопросы по теме