Питання 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()]]