openjdk.ruOpenJDK на русском

JEP 269: Convenience Factory Methods for Collections

Удобные фабричные методы для коллекций

ОтветственныйStuart Marks
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск9
Компонентcore-libs / java.util:collections
Обсуждениеcore dash libs dash dev at openjdk dot java dot net
ТрудоёмкостьS
ДлительностьS
Связан сJEP 186: Collection Literals
РецензентыBrian Goetz, Chris Hegarty, Paul Sandoz
ОдобренBrian Goetz
Создан2014/06/26 23:05
Обновлён2017/06/26 21:31
Задача8048330

Аннотация

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

Цели

Добавить в интерфейсы коллекций статические фабричные методы, которые будут создавать компактные неизменяемые экземпляры коллекций. API намеренно сделан минимальным.

Что не является целью

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

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

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

Не является целью создание «неизменяемых персистентных» или «функциональных» коллекций.

Мотивация

Java часто критикуют за многословность. Чтобы создать небольшую неизменяемую коллекцию (скажем, множество), нужно сконструировать её, сохранить в локальной переменной, несколько раз вызвать для неё add(), а затем обернуть её. Например,

Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");
set = Collections.unmodifiableSet(set);

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

Set<String> set = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("a", "b", "c")));

Это всё ещё несколько многословно и к тому же менее очевидно, поскольку прежде чем создать Set, нужно создать List. Ещё один вариант — использовать так называемый приём «двойных фигурных скобок»:

Set<String> set = Collections.unmodifiableSet(new HashSet<String>() {{
    add("a"); add("b"); add("c");
}});

Здесь используется конструкция инициализатора экземпляра в анонимном внутреннем классе, что выглядит немного симпатичнее. Однако этот приём довольно малопонятен, и каждое его использование обходится в лишний класс. Кроме того, он хранит скрытые ссылки на объемлющий экземпляр и на все захваченные объекты. Это может приводить к утечкам памяти или проблемам с сериализацией. По этим причинам этого приёма лучше избегать.

Для создания небольших коллекций можно использовать Stream API из Java 8, сочетая фабричные методы потоков и коллекторы. Например,

Set<String> set = Collections.unmodifiableSet(Stream.of("a", "b", "c").collect(toSet()));

(Коллекторы потоков не дают никаких гарантий относительно изменяемости возвращаемых ими коллекций. В Java 8 возвращаются обычные изменяемые коллекции, такие как ArrayList, HashSet и HashMap, но в будущих выпусках JDK это может измениться.)

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

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

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

Set<String> set = Set.of("a", "b", "c");

В классе Collections уже есть фабрики для создания пустых List, Set и Map. Есть также фабрики для создания одноэлементных List, Set и Map, содержащих ровно один элемент или одну пару «ключ — значение». EnumSet содержит несколько перегруженных методов of(...), принимающих фиксированное или переменное число аргументов, для удобного создания EnumSet с указанными элементами. Однако хорошего универсального способа создавать List, Set и Map, содержащие объекты произвольных типов, нет.

В классе Collections есть комбинирующие методы для создания неизменяемых List, Set и Map. Они не создают коллекции, неизменяемые по своей природе. Вместо этого они принимают другую коллекцию и оборачивают её в класс, отклоняющий запросы на изменение, тем самым создавая неизменяемое представление исходной коллекции. Имея ссылку на исходную коллекцию, её по-прежнему можно изменить. Каждая обёртка — это дополнительный объект, требующий ещё одного уровня косвенности и потребляющий больше памяти, чем исходная коллекция. Наконец, обёрнутая коллекция по-прежнему несёт издержки поддержки изменений, даже если её никогда не предполагается изменять.

Описание

Добавить в интерфейсы List, Set и Map статические фабричные методы для создания неизменяемых экземпляров этих коллекций. (Обратите внимание, что, в отличие от статических методов классов, статические методы интерфейсов не наследуются, поэтому вызвать их через реализующий класс или через экземпляр типа интерфейса будет нельзя.)

Для List и Set эти фабричные методы работали бы так:

List.of(a, b, c);
Set.of(d, e, f, g);

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

Для Map будет предоставлен набор методов с фиксированным числом аргументов:

Map.of()
Map.of(k1, v1)
Map.of(k1, v1, k2, v2)
Map.of(k1, v1, k2, v2, k3, v3)
...

Мы ожидаем, что поддержки небольших отображений, содержащих до десяти пар «ключ — значение», будет достаточно для большинства сценариев использования. Для большего числа записей будет предоставлен API, создающий экземпляр Map по произвольному числу пар «ключ — значение»:

Map.ofEntries(Map.Entry<K,V>...)

Хотя этот подход аналогичен соответствующим API с varargs для List и Set, он, к сожалению, требует упаковывать каждую пару «ключ — значение». Сделать это удобнее позволит метод для упаковки ключей и значений, пригодный для статического импорта:

Map.Entry<K,V> entry(K k, V v)

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

Map.ofEntries(
    entry(k1, v1),
    entry(k2, v2),
    entry(k3, v3),
    // ...
    entry(kn, vn));

(Возможно, в будущей версии JDK издержки упаковки удастся уменьшить с помощью value-типов. Удобный метод entry() на самом деле будет возвращать новый конкретный тип, реализующий Map.Entry, чтобы облегчить возможный будущий переход на value-тип.)

API для создания небольших неизменяемых коллекций покрывают большое число сценариев использования и помогают сохранить простоту спецификации и реализаций. Неизменяемые коллекции избавляют от необходимости делать защитные копии и лучше подходят для параллельной обработки.

Важное соображение — также объём памяти, занимаемый небольшими коллекциями во время выполнения. Если просто создать неизменяемый HashSet из двух элементов с помощью API-обёрток, он будет состоять из шести объектов: обёртки, HashSet, содержащего HashMap, его таблицы корзин (массива) и одного экземпляра Node на каждый элемент. Это огромные накладные расходы по сравнению с объёмом хранимых данных, а доступ к данным неизбежно требует нескольких вызовов методов и разыменований указателей. Реализации, рассчитанные на небольшие коллекции фиксированного размера, могут избежать большей части этих расходов за счёт компактного размещения на основе полей или массива. Экономии памяти способствует и то, что не нужно поддерживать изменения (а размер коллекции известен в момент создания).

Конкретные классы, возвращаемые этими фабриками, не будут доступны как публичные API. Никаких гарантий относительно типа во время выполнения или Identity (идентичность объекта) возвращаемой коллекции не даётся. Это позволит со временем менять реализации, не нарушая совместимости. Единственное, на что вызывающий код должен полагаться, — что возвращаемая ссылка является реализацией соответствующего интерфейсного типа.

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

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

Ожидается, что реализации List будут обеспечивать быстрый доступ к элементам по индексу, поэтому они будут реализовывать маркерный интерфейс RandomAccess.

Элементы, хранящиеся в этих коллекциях, должны поддерживать обычные контракты коллекций, в том числе корректно поддерживать hashCode() и equals(). Если элемент Set или ключ Map изменится так, что это повлияет на его методы hashCode() или equals(), поведение коллекции может стать неопределённым.

После создания и безопасной публикации эти экземпляры коллекций будут безопасны для одновременного доступа из нескольких потоков.

В JDK будет проведён поиск мест, где можно использовать эти новые API. Эти места будут переведены на новые API, насколько позволят время и график.

Альтернативы

Изменения языка рассматривались несколько раз и были отклонены:

  1. Предложение для Project Coin, 29 марта 2009
  2. Предложение для Project Coin, 30 марта 2009
  3. Обсуждение JEP 186 в рассылке lambda-dev, январь–март 2014

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

В библиотеках Google Guava есть богатый набор утилит для создания неизменяемых коллекций, включая шаблон «строитель», и для создания самых разных изменяемых коллекций. Библиотеки Guava очень полезны и универсальны, но для включения в платформу Java SE они, пожалуй, избыточны. Это предложение похоже на предложение Stephen Colebourne lambda-dev, 19 февраля 2014 и включает некоторые идеи из фабричных методов неизменяемых коллекций Guava.

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

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

Есть и ещё одна сложность: статические методы классов наследуются подклассами. Предположим, что был бы добавлен статический фабричный метод HashMap.of(). Поскольку LinkedHashMap — подкласс HashMap, код приложения мог бы вызвать LinkedHashMap.of(). В итоге был бы вызван HashMap.of(), а этого никто не ожидает! Один из способов смягчить проблему — обеспечить, чтобы у всех конкретных реализаций коллекций был одинаковый набор фабричных методов и наследования не происходило. Для пользовательских подклассов конкретных коллекций наследование всё равно оставалось бы проблемой.

Тестирование

В набор регрессионных тестов JDK будут входить обычные модульные тесты, а для публичных API будут тесты JCK. Сериализованные формы также могут быть покрыты JCK.

Будет разработан набор тестов размера и производительности. В отличие от обычной цели — сравнения с базовыми измерениями, эти тесты будут сравнивать новые реализации коллекций с существующими. Ожидается, что новые коллекции будут занимать меньше места в куче как по фиксированным накладным расходам, так и в расчёте на один элемент. Однако в некоторых случаях новые коллекции могут работать медленнее из-за внутреннего представления, отличающегося от представления существующих коллекций. Любое такое замедление должно быть разумным. Хотя конкретных целей по производительности нет, замедление в 10 раз недопустимо. Кроме того, производительность новых коллекций должна оставаться стабильной при увеличении числа элементов. Наконец, измерения зададут базовые показатели производительности, с которыми следует сравнивать будущие изменения.