Що робить операція map()
Вона приймає функціональний інтерфейс java.util.function.Function
🟢 Junior Level
Коротка відповідь для співбесіди (30 секунд)
map(Function<? super T, ? extends R> mapper) — це проміжна (intermediate) лінива операція Stream API, яка перетворює кожен елемент потоку на інший елемент за принципом «один до одного» (1-to-1 mapping).
Вона приймає функціональний інтерфейс java.util.function.Function<T, R> (метод R apply(T t)), застосовує передану функцію до кожного вхідного об’єкта і передає перетворений результат далі по конвеєру. У результаті утворюється новий стрім Stream<R>. Найважливіша властивість: операція map ніколи не змінює загальну кількість елементів у потоці.
Базові приклади на Java 21
import java.util.List;
public class MapBasicsDemo {
record User(Long id, String email, String role) {}
record UserDto(Long id, String email) {}
public static void main(String[] args) {
List<User> users = List.of(
new User(1L, "alice@example.com", "ADMIN"),
new User(2L, "bob@example.com", "USER")
);
// 1. Трансформація сутностей у DTO
List<UserDto> dtos = users.stream()
.map(u -> new UserDto(u.id(), u.email()))
.toList();
System.out.println("DTOs: " + dtos);
// 2. Вилучення однієї властивості з перетворенням типу (String -> Integer)
List<String> words = List.of("Java", "Stream", "API");
List<Integer> lengths = words.stream()
.map(String::length)
.toList();
System.out.println("Lengths: " + lengths); // [4, 6, 3]
}
}
🟡 Middle Level
Внутрішня будова: Sink.ChainedReference
Усередині бібліотеки java.util.stream операція map конструює вузол конвеєра, метод accept якого трансформує вхідне значення на льоту і відразу передає його наступній ланці:
// Спрощена реалізація всередині OpenJDK (ReferencePipeline.java):
@Override
public Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> downstream) {
return new Sink.ChainedReference<P_OUT, P_OUT>(downstream) {
@Override
public void accept(P_OUT u) {
// Застосовуємо функцію mapper і негайно проштовхуємо результат далі
downstream.accept(mapper.apply(u));
}
};
}
Завдяки лінивості конвеєра трансформація відбувається по одному елементу за раз, без створення проміжних списків.
Спеціалізація під примітиви: Уникнення Autoboxing
Використання стандартного map() для числових перетворень призводить до створення мільйонів об’єктів-обгорток у купі Java:
// ❌ АНТИПАТЕРН: Створення 1 000 000 об'єктів Integer (24 байти кожен + посилання = ~32 МБ)
Stream<Integer> boxedStream = words.stream().map(String::length);
// ✅ ЕФЕКТИВНО: Перехід у примітивний IntStream (0 алокацій об'єктів обгорток)
IntStream primitiveStream = words.stream().mapToInt(String::length);
int totalCharacters = primitiveStream.sum();
Методи спеціалізації:
mapToInt(ToIntFunction)-> повертаєIntStreammapToLong(ToLongFunction)-> повертаєLongStreammapToDouble(ToDoubleFunction)-> повертаєDoubleStream- Для зворотного переходу з примітивного потоку в посилальний використовується метод
boxed()абоmapToObj(IntFunction).
Порівняння: map() vs flatMap()
| Критерій | map() |
flatMap() |
|---|---|---|
| Сигнатура функції | Function<T, R> |
Function<T, Stream<R>> |
| Співвідношення елементів | Суворо 1 до 1 (на 1 вхідний елемент рівно 1 вихідний) | 1 до багатьох (0..N) (на 1 вхідний елемент повертається цілий стрім) |
| Результат структури | Зберігає вкладеність (наприклад, List<List<T>> -> Stream<List<T>>) |
«Сплющує» (flatten) вкладені структури в плаский потік Stream<T> |
Вимога чистоти функцій (Purity & Side-Effect-Free)
Функція, що передається в map, має бути чистою:
- Не мутувати переданий об’єкт (якщо потрібно змінити поле, створюйте новий об’єкт або DTO).
- Не звертатися до змінного зовнішнього стану.
- Не викликати побічних ефектів (I/O, запис у БД). Для виконання побічних дій призначений термінальний метод
forEach().
🔴 Senior Level
Вплив на прапорці характеристик конвеєра (Stream Flags)
Операція map() має специфічний вплив на метадані конвеєра:
- Прапорець
SIZED— ЗБЕРІГАЄТЬСЯ: Конвеєр гарантовано знає точну кількість елементів на виході, оскількиmapне може ні додати, ні видалити елементи. Термінальні методи на зразокtoList()можуть заздалегідь виділити масив точного розміру. - Прапорці
DISTINCTтаSORTED— ЗАВЖДИ СКИДАЮТЬСЯ (NOT_DISTINCT,NOT_SORTED): Компілятор і середовище виконання не можуть гарантувати, що користувацька функція збереже унікальність і порядок:List<String> distinctWords = List.of("cat", "dog", "bat"); // Усі елементи унікальні distinctWords.stream() .distinct() // Прапорець DISTINCT активний .map(String::length) // Прапорець DISTINCT СКИДАЄТЬСЯ: результат [3, 3, 3] містить дублікати! .toList();
Обробка Checked Exceptions у лямбдах map
Інтерфейс java.util.function.Function не декларує перевірюваних винятків у методі R apply(T t). Якщо метод бізнес-логіки викидає checked-виняток (наприклад, IOException або SQLException), код не скомпілюється.
Патерни вирішення:
// 1. Загортання в Unchecked exception локально
stream.map(path -> {
try {
return Files.readString(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
// 2. Ідіоматичний функціональний адаптер (Try / Either / Result pattern)
public static <T, R> Function<T, R> unchecked(ThrowingFunction<T, R> f) {
return t -> {
try {
return f.apply(t);
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
}
Небезпека Null-значень downstream
Якщо функція мапера повертає null, map не відфільтрує його — null піде далі конвеєром як повноцінний елемент:
// ❌ ПРИХОВАНИЙ БАГ: NullPointerException на наступному етапі
list.stream()
.map(User::getOptionalMiddleName) // може повернути null
.map(String::toUpperCase) // 💥 NullPointerException!
.toList();
// ✅ БЕЗПЕЧНИЙ ПІДХІД:
list.stream()
.map(User::getOptionalMiddleName)
.filter(Objects::nonNull)
.map(String::toUpperCase)
.toList();
🎯 Шпаргалка для інтерв’ю
Питання для швидкої перевірки
- Яке співвідношення елементів гарантує операція
map()? Відповідь: Суворо «один до одного» (1-to-1). На кожен елемент вхідного потоку на виході формується рівно один елемент. Загальна кількість елементів потоку не змінюється. - Як
map()впливає на прапорці характеристик стріма (SIZED,SORTED,DISTINCT)? Відповідь: ПрапорецьSIZEDзавжди зберігається (розмір потоку відомий). ПрапорціSORTEDіDISTINCTзавжди скидаються, оскільки довільна функція трансформації може порушити вихідний порядок сортування або породити однакові значення. - Чому для числових полів переважніше використовувати
mapToInt()замістьmap()? Відповідь:map()із посилальною функцією загортає кожне число в об’єктInteger(Autoboxing), створюючи навантаження на пам’ять і збирач сміття (24 байти на об’єкт).mapToInt()повертає примітивнийIntStream, що зберігає числа безпосередньо без алокації об’єктів у купі. - Що станеться, якщо функція в
map()повернеnull? Відповідь: Стрім продовжить роботу, передавшиnullнаступній стадії конвеєра. Якщо подальша операція спробує викликати метод у цього елемента, виникнеNullPointerException.
Типові помилки та Red Flags
- ❌ Спроба використовувати
map()для фільтрації з поверненнямnull: Для фільтрації потрібно використовуватиfilter(), інакше стрім наповнитьсяnull-значеннями. - ❌ Мутація полів вихідного об’єкта всередині
map(): Порушує принцип чистоти функцій і призводить до race conditions у паралельних стрімах. - ❌ Виконання I/O-операцій (запити до бази, виклик REST) усередині
map(): Операціяmapмає бути легкою обчислювальною функцією в пам’яті. - ❌ Плутанина між
map()таflatMap(): Нерозуміння різниці між 1-to-1 проекцією та сплющуванням вкладених колекцій (1-to-N).