Только самообслуживание
В 2004 году я работал архитектором ПО на Java в крупной финансовой компании. На тот момент в этом языке отсутствовало большинство эффективных функциональностей коллекций, которые свободно предоставлялись в Smalltalk. Я решил не ждать у моря погоды и самостоятельно приступил к созданию первых утилитных классов, впоследствии ставших частью открытой библиотеки Java под названием Eclipse Collections.
Все 40 лет — ежедневное полноценное меню на ужин
В отличие от Java, Smalltalk всегда располагал методами преобразования для типов коллекций, которые позволяют трансформировать один тип в другой с помощью имени метода, отражающего намерение. Все они начинались с префикса as
. Представленная ниже диаграмма связей отображает методы преобразования, доступные в API коллекций Smalltalk уже на протяжении 40 лет.
Все эти годы перечисленные методы служили верой и правдой разработчикам Smalltalk. В 1990-х я тоже не раз прибегал к их помощи.
Smalltalk -> аналоги Java
asArray
->toArray
asDictionary
->Collectors.toMap
asOrderedCollection
->Collectors.toList
asSet
->Collectors.toSet
В Java существует префикс to
для методов преобразования в классе Collectors
и для toArray
в Collection
и Stream
. В настоящее время другие доступные в Smalltalk методы не имеют аналогов в Collectors
.
Почему 40 лет назад в Smalltalk были добавлены симметричные методы преобразования для List
, Bag
,Set
, Map
, IdentitySet
, OrderedMap
, SortedList
? Да потому, что предоставляемые ими преимущества намного превосходят стоимость каждого из них.
Планирование идеального меню на ужин в течение 7 лет
К тому моменту, когда в марте 2021 года на свет появится Java 16, минует уже 7 лет со дня выпуска Java 8 с лямбда-выражениями и Stream. Новый метод Stream.toList()
в Java 16 без своего семейства и дружеского окружения (toSet
, toMap
, toCollection
) отчасти станет разочарованием.
При беглом просмотре GitHub я насчитал следующее количество встретившихся мне методов Collectors.toList()
, Collectors.toSet()
, Collectors.toMap()
и Collectors.toCollection()
:
Collectors.toList()
->1,363,648
Collectors.toSet()
->283,944
Collectors.toMap()
->169,753
Collectors.toCollection()
->61,831
Collectors.counting()
->14,765
(аналогtoBag
)Collectors.toUnmodifiableList()
->4,712
Collectors.toUnmodifiableSet()
->2,064
Collectors.toUnmodifiableMap()
->1,386
Как видно, toList()
— самый распространенный из них, что и неудивительно. Однако и число случаев применения других методов преобразования превышает отметку в полмиллиона. И речь идет лишь о результате поиска проектов на GitHub.
При этом на данном веб-сервисе нет большинства корпоративных приложений. Если бы у нас была возможность проверить все частные репозитории Java-кода в мире, то мы бы, наверняка, обнаружили в десятки раз больше случаев их использования.
После того, как toList
будет добавлен в Stream JDK 16, сколько месяцев или лет еще должно пройти, прежде чем появятся toSet
, toMap
и toCollection
?
Выбор блюд на ужин по принципу Хобсона
Когда Java 16 подарит Stream.toList()
, каждый вечер мы сможем выбирать одно блюдо на ужин, задействуя любой метод преобразования в Stream
, если это toList
. То есть перед нами не что иное, как наглядный пример выбора Хобсона.
Если же потребуется другой метод преобразования, можно будет воспользоваться collect
и Collectors
для иных типов коллекций. Такой подход приведет к нежелательной асимметрии в применении Stream
API. Качество реализации Collectors
для других типов будет уступать эффективности метода toList
.
Изменяемый или неизменяемый или немодифицируемый
Как корабль назовешь, так он и поплывет. А придумать хорошее название непросто. List
— это изменяемый интерфейс. Также существуют “немодифицируемые” реализации, которые делают List
“условно” изменяемым. Это аналогично его “синхронизированным” версиям, которые обеспечивают “условную потокобезопасность”.
В случае с любыми условными категориями их потенциальную условность трактует и обрабатывает сам разработчик. Я бы ожидал, что метод с именем toList
будет возвращать изменяемый тип.
При этом имя не указывает на возвращение немодифицируемой или неизменяемой реализации. Вызывающий компонент должен проигнорировать имя и возвращаемый тип, обращая внимание на спецификацию в Javadoc или непосредственно просматривая код реализации для понимания поведения, поддерживаемого возвращаемым результатом.
Поскольку возвращается немодифицируемая реализация, метод был бы намного понятнее, назови мы его toUnmodifiableList
. Это имя отлично бы соотносилось с Collectors.toUnmodifiableList()
. На самом деле Stream.toList
больше похож на Collectors.toUnmodifiabList
. Если бы toList
действительно возвращал изменяемый List
, он бы составил хорошую симметричную пару с Collectors.toList()
.
Stream.toCollection = шведский стол
С практической точки зрения было бы очень выгодно добавить метод toCollection
в Stream
. Collectors.toCollection
принимает Supplier
, который возвращает подтип Collection
. Фактический возвращаемый тип определяется разработчиком, при этом все реализации должны быть изменяемыми.
<T, R extends Collection<T>> R toCollection(Supplier<R> supplier)
В этом случае Stream
будет прекрасно работать со всеми типами Collection
в мире. Он также будет согласован с аналогичным методом в Collectors
. Его реализацию можно осуществить с помощью метода по умолчанию. Для краткости я бы сократил имя до простого to
, поскольку Supplier
уже проясняет возвращаемый тип.
<T, R extends Collection<T>> R to(Supplier<R> supplier)
Многовариантное меню на ужин
Если симметрия методов преобразования имеет для вас значение, то возможность выбора все-таки есть. Вы всегда сможете воспользоваться Eclipse Collections, содержащей множество таких методов как для объектов, так и для итерируемых примитивов. В RichIterable
доступны следующие методы:
RichIterable
является родительским интерфейсом для большинства контейнеров объектных типов в Eclipse Collections.
Заключение
Имея опыт преподавания Java как новичкам, так и опытным программистам, могу сказать, что метод Stream.toList()
, который появится в JDK 16 вместе с немодифицируемым возвращаемым типом, может вызвать недопонимание.
Имя toList
не раскрывает сути намерения. А вот вариант toUnmodifiableList
привел бы его в соответствие с аналогичным методом в Collectors
.
Вряд ли разработчики обрадуются, если после 7 лет ожидания вместо обещанных гамбургеров средней прожарки им предложат хорошо прожаренные с одной булочкой. Поводом для разочарования также может стать факт отсутствия рыбных или вегетарианских блюд (toSet
и toMap
).
В JDK 17 добавление в Stream
метода toCollection
вместо нового Stream.toList
подошло бы для значительно большего числа случаев. Таким образом мы избежим неоднозначности в именовании, поскольку имя полностью будет соответствовать методу toCollection
в Collectors
.
При этом разработчики получили бы практически безграничное меню возможностей. Поскольку мы в любом случае проводим с ним преобразование toList
, то следует непременно добавить этот метод для удовлетворения запросов программистов.
Eclipse Collections уже содержит много методов преобразования для объектных, простейших, последовательных, параллельных, безотложных, отложенных, изменяемых и неизменяемых API. Наличие Stream.toCollection
обеспечило бы удобное преобразование из Streams в большое число типов коллекций.
Согласитесь, что получилось бы отличное дополнение. Я буду настаивать на его включении в JDK 17. На мой взгляд, подошло бы более простое и короткое имя, но и toCollection
тоже приемлемо, учитывая его сочетаемость с Collectors.toCollection
. Согласованность и ясность намного важнее краткости.
Читайте также:
- Новый подход к пониманию RxJava
- Избегаем исключения Null Pointer Exception в Java с помощью Optional
- Графовое моделирование данных на Java
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Donald Raab: Stream.toList() and other converter methods I’ve wanted since Java 2