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); // Или обработать иначе
}
};
Иначе компилятор не даст использовать такую функцию в стриме или коллекции.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ