Коллекции emptyList/синглтон/singletonList/список / Set toArray

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

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

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

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

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]

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

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

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

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

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

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

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]

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

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

4 ответов


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

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

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

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

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

int toArray(T[] a)

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

void copyInto​(Object[] anArray)

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

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

тьфу!

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

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

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

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

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

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

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

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

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


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

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

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


из исходного кода 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] to null.

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

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

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


код 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;
}

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