1. Обзор
Java 8 Stream API предлагает эффективную альтернативу коллекциям Java для рендеринга или обработки результирующего набора. Тем не менее, это обычная дилемма, чтобы решить, какой из них использовать, когда.
В этой статье мы рассмотрим Stream
и Collection
и обсудим различные сценарии, соответствующие их применению.
2. Коллекция
против потока
Коллекции
Java предлагают эффективные механизмы для хранения и обработки данных, предоставляя такие структуры данных, как List
, Set
и Map
.
Однако Stream API полезен для выполнения различных операций с данными без необходимости промежуточного хранения. Таким образом, поток
работает аналогично прямому доступу к данным из базового хранилища, такого как коллекции и ресурсы ввода-вывода .
Кроме того, коллекции в первую очередь связаны с предоставлением доступа к данным и способов их изменения. С другой стороны, потоки связаны с эффективной передачей данных.
Хотя Java позволяет легко преобразовывать коллекцию
в поток
и наоборот, полезно знать, какой механизм лучше всего подходит для рендеринга/обработки результирующего набора.
Например, мы можем преобразовать коллекцию
в поток
, используя методы stream
и parallelStream :
public Stream<String> userNames() {
ArrayList<String> userNameSource = new ArrayList<>();
userNameSource.add("john");
userNameSource.add("smith");
userNameSource.add("tom");
return userNames.stream();
}
Точно так же мы можем преобразовать Stream
в Collection
, используя метод collect
Stream API :
public List<String> userNameList() {
return userNames().collect(Collectors.toList());
}
Здесь мы преобразовали поток в
список с
помощью метода Collectors.toList()
. Точно так же мы можем преобразовать поток
в набор
или в карту
:
public static Set<String> userNameSet() {
return userNames().collect(Collectors.toSet());
}
public static Map<String, String> userNameMap() {
return userNames().collect(Collectors.toMap(u1 -> u1.toString(), u1 -> u1.toString()));
}
3. Когда возвращать поток
?
3.1. Высокая стоимость материализации
Stream API предлагает отложенное выполнение и фильтрацию результатов на ходу, что является наиболее эффективным способом снижения стоимости материализации.
Например, метод readAllLines
в классе Java NIO Files
отображает все строки файла, для которых JVM должна хранить все содержимое файла в памяти. Таким образом, этот метод имеет высокую стоимость материализации, связанную с возвратом списка строк.
Однако класс Files
также предоставляет метод lines
, возвращающий Stream
, который мы можем использовать для рендеринга всех строк или, что еще лучше, для ограничения размера результирующего набора с помощью метода limit
— в обоих случаях с отложенным выполнением:
Files.lines(path).limit(10).collect(toList());
Кроме того, Stream
не выполняет промежуточные операции, пока мы не вызовем над ним терминальные операции, такие как forEach
:
userNames().filter(i -> i.length() >= 4).forEach(System.out::println);
Таким образом, Stream
позволяет избежать затрат, связанных с преждевременной материализацией.
3.2. Большой или бесконечный результат
Потоки
предназначены для лучшей производительности с большими или бесконечными результатами. Поэтому всегда полезно использовать Stream
для такого варианта использования.
Кроме того, в случае бесконечных результатов мы обычно не обрабатываем весь набор результатов. Таким образом, встроенные функции Stream API, такие как фильтр
и ограничение,
оказываются удобными при обработке желаемого набора результатов, что делает Stream
предпочтительным выбором.
3.3. Гибкость
Потоки
очень гибкие, позволяя обрабатывать результаты в любой форме и порядке.
Поток является очевидным выбором, когда мы не хотим навязывать потребителю непротиворечивый набор результатов .
Кроме того, Stream
— отличный выбор, когда мы хотим предложить столь необходимую потребителю гибкость.
Например, мы можем фильтровать/упорядочивать/ограничивать результаты, используя различные операции, доступные в Stream API:
public static Stream<String> filterUserNames() {
return userNames().filter(i -> i.length() >= 4);
}
public static Stream<String> sortUserNames() {
return userNames().sorted();
}
public static Stream<String> limitUserNames() {
return userNames().limit(3);
}
3.4. Функциональное поведение
Поток работает
. Он не допускает никаких изменений исходного кода при обработке различными способами. Поэтому предпочтительнее отображать неизменяемый результирующий набор.
Например, давайте отфильтруем
и ограничим
набор результатов, полученных от основного потока
:
userNames().filter(i -> i.length() >= 4).limit(3).forEach(System.out::println);
Здесь такие операции, как фильтрация
и ограничение
потока , каждый раз возвращают новый поток
и не изменяют исходный поток
, предоставленный методом
userNames
. ``
4. Когда возвращать коллекцию
?
4.1. Низкая стоимость материализации
Мы можем выбирать коллекции вместо потоков при рендеринге или обработке результатов с низкой стоимостью материализации.
Другими словами, Java с готовностью создает коллекцию
, вычисляя все элементы в начале. Следовательно, коллекция
с большим набором результатов оказывает большое давление на память кучи при материализации.
Следовательно, мы должны рассматривать коллекцию
для рендеринга результирующего набора, который не оказывает большого давления на динамическую память для его материализации.
4.2. Фиксированный формат
Мы можем использовать коллекцию
, чтобы обеспечить согласованный набор результатов для пользователя. Например, Collections
, такие как TreeSet
и TreeMap
, возвращают естественно упорядоченные результаты.
Другими словами, с помощью Collection
мы можем гарантировать, что каждый потребитель получит и обработает один и тот же набор результатов в идентичном порядке.
4.3. Повторно используемый результат
Когда результат возвращается в виде Collection
, его можно легко просмотреть несколько раз. Однако поток
считается потребляемым после прохождения и выдает исключение IllegalStateException
при повторном использовании
:
public static void tryStreamTraversal() {
Stream<String> userNameStream = userNames();
userNameStream.forEach(System.out::println);
try {
userNameStream.forEach(System.out::println);
} catch(IllegalStateException e) {
System.out.println("stream has already been operated upon or closed");
}
}
Таким образом, возврат коллекции
является лучшим выбором, когда очевидно, что потребитель будет просматривать результат несколько раз.
4.4. Модификация
Collection
, в отличие от Stream
, позволяет модифицировать элементы, например добавлять или удалять элементы из источника результатов. Следовательно, мы можем рассмотреть возможность использования коллекций для возврата результирующего набора, чтобы потребитель мог внести изменения.
Например, мы можем изменить ArrayList
, используя методы добавления
/ удаления :
userNameList().add("bob");
userNameList().add("pepper");
userNameList().remove(2);
Точно так же такие методы, как put
и remove
, позволяют изменять карту:
Map<String, String> userNameMap = userNameMap();
userNameMap.put("bob", "bob");
userNameMap.remove("alfred");
4.5. Результат в памяти
Кроме того, использование коллекции
является очевидным выбором, когда материализованный результат в виде коллекции уже присутствует в памяти.
5. Вывод
В этой статье мы сравнили Stream
и Collection
и рассмотрели различные сценарии, которые им подходят.
Мы можем сделать вывод, что Stream
— отличный кандидат для рендеринга больших или бесконечных наборов результатов с такими преимуществами, как ленивая инициализация, столь необходимая гибкость и функциональное поведение.
Однако, когда нам требуется согласованная форма результатов или когда речь идет о низкой материализации, мы должны выбрать Collection
вместо Stream
.
Как обычно, исходный код доступен на GitHub .