Вопрос 4 · Раздел 8

Что делает операция map()?

Принимает Function — функциональный интерфейс с методом R apply(T t).

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

map(Function) — это промежуточная операция, которая преобразует каждый элемент стрима в другой элемент (“один к одному”).

Принимает Function<T, R> — функциональный интерфейс с методом R apply(T t).

List<String> words = List.of("hello", "world");

// Преобразуем слова в их длину
List<Integer> lengths = words.stream()
    .map(String::length)
    .collect(Collectors.toList());
// Результат: [5, 5]

// Преобразование в верхний регистр
List<String> upper = words.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());
// Результат: ["HELLO", "WORLD"]

Важно: map всегда возвращает один объект для каждого входного элемента.

🟡 Middle Level

Внутренняя реализация

map встраивается в цепочку Sink:

public void accept(T t) {
    downstream.accept(mapper.apply(t));
}

Благодаря ленивости, если результат map не используется terminal операцией, функция преобразования никогда не будет вызвана.

Избегайте Wrapper-ада (Автобоксинг)

// ПЛОХО — создает миллионы объектов Integer в куче
list.stream().map(s -> s.length()).reduce(0, Integer::sum);

// ХОРОШО — работает с примитивами в стеке
list.stream().mapToInt(String::length).sum();

Primitive Streams (IntStream, LongStream, DoubleStream) радикально снижают нагрузку на GC и потребление памяти.

IntStream/LongStream/DoubleStream хранят примитивы напрямую, без обёрток (Integer, Long). Для 1 млн элементов: Stream = 1 млн объектов в куче (~24MB), IntStream = один int[] массив (~4MB). Разница в 6x по памяти.

Map vs FlatMap

  • map: возвращает один объект для каждого входного
  • flatMap: возвращает стрим объектов (от 0 до бесконечности), которые “сплющиваются” в один поток

Чистота функций (Purity)

Функция в map рекомендуется чистой (без побочных эффектов). На практике impure-функции работают, но с рисками: в parallelStream результат будет непредсказуемым.

  • Не менять входной объект (иммутабельность)
  • Не иметь побочных эффектов (не писать в БД, не менять статические поля)
  • Возвращать один и тот же результат для тех же входных данных

Когда НЕ использовать map

  1. Вам не нужен результат преобразования — используйте forEach (для действий) или peek (для отладки)
  2. Фильтрация + трансформация в одну — иногда лучше один цикл с if + transform
  3. Работа с побочными эффектами — map не для I/O, используйте forEach

🔴 Senior Level

Method References и JIT оптимизация

Используйте String::toUpperCase вместо s -> s.toUpperCase(). Это не только чище, но и помогает JIT-компилятору лучше оптимизировать вызовы через invokedynamic.

Null Handling

Если функция маппинга возвращает null, стрим пойдет дальше с null-элементом. Это часто приводит к NPE в следующих звеньях:

// Безопасный подход
stream.map(User::getEmail)
      .filter(Objects::nonNull)
      .map(String::toLowerCase)

Checked Exceptions

Лямбды в map не могут бросить checked исключение. Приходится оборачивать в RuntimeException или использовать вспомогательные интерфейсы:

// Обёртка checked исключения
stream.map(s -> {
    try {
        return URLEncoder.encode(s, "UTF-8");
    } catch (UnsupportedEncodingException e) {
        throw new RuntimeException(e);
    }
})

Object Allocation и Highload

Если функция в map создает тяжелые объекты, рассмотрите возможность переиспользования объектов (Object Pooling), хотя в стримах это сложно реализовать.

Диагностика

  • Type Changes: Следите за тем, как меняется тип стрима в цепочке — IntelliJ IDEA подсвечивает типы
  • Side Effect Detection: Если map содержит System.out.println или log.info — это признак плохого дизайна. Для логирования используйте peek().

🎯 Шпаргалка для интервью

Обязательно знать:

  • map(Function) — промежуточная операция, преобразование «один к одному»
  • Принимает Function<T, R>, возвращает Stream<R>
  • Primitive Streams (mapToInt, mapToLong) избегают автобоксинга и экономят память
  • Функция маппинга должна быть чистой: без побочных эффектов, детерминированная
  • Method References (String::toUpperCase) предпочтительнее лямбд — чище и помогает JIT
  • Если mapper возвращает null — стрим пойдёт дальше с null-элементом (риск NPE)

Частые уточняющие вопросы:

  • map vs flatMap? — map возвращает один объект, flatMap — стрим объектов, который «сплющивается»
  • Почему mapToInt лучше map? — IntStream хранит примитивы в массиве, Stream создаёт объекты в куче
  • Как обрабатывать checked exceptions в лямбде? — Обернуть в RuntimeException или использовать вспомогательные интерфейсы
  • Можно ли использовать map для I/O? — Нет, map не для побочных эффектов, используйте forEach

Красные флаги (НЕ говорить):

  • «map может вернуть несколько объектов для одного элемента» — это flatMap
  • «Автобоксинг не влияет на производительность» — для миллионов элементов разница в 6x по памяти
  • «Функция в map может менять внешние переменные» — это нарушает чистоту и ломает parallelStream
  • «map и forEach взаимозаменяемы» — map трансформирует, forEach выполняет действие

Связанные темы:

  • [[7. Что делает операция flatMap()]]
  • [[8. В чём разница между map() и flatMap()]]
  • [[3. Что делает операция filter()]]
  • [[5. Что делает операция collect()]]