JavaRush /Курси /JAVA 25 SELF /Розбір помилок у функціональному програмуванні

Розбір помилок у функціональному програмуванні

JAVA 25 SELF
Рівень 49 , Лекція 4
Відкрита

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); // Або обробити інакше
    }
};

Інакше компілятор не дозволить використовувати таку функцію у стрімі або колекції.

1
Опитування
Функціональне програмування, рівень 49, лекція 4
Недоступний
Функціональне програмування
Функціональне програмування
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ