Источник: Nuances of Programming
Только самообслуживание
В 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 . Согласованность и ясность намного важнее краткости.
Читайте также:
Перевод статьи Donald Raab : Stream.toList() and other converter methods I’ve wanted since Java 2