Коллекции 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. Вероятно, это говорит о том, что первый элемент не существует в пустом списке, поэтому он устанавливает значение 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]

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

Если этот набор подходит в указанном массиве с запасной комнатой (т.е. Массив имеет больше элементов, чем этот набор), элемент в массиве сразу после конца набора устанавливается в нуль.

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

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

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

И еще один пример funner, который не имеет для меня никакого смысла:

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]

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

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

Ответ 1

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

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

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

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

Обработка корпуса (1) оказывается довольно тонкой проблемой API. Вызывающий хочет повторно использовать массив, поэтому он явно должен быть передан. В отличие от метода no-arg 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);

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

Ответ 2

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

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

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

Ответ 3

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

@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a 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 нельзя рассказать отдельно.

Ответ 4

Код toArray (T [] a) of (например) ArrayList достаточно ясен:

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a 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;
}

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