Коллекции emptyList / singleton / singletonList / List / Set toArray

Предположим, у меня есть такой код:

String[] left = { "1", "2" };
String[] leftNew = Collections.emptyList().toArray(left);
System.out.println(Arrays.toString(leftNew));

Это напечатает [null, 2]. Этот вроде, как бы, что-то вроде имеет смысл, поскольку у нас есть пустой список, он каким-то образом должен справиться с тем фактом, что мы передаем массив большего размера и устанавливаем первый элемент в нуль. Вероятно, это означает, что первый элемент не существует в пустом списке, поэтому он установлен на null.

Но это все еще сбивает с толку, поскольку мы передаем массив с определенным типом только для того, чтобы помочь вывести тип массива вернулся; но в любом случае в этом есть хоть какая-то логика. Но что, если я сделаю:

String[] right = { "nonA", "b", "c" };
// or Collections.singletonList("a");
// or a plain List or Set; does not matter
String[] rightNew = Collections.singleton("a").toArray(right);
System.out.println(Arrays.toString(rightNew));

Взяв за образец предыдущий пример, я ожидаю, что он покажет:

["a", "b", "c"]

Но, что для меня немного неожиданно, он печатает:

[a, null, c]

И, конечно же, я перехожу к документации, в которой явно сказано, что этого ожидают:

If this set fits in the specified array with room to spare (i.e., the array has more elements than this set), the element in the array immediately following the end of the set is set to null.

Хорошо, это хотя бы задокументировано. Но позже говорится:

This is useful in determining the length of this set only if the caller knows that this set does not contain any null elements.

Это часть документации, которая меня больше всего смущает: |

И еще более забавный пример, который для меня не имеет большого смысла:

String[] middle = { "nonZ", "y", "u", "m" };
List<String> list = new ArrayList<>();
list.add("z");
list.add(null);
list.add("z1");
System.out.println(list.size()); // 3

String[] middleNew = list.toArray(middle);
System.out.println(Arrays.toString(middleNew));

Это напечатает:

[z, null, z1, null]

Итак, он очищает последний элемент из массива, но почему бы этого не сделать в первом примере?

Может кто-нибудь пролить здесь свет?

Я не понимаю вопроса. Все они устанавливают для следующего элемента значение null. Где непоследовательность?

shmosel 17.08.2018 22:33

@shmosel нет противоречий, реальный вопрос в том, Зачем происходит ли это так, и есть ли (и может быть) код, который действительно использует это?

Eugene 17.08.2018 22:34

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

shmosel 17.08.2018 22:39

@shmosel, как насчет этого ответа и комментариев? stackoverflow.com/a/51902457/1059372 имеет ли это больше смысла? Думаю, это ответ на мой вопрос ...

Eugene 17.08.2018 22:57

Я все еще не совсем понимаю, в чем ваш вопрос. Если вы спрашиваете, как полезно установить для следующего элемента значение null, если есть предыдущие нули, ну, это не так. Именно об этом говорится в документации во второй цитате.

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

Ответы 4

Он очистит только элемент в индексе сразу после, последний элемент в исходном списке, поэтому в первом примере список пуст, следовательно, он обнуляет элемент с нулевым индексом (первый элемент, который является "1").

В вашем последнем примере так получилось, что последний элемент находится сразу после последнего элемента в исходном списке. Зная, что последний сценарий на самом деле не поможет в определении размера списка, потому что сделал допускает нулевые значения.

Но если список не допускает null (например, неизменяемые списки, представленные в Java 9), то это полезно, потому что в случае, если вы перебираете возвращаемый массив, вы не хотели бы обрабатывать лишние элементы, и в этом случае вы можете остановить итератор на первом нулевом элементе.

вы имеете смысл, поэтому вы говорите, что если бы этот конкретный List/Set не допускал null, и вы бы вызывали toArray с array большего размера, чем List, при циклическом обходе array, как только вы встретите первый ноль, вы бы уверен, что вы сделали. Именно так обстоит дело с неизменяемыми коллекциями в java-9, которые запрещают нули. Если бы вы добавили эту часть, я бы принял ее

Eugene 17.08.2018 22:51

но это очень сбивает с толку для ArrayList (который принимает нули), и когда вы встречаетесь с первым нулем, ну, вы понятия не имеете, закончили ли вы. Ну, по крайней мере, в документации это очень понятно.

Eugene 17.08.2018 22:52

@Eugene Да, вы правы, поэтому он говорит: «Только, если вызывающий знает, что этот набор не содержит никаких нулевых элементов».

M A 17.08.2018 22:59

@Eugene Ничего страшного, ответ Стюарта действительно лучше охватывает это и исходит от оригинальных разработчиков.

M A 18.08.2018 12:10

Из исходного кода JDK 9 для ArrayList:

@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

а в Arrays.ArrayList реализация List, возвращенная Arrays.asList:

@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
    int size = size();
    if (a.length < size)
        return Arrays.copyOf(this.a, size,
                             (Class<? extends T[]>) a.getClass());
    System.arraycopy(this.a, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

Если размер списка, который нужно преобразовать в массив, равен size, то они оба устанавливают a[size] в null.

В пустом списке size - это 0, поэтому для a[0] установлено значение null, а другие элементы не затрагиваются.

В одноэлементном списке size - это 1, поэтому для a[1] устанавливается значение null, а другие элементы не затрагиваются.

Если размер списка на единицу меньше длины массива, a[size] относится к последнему элементу массива, поэтому он устанавливается на null. В вашем примере у вас есть null во второй позиции (индекс 1), поэтому он установлен на null в качестве элемента. Если бы кто-то искал null для подсчета элементов, они бы остановились здесь, а не на другом null, которым является null, полученный в результате установки следующего элемента вне содержимого списка на null. Эти null нельзя отличить друг от друга.

Вы правы, это просто то, что другой ответ (мудрый многословие) ближе к моему пониманию. Спасибо!

Eugene 17.08.2018 22:59

Код toArray (T [] a) из (например) ArrayList довольно ясен:

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

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

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

Метод <T> T[] toArray(T[] a) в Collection странный, потому что он пытается выполнить две цели одновременно.

Сначала посмотрим на toArray(). Это берет элементы из коллекции и возвращает их в Object[]. То есть типом компонента возвращаемого массива всегда является Object. Это полезно, но не подходит для пары других вариантов использования:

1) Вызывающий хочет повторно использовать существующего массива, если это возможно; и

2) Вызывающий хочет указать тип компонента возвращаемого массива.

Обработка случая (1) оказывается довольно тонкой проблемой API. Вызывающий хочет повторно использовать массив, поэтому его явно необходимо передать. В отличие от метода toArray() без аргументов, который возвращает массив нужного размера, если массив вызывающего абонента используется повторно, нам нужно чтобы вернуть количество скопированных элементов. Хорошо, давайте получим API, которое выглядит так:

int toArray(T[] a)

Вызывающий передает массив, который используется повторно, а возвращаемое значение - это количество элементов, скопированных в него. Массив не нужно возвращать, потому что у вызывающего уже есть ссылка на него. Но что, если массив слишком мал? Ну, может, выбросить исключение. Фактически, это то, что делает Vector.copyInto.

void copyInto​(Object[] anArray)

Это ужасный API. Он не только не возвращает количество скопированных элементов, но и выдает IndexOutOfBoundsException, если целевой массив слишком короткий. Поскольку Vector является параллельной коллекцией, размер может измениться в любое время до вызова, поэтому вызывающий не можешь гарантирует, что целевой массив имеет достаточный размер, и не может знать количество скопированных элементов. Единственное, что может сделать вызывающий, - это заблокировать вектор вокруг всей последовательности:

synchronized (vec) {
    Object[] a = new Object[vec.size()];
    vec.copyInto(a);
}

Фу!

API Collections.toArray(T[]) позволяет избежать этой проблемы за счет другого поведения, если целевой массив слишком мал. Вместо того, чтобы генерировать исключение, подобное Vector.copyInto (), он выделяет массив новый нужного размера. Это позволяет отказаться от повторного использования массива в пользу более надежной работы. Теперь проблема в том, что вызывающий не может определить, был ли его массив повторно использован или был выделен новый. Таким образом, возвращаемое значение toArray(T[]) должно возвращать массив: массив аргументов, если он был достаточно большим, или вновь выделенный массив.

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

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

Я не думаю, что когда-либо видел код, который повторно использует массивы или таким образом проверяет наличие нулей. Вероятно, это пережиток первых дней, когда выделение памяти и сборка мусора были дорогостоящими, поэтому люди хотели повторно использовать память в максимально возможной степени. Совсем недавно принятой идиомой для использования этого метода был второй вариант использования, описанный выше, то есть, чтобы установить желаемый тип компонента массива следующим образом:

MyType[] a = coll.toArray(new MyType[0]);

(Кажется расточительным выделять для этой цели массив нулевой длины, но оказывается, что это распределение может быть оптимизировано JIT-компилятором, а очевидная альтернатива toArray(new MyType[coll.size()]) на самом деле работает медленнее. Это связано с необходимостью инициализировать массив в нули, а затем заполнить его содержимым коллекции (см. статью Алексея Шипилева на эту тему, Массивы мудрости древних.)

Однако многим кажется, что массив нулевой длины противоречит здравому смыслу. В JDK 11 есть новый API, который позволяет вместо этого использовать ссылку на конструктор массива:

MyType[] a = coll.toArray(MyType[]::new);

Это позволяет вызывающей стороне указать тип компонента массива, но позволяет коллекции предоставлять информацию о размере.

У нас больше нет способа сообщить вызывающей стороне количество элементов, скопированных из коллекции в массив. Почему бы не проверить размер исходной коллекции?
shmosel 19.08.2018 10:19
В JDK 11 есть новый API, который позволяет вместо этого использовать ссылку на конструктор массива. Сладкое. Разве это не было отклонено в Java 8 по соображениям совместимости?
shmosel 19.08.2018 10:20

@shmosel Почему бы не проверить размер исходной коллекции? Вызывающий может вызвать size () до или после вызова toArray (). Однако значение размера может не соответствовать фактическому количеству элементов, скопированных toArray (), если другой поток изменил исходную коллекцию. Только toArray () знает точное количество скопированных элементов. Конечно, это не проблема, если вы однопоточный, но в общем случае источником является параллельная коллекция.

Stuart Marks 19.08.2018 18:26

@shmosel Разве это не было отклонено в Java 8 по соображениям совместимости? Я так не думаю. Он выпал из 8, потому что у нас не хватило времени. В 9 или 10 периодах разработки было обсуждение того, является ли это наиболее естественным API и о производительности - очевидное переопределение на самом деле оказалось медленнее! Мы преодолели эти проблемы во время разработки 11.

Stuart Marks 19.08.2018 18:31

Нашел: mail.openjdk.java.net/pipermail/lambda-libs-spec-experts/…

shmosel 19.08.2018 18:37

@shmosel Ах да. Существует несовместимость источников, но не настолько фундаментальная, что сама по себе помешала бы добавлению API. Эта функция была исключена из 8, потому что обновление тестов в ответ на несовместимость потребовало бы дополнительной работы в разных командах, и просто не было времени на ее координацию. Когда мы добавили эту функцию для 11, мы столкнулись с приложением, на которое повлияла несовместимость. Это просто то, с чем нужно разобраться во время миграции.

Stuart Marks 20.08.2018 23:19

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