Только самообслуживание
В 2004 году я работал архитектором ПО на Java в крупной финансовой компании. На тот момент в этом языке отсутствовало большинство эффективных функциональностей коллекций, которые свободно предоставлялись в Smalltalk. Я решил не ждать у моря погоды и самостоятельно приступил к созданию первых утилитных классов, впоследствии ставших частью открытой библиотеки Java под названием Eclipse Collections.
Все 40 лет — ежедневное полноценное меню на ужин
В отличие от Java, Smalltalk всегда располагал методами преобразования для типов коллекций, которые позволяют трансформировать один тип в другой с помощью имени метода, отражающего намерение. Все они начинались с префикса as. Представленная ниже диаграмма связей отображает методы преобразования, доступные в API коллекций Smalltalk уже на протяжении 40 лет.

Все эти годы перечисленные методы служили верой и правдой разработчикам Smalltalk. В 1990-х я тоже не раз прибегал к их помощи.
Smalltalk -> аналоги Java
asArray->toArrayasDictionary->Collectors.toMapasOrderedCollection->Collectors.toListasSet->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,648Collectors.toSet()->283,944Collectors.toMap()->169,753Collectors.toCollection()->61,831Collectors.counting()->14,765(аналогtoBag)Collectors.toUnmodifiableList()->4,712Collectors.toUnmodifiableSet()->2,064Collectors.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





