1. Преобразование List → Set и обратно
Давайте вспомним, зачем вообще нужны преобразования коллекций. Часто в реальных задачах нам нужно:
- Получить из списка уникальные элементы (например, список e-mail → множество уникальных адресов).
- Построить отображение (Map), например, из списка имён получить карту «имя → длина имени».
- Объединить элементы в одну строку (например, для красивого вывода).
Раньше для этого приходилось писать много кода с циклами, условиями и временными коллекциями. Со Stream API всё стало проще и… стильнее!
Пример: получить множество уникальных имён из списка
Допустим, у нас есть список имён (вдруг кто-то в вашей программе ввёл себя два раза — бывает!):
List<String> names = List.of("Анна", "Сергей", "Анна", "Мария", "Иван", "Сергей");
Наша задача — получить коллекцию, где каждое имя встречается только один раз, то есть множество (Set). С помощью Stream API это делается буквально в одну строчку:
Set<String> uniqueNames = names.stream()
.collect(Collectors.toSet());
System.out.println(uniqueNames);
Вывод:
[Мария, Иван, Анна, Сергей]
(Порядок в Set не гарантируется — не удивляйтесь, если у вас будет другой порядок.)
А если нужно обратно: Set → List?
Иногда нужно наоборот — превратить множество в список (например, чтобы отсортировать или получить доступ по индексу):
List<String> namesList = uniqueNames.stream()
.collect(Collectors.toList());
System.out.println(namesList);
2. Преобразование в Map: Collectors.toMap()
Пример: из списка имён получить Map «имя → длина имени»
Иногда хочется быть не просто программистом, а настоящим картографом — строить карты! Давайте попробуем:
List<String> names = List.of("Анна", "Сергей", "Мария", "Иван");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name, // ключ — само имя
name -> name.length() // значение — длина имени
));
System.out.println(nameToLength);
Вывод:
{Мария=5, Иван=4, Анна=4, Сергей=6}
Важный момент: дубликаты ключей
Если в исходном списке есть одинаковые имена, то при попытке собрать их в Map возникнет ошибка IllegalStateException: Duplicate key. Java не любит, когда вы пытаетесь положить два значения по одному и тому же ключу.
Как обработать дубликаты?
Можно указать, что делать при совпадении ключей — например, оставить первое значение или последнее:
List<String> names = List.of("Анна", "Сергей", "Анна", "Мария", "Иван", "Сергей");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name,
name -> name.length(),
(oldValue, newValue) -> oldValue // оставить первое значение
));
System.out.println(nameToLength);
Теперь программа не упадёт, и в Map попадёт только первое вхождение каждого имени.
Пример: Map с объектами
Давайте немного усложним: у нас есть список пользователей, и мы хотим построить Map «имя → пользователь»:
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// Пример списка пользователей
List<User> users = List.of(
new User("Анна", 25),
new User("Сергей", 30),
new User("Мария", 22)
);
Map<String, User> nameToUser = users.stream()
.collect(Collectors.toMap(
user -> user.name,
user -> user
));
System.out.println(nameToUser);
Вывод:
{Мария=Мария (22), Анна=Анна (25), Сергей=Сергей (30)}
3. Сборка в строку: Collectors.joining()
Иногда хочется не просто собрать коллекцию, а сделать красивую строку для вывода пользователю или в лог. Например, собрать все имена через запятую:
List<String> names = List.of("Анна", "Сергей", "Мария", "Иван");
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result);
Вывод:
Анна, Сергей, Мария, Иван
Можно добавить префикс и суффикс
String result = names.stream()
.collect(Collectors.joining(", ", "Список: [", "]"));
System.out.println(result);
Вывод:
Список: [Анна, Сергей, Мария, Иван]
4. Терминальные операции: forEach, collect, count, anyMatch, allMatch, noneMatch
Метод forEach
С forEach мы уже хорошо знакомы: эта операция выполняет действие для каждого элемента потока.
names.stream().forEach(name -> System.out.println("Привет, " + name + "!"));
Метод collect
Собирает элементы в коллекцию, строку или другую структуру. Самая частая операция — сборка в List или Set с помощью Collectors.toList() и Collectors.toSet().
Метод count
Подсчитывает количество элементов в потоке.
long count = names.stream()
.filter(name -> name.length() > 4)
.count();
System.out.println("Имен длиннее 4 букв: " + count);
Методы anyMatch, allMatch, noneMatch
Проверяют, выполняется ли условие хотя бы для одного элемента (anyMatch), для всех (allMatch) или ни для одного (noneMatch).
boolean hasShortName = names.stream()
.anyMatch(name -> name.length() < 4);
System.out.println("Есть короткое имя? " + hasShortName);
boolean allLong = names.stream()
.allMatch(name -> name.length() > 3);
System.out.println("Все имена длиннее 3 букв? " + allLong);
boolean noneIvan = names.stream()
.noneMatch(name -> name.equals("Иван"));
System.out.println("Нет ли Ивана? " + noneIvan);
Вывод:
Есть короткое имя? false
Все имена длиннее 3 букв? true
Нет ли Ивана? false
5. Терминальные и промежуточные операции: закрепим понятия
Промежуточные операции (filter, map, distinct, sorted, limit, skip, peek) — возвращают новый Stream, можно строить цепочки.
Терминальные операции (forEach, collect, count, anyMatch, allMatch, noneMatch, reduce, findFirst, findAny) — завершают поток, результата больше не будет!
Пример цепочки:
List<String> result = users.stream()
.filter(user -> user.age > 20)
.map(user -> user.name.toUpperCase())
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(result);
Вывод:
[АННА, ИВАН, МАРИЯ, СЕРГЕЙ]
6. Типичные ошибки при преобразовании коллекций через Stream
Ошибка №1: Необработка дубликатов ключей в toMap
Если в исходной коллекции встречаются дублирующиеся ключи, а вы используете Collectors.toMap() без явного обработчика, программа выбросит исключение. Для таких случаев всегда указывайте функцию слияния:
// Оставить последнее значение
.toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
Ошибка №2: Использование forEach вместо collect
Иногда новички пытаются «собрать» коллекцию с помощью forEach, например:
List<String> list = new ArrayList<>();
names.stream().forEach(name -> list.add(name)); // Работает, но это не Stream-way!
Лучше использовать collect(Collectors.toList()) — это безопаснее и чище.
Ошибка №3: Попытка повторно использовать поток
Поток можно использовать только один раз. После терминальной операции (например, collect, forEach) попытка продолжить работать с этим же Stream приведёт к IllegalStateException.
Ошибка №4: Нарушение принципа «без побочных эффектов»
Промежуточные операции должны быть «чистыми» (без изменения внешних переменных). Не стоит внутри map или filter что-то менять вне потока.
Ошибка №5: Неучёт порядка в Set и Map
Если важен порядок элементов, используйте соответствующие коллекции — например, LinkedHashSet, TreeMap — и указывайте нужный коллектор.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ