Stream.map: примеры (JAVA)
Stream.map(Function super T, ? extends R> mapper): StreamОписание метода Stream.map
Метод map в пакете java.util.stream применяется для преобразования элементов потока одного типа в элементы другого типа с помощью функции отображения. Оригинальный сигнатурный вид в интерфейсе Stream:
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Параметры и возвращаемые значения:
- mapper: функция типа
Function<? super T, ? extends R>. Принимает элемент потока типаTи возвращает значение типаR. Функция не должна изменять внешнее состояние (предпочтительно), может выбросить исключение. - Возвращает
Stream<R>: ленивый последовательный/параллельный поток, в котором каждый элемент - результат примененияmapperк соответствующему элементу входного потока.
Примеры специализированных версий для примитивных потоков:
IntStream.map(IntUnaryOperator mapper)LongStream.map(LongUnaryOperator mapper)DoubleStream.map(DoubleUnaryOperator mapper)- Для преобразования между видами предусмотрены
mapToInt,mapToLong,mapToDouble,mapToObj.
Особенности поведения:
- Операция ленивa: mapper применяется только при терминальной операции.
- Не является короткозамыкающей: обработка всех элементов не прекращается раньше терминальной операции (если сама терминальная операция не короткозамыкающая).
- Поддерживает параллельное выполнение, mapper должен быть безопасен для параллельного вызова.
- Поток не должен содержать
null- многие реализации и операции могут привести кNullPointerException, если mapper вернулnullили входные элементы равныnull.
Ниже приведены практические примеры в секции «examples».
Короткие примеры использования
Примеры демонстрируют код и результат.
1. Простое преобразование строк в верхний регистр
List<String> list = List.of("one", "two", "three");
List<String> upper = list.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upper);
[ONE, TWO, THREE]
2. Преобразование в длины (IntStream через mapToInt)
List<String> words = List.of("a", "bb", "ccc");
int sum = words.stream()
.mapToInt(String::length)
.sum();
System.out.println(sum);
6
3. map с методом, возвращающим объект другого типа
record Person(String name, int age) {}
List<Person> people = List.of(new Person("Anna", 30), new Person("Boris", 25));
List<String> names = people.stream()
.map(Person::name)
.collect(Collectors.toList());
System.out.println(names);
[Anna, Boris]
4. map на примитивный поток и обратно
IntStream.range(1, 4)
.map(i -> i * 2) // IntStream
.mapToObj(Integer::toString) // Stream<String>
.forEach(System.out::println);
2 4 6
5. Использование метода, возвращающего null (показан NPE)
List<String> data = List.of("x", "y");
List<String> res = data.stream()
.map(s -> null)
.collect(Collectors.toList());
System.out.println(res);
[null, null] // но дальнейшие операции с null могут привести к NullPointerException
Похожие методы в Java и их особенности
- flatMap
- mapMulti
- mapToInt / mapToLong / mapToDouble
- peek
Возвращает для каждого входного элемента поток элементов и затем "разворачивает" их в единый поток. Предпочтительнее, когда требуется получить ноль, один или несколько элементов из одного входного.
Добавляет возможность более эффективного вывода нескольких элементов из одного входного (доступно в новых версиях Java). Удобнее и производительнее, чем реализация flatMap с коллекциями в ряде случаев.
Специализированные версии для примитивов: уменьшают автоупаковку, повышают производительность и доступ к статистическим методам примитивных стримов.
Не трансформирует тип; используется для отладки и побочных действий; не подходит для преобразований.
Выбор между ними зависит от требуемого результата: если нужен один объект на вход - map; несколько объектов - flatMap или mapMulti; если нужен примитивный результат - mapToInt и аналоги.
Аналоги в других языках и особенности
- JavaScript
const arr = [1,2,3];
const doubled = arr.map(x => x*2);
console.log(doubled);
[2,4,6]
Array.prototype.map выполняет синхронно, возвращая новый массив; нет ленивости, в отличие от Stream.
nums = [1,2,3]
doubled = list(map(lambda x: x*2, nums))
print(doubled)
[2, 4, 6]
map возвращает итератор в Python 3 (ленивость), однако обычно используется list comprehension для читаемости: [x*2 for x in nums].
$arr = [1,2,3];
$res = array_map(fn($x) => $x*2, $arr);
print_r($res);
Array ( [0] => 2 [1] => 4 [2] => 6 )
array_map сразу возвращает массив, нет встроенной ленивости.
SELECT UPPER(name) FROM users;
-- Результат: столбец с преобразованными значениями
Проекция в SQL эквивалентна map на множестве строк.
var doubled = new[]{1,2,3}.Select(x => x*2);
Console.WriteLine(string.Join(",", doubled));
2,4,6
LINQ Select ленивый как Stream и часто более выразителен в цепочках.
val doubled = listOf(1,2,3).map { it * 2 }
println(doubled)
[2, 4, 6]
Коллекции Kotlin имеют eager map; Sequence.map - ленивый аналог.
// В Go часто используется явный цикл
in := []int{1,2,3}
out := make([]int, len(in))
for i,v := range in { out[i] = v*2 }
fmt.Println(out)
[2 4 6]
Отсутствует встроенная функциональная map в стандартной библиотеке; предпочитаются циклы.
local t={1,2,3}
local out={}
for i,v in ipairs(t) do out[i]=v*2 end
print(table.concat(out,","))
2,4,6
Реализация вручную через циклы.
Ключевые отличия от Java: наличие или отсутствие ленивости, специализация для примитивов, синтаксис и поточная семантика.
Типичные ошибки при использовании map
- Возвращение null из mapper
List<String> l = List.of("a");
l.stream().map(s -> null).forEach(System.out::println);
null
Хотя выводит null, дальнейшие операции, ожидающие неподложенные значения, могут привести к NPE.
List<Integer> src = List.of(1,2,3);
List<Integer> out = new ArrayList<>();
src.stream().map(i -> { out.add(i); return i; }).count();
System.out.println(out);
[1,2,3] // но поведение при параллельном потоке не безопасно
При параллельном выполнении могут возникнуть состояния гонки. Лучше избегать побочных эффектов.
List<String> s = List.of("a","","b");
List<String> r = s.stream().map(str -> str.isEmpty() ? null : str)
.collect(Collectors.toList());
["a", null, "b"]
Лучше использовать filter для исключения элементов вместо создания null-значений.
Stream<Integer> st = Stream.of(1,2,3).map(i -> i*2);
st.forEach(System.out::println);
st.forEach(System.out::println);
1 раз пройдет, второе вызовет IllegalStateException: stream has already been operated upon or closed
Object o = "text";
Stream.of(o).map(x -> (Integer)x).forEach(System.out::println);
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Изменения и эволюция метода
Метод map является частью Streams API с Java 8 и сохраняет стабильный контракт. Ключевые моменты в развитии экосистемы:
- Специализированные примитивные потоки (
IntStream,LongStream,DoubleStream) появились одновременно с API и поддерживают свои версииmap. - В новых релизах Java добавлены утилиты для потоков, например
Stream.toList()(Java 16) иmapMulti(в одном из последних релизов Java 16+), предоставляющие улучшенные и более эффективные сценарии вывода нескольких элементов из одного входного. - Оптимизации реализации и исправления производительности происходили в последующих версиях JDK, но семантика
mapосталась прежней.
Расширенные и редко встречающиеся сценарии
1. map с индексом (через IntStream.range)
List<String> names = List.of("a","b","c");
List<String> numbered = IntStream.range(0, names.size())
.mapToObj(i -> i + ": " + names.get(i))
.collect(Collectors.toList());
System.out.println(numbered);
[0: a, 1: b, 2: c]
2. mapMulti - эффективная генерация нескольких элементов
List<String> data = List.of("ab", "c");
List<String> chars = data.stream()
.mapMulti((s, consumer) -> { for (char ch : s.toCharArray()) consumer.accept(String.valueOf(ch)); })
.collect(Collectors.toList());
System.out.println(chars);
[a, b, c]
3. Преобразование и агрегация в Map с конфликтом ключей
List<String> items = List.of("a","A","b","B");
Map<String, String> map = items.stream()
.map(String::toLowerCase)
.collect(Collectors.toMap(s -> s, s -> s, (a,b) -> a));
System.out.println(map);
{a=a, b=b}
4. Обработка исключений в mapper
List<String> in = List.of("1","x","3");
List<Integer> out = in.stream()
.map(s -> {
try { return Integer.parseInt(s); }
catch(NumberFormatException e) { return -1; }
})
.collect(Collectors.toList());
System.out.println(out);
[1, -1, 3]
5. Ленивые бесконечные потоки и map
Stream.iterate(0, n -> n+1)
.map(n -> n*n)
.filter(n -> n < 10)
.limit(5)
.forEach(System.out::println);
0 1 4 9 16
6. Параллельный map с безопасными операциями
List<Integer> in = IntStream.range(0,1000).boxed().collect(Collectors.toList());
List<Integer> out = in.parallelStream()
.map(i -> i * 2)
.collect(Collectors.toList());
System.out.println(out.size());
1000
Для корректной работы в параллели mapper не должен иметь побочных эффектов и зависеть от изменяемого внешнего состояния.
7. Преобразование в CompletableFuture и последовательная сборка
List<String> urls = List.of("u1","u2");
List<CompletableFuture<String>> futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> "content:" + url))
.collect(Collectors.toList());
List<String> contents = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
System.out.println(contents);
[content:u1, content:u2]
8. map с изменением структуры, затем groupingBy
List<String> names = List.of("Ann","Bob","Amy");
Map<Character, List<String>> byFirst = names.stream()
.map(String::toUpperCase)
.collect(Collectors.groupingBy(s -> s.charAt(0)));
System.out.println(byFirst);
{A=[ANN, AMY], B=[BOB]}
9. Отличие map и flatMap на реальном примере
List<List<Integer>> data = List.of(List.of(1,2), List.of(3));
var wrong = data.stream().map(x -> x).collect(Collectors.toList());
var flat = data.stream().flatMap(Collection::stream).collect(Collectors.toList());
System.out.println(wrong);
System.out.println(flat);
[[1, 2], [3]] [1, 2, 3]
Варианты показывают, как сочетать map с другими операциями для получения эффективных и безопасных конвейеров обработки.