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 exceptions в лямбдах

Функциональные интерфейсы из пакета 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
Задача
JAVA 25 SELF, 49 уровень, 4 лекция
Недоступна
Подсчёт общей выручки стартапа "Космические Конструкторы" 🚀
Подсчёт общей выручки стартапа "Космические Конструкторы" 🚀
1
Задача
JAVA 25 SELF, 49 уровень, 4 лекция
Недоступна
Модерация сообщений на "Галактическом Форуме" 💬
Модерация сообщений на "Галактическом Форуме" 💬
1
Опрос
Функциональное программирование, 49 уровень, 4 лекция
Недоступен
Функциональное программирование
Функциональное программирование
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ