1. Помилки з лямбда‑виразами: захоплення змінних
У Java лямбда‑вирази можуть використовувати змінні із зовнішнього контексту. Але є обмеження: такі змінні мають бути final або «ефективно фінальними» (effectively final), тобто не змінюватися після ініціалізації.
Приклад помилки
int sum = 0;
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.forEach(n -> sum += n); // Помилка компіляції!
Чому?
Компілятор скаржиться: змінна використовується у лямбді, отже, вона має бути final або effectively final, а sum змінюється всередині лямбди.
Як уникнути?
- Використовуйте термінальні операції стрімів, які не потребують зовнішніх змінних: mapToInt + sum().
- У крайньому разі — контейнери на кшталт AtomicInteger або масив із одного елемента (але це радше хак).
int sum = list.stream().mapToInt(Integer::intValue).sum();
Аналогія
Уявіть, що лямбда — це «мандрівник у часі»: вона «запам’ятовує» значення змінної на момент створення і не може спостерігати, як воно змінюється. Спроба змінити — це «парадокс дідуся»: компілятор не дасть зібрати програму.
2. Помилки з областю видимості та this
У лямбда‑виразі ключове слово this посилається на зовнішній об’єкт, а не на анонімний клас (як у випадку з анонімними класами).
Приклад
public class Example {
int value = 42;
void foo() {
Runnable r = () -> {
System.out.println(this.value); // this — це Example, а не Runnable!
};
r.run();
}
}
Важливо: під час переписування коду з анонімних класів на лямбди логіка роботи this змінюється — враховуйте це, щоб не отримати неочікуваного результату.
3. Проблеми зі змінним станом (side effects)
Функціональний підхід рекомендує відсутність побічних ефектів: функції не змінюють стан поза собою і не мутують зовнішні колекції/змінні.
List<String> names = new ArrayList<>(List.of("Анна", "Борис", "Віка"));
List<String> newNames = new ArrayList<>();
names.forEach(name -> {
if (name.startsWith("А")) {
newNames.add(name); // Побічний ефект!
}
});
Код «працює», але він менш передбачуваний і небезпечніший під час використання parallelStream() (ризик гонок і винятків). Тестувати та підтримувати складніше.
Правильно: використовуйте операції, що явно формують новий результат без зміни зовнішнього стану.
List<String> newNames = names.stream()
.filter(name -> name.startsWith("А"))
.collect(Collectors.toList());
4. Помилки з типами та generics
Java — суворо типізована мова. Іноді компілятор не здатен вивести типи зі занадто складних лямбд або ланцюжків.
Приклад
List<Object> objects = List.of(1, "рядок", 3.14);
List<String> strings = objects.stream()
.filter(obj -> obj instanceof String)
.map(obj -> (String) obj)
.collect(Collectors.toList());
Виглядає логічно, але будь‑яка описка або помилкове перетворення типів може призвести до помилки компіляції або, гірше, до ClassCastException під час виконання.
Як уникнути?
- Додавайте явні типи, якщо виведення типів дає збій.
- Не бійтеся писати <String> або параметризувати лямбди: (String s) -> ....
- Перевіряйте сумісність типів під час перетворень.
Типовий випадок з Optional
Optional<String> opt = Optional.of("hello");
opt.map(s -> s.length()); // результат — Optional<Integer>
Якщо ви очікували Optional<String>, а отримали Optional<Integer>, перевірте, що повертає ваша функція.
5. Побічні ефекти в лямбдах і паралелізм
Паралельні стріми (parallelStream()) плюс побічні ефекти — небезпечна комбінація.
Приклад
List<Integer> numbers = IntStream.range(0, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();
numbers.parallelStream().forEach(n -> results.add(n)); // НЕБЕЗПЕЧНО!
Що може статися?
- Втрата або дублювання даних.
- ConcurrentModificationException або «містичні» баги.
Як правильно?
- Використовуйте потокобезпечні колекції: ConcurrentLinkedQueue, CopyOnWriteArrayList.
- Ще краще — взагалі уникати побічних ефектів і збирати результат через collect(...).
List<Integer> results = numbers.parallelStream()
.map(n -> n)
.collect(Collectors.toList());
6. Втрата читабельності: «stream‑спагеті» та довгі ланцюжки
Функціональний стиль хороший доти, доки ланцюжок не перетворюється на «чек з гіпермаркету».
List<String> result = list.stream()
.filter(s -> s.length() > 2)
.map(String::trim)
.map(s -> s.toUpperCase())
.filter(s -> s.contains("JAVA"))
.sorted()
.distinct()
.collect(Collectors.toList());
Поради:
- Розбивайте ланцюжки на логічні блоки.
- Виносьте складні лямбди в окремі методи із зрозумілими іменами.
- Додавайте коментарі за потреби — навіть у Stream‑коді.
7. Невдалі імена змінних і функцій
Надто короткі імена (x, y, z) ускладнюють розуміння.
list.stream()
.map(x -> x.trim())
.filter(y -> y.length() > 3)
.map(z -> z.toUpperCase())
.forEach(System.out::println);
Використовуйте змістовні імена, особливо якщо лямбда багаторядкова або виражає нетривіальну логіку.
8. Помилки з null і Optional
Stream API та функціональні інтерфейси не люблять null. Передача null у лямбду або стрім — часта причина NullPointerException.
List<String> list = Arrays.asList("a", null, "b");
list.stream()
.map(String::toUpperCase) // Бум! NPE на другому елементі
.forEach(System.out::println);
Як правильно?
- Фільтруйте null заздалегідь: .filter(Objects::nonNull).
- Використовуйте Optional для явного подання відсутності значення.
9. Проблеми з типом, що повертається, у комбінованих функціях
Під час використання compose та andThen легко переплутати порядок застосування функцій і очікувані типи.
Function<String, Integer> parse = Integer::parseInt;
Function<Integer, Integer> square = x -> x * x;
Function<String, Integer> parseAndSquare = parse.andThen(square);
// Працює: спочатку parse, потім square
Function<String, Integer> squareThenParse = parse.compose(square);
// Помилка! square приймає Integer, а parse очікує String
Висновок: завжди перевіряйте порядок застосування та відповідність типів.
10. Проблеми з checked‑винятками в лямбдах
Функціональні інтерфейси з пакета java.util.function не допускають викидання checked‑винятків (наприклад, IOException). Якщо всередині лямбди потрібен код, який їх викидає, обробіть виняток вручну.
Function<String, String> readFile = path -> {
try {
return Files.readString(Path.of(path));
} catch (IOException e) {
throw new RuntimeException(e); // Або обробити інакше
}
};
Інакше компілятор не дозволить використовувати таку функцію у стрімі або колекції.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ