В ожидании Java 16: Stream.toList() и другие методы преобразования

Только самообслуживание 

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

Все 40 лет  —  ежедневное полноценное меню на ужин 

В отличие от Java, Smalltalk всегда располагал методами преобразования для типов коллекций, которые позволяют трансформировать один тип в другой с помощью имени метода, отражающего намерение. Все они начинались с префикса as. Представленная ниже диаграмма связей отображает методы преобразования, доступные в API коллекций Smalltalk уже на протяжении 40 лет. 

Методы преобразования Smalltalk

Все эти годы перечисленные методы служили верой и правдой разработчикам 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

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. Согласованность и ясность намного важнее краткости. 

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Donald Raab: Stream.toList() and other converter methods I’ve wanted since Java 2

Предыдущая статьяСовмещение Typescript и GraphQL Code Generator
Следующая статьяЗа гранью HCD: нужен ли новый подход в дизайне для ИИ?