CodeGym /Courses /JAVA 25 SELF /Debugging errors in functional programming

Debugging errors in functional programming

JAVA 25 SELF
Level 49 , Lesson 4
Available

1. Errors with lambda expressions: variable capture

In Java, lambda expressions can use variables from an outer context. But there’s a constraint: such variables must be final or “effectively final” (effectively final), meaning they are not modified after initialization.

Example of an error

int sum = 0;
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.forEach(n -> sum += n); // Compilation error!

Why?
The compiler complains: the variable is used in a lambda, which means it must be final or effectively final, and sum is modified inside the lambda.

How to avoid it?

  • Use terminal stream operations that don’t require external variables: mapToInt + sum().
  • In extreme cases—containers like AtomicInteger or a single-element array (but that’s more of a hack).
int sum = list.stream().mapToInt(Integer::intValue).sum();

Analogy
Imagine a lambda as a “time traveler”: it “remembers” the value of a variable at the moment it’s created and cannot observe it changing. Attempting to change it is a “grandfather paradox”; the compiler won’t let the program compile.

2. Errors with scope and this

In a lambda expression, the keyword this refers to the enclosing object, not to an anonymous class (as it would with anonymous classes).

Example

public class Example {
    int value = 42;

    void foo() {
        Runnable r = () -> {
            System.out.println(this.value); // this refers to Example, not Runnable!
        };
        r.run();
    }
}

Important: when rewriting code from anonymous classes to lambdas, the semantics of this changes—keep that in mind to avoid unexpected results.

3. Problems with mutable state (side effects)

The functional approach recommends the absence of side effects: functions do not change state outside themselves and do not mutate external collections/variables.

List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));
List<String> newNames = new ArrayList<>();

names.forEach(name -> {
    if (name.startsWith("A")) {
        newNames.add(name); // Side effect!
    }
});

The code “works”, but it’s less predictable and dangerous when using parallelStream() (risk of races and exceptions). It’s harder to test and maintain.

Correct: use operations that explicitly form a new result without changing external state.

List<String> newNames = names.stream()
    .filter(name -> name.startsWith("A"))
    .collect(Collectors.toList());

4. Errors with types and generics

Java is a statically typed language. Sometimes the compiler can’t infer types from overly complex lambdas or chains.

Example

List<Object> objects = List.of(1, "string", 3.14);
List<String> strings = objects.stream()
    .filter(obj -> obj instanceof String)
    .map(obj -> (String) obj)
    .collect(Collectors.toList());

It looks reasonable, but any typo or incorrect cast can lead to a compilation error or, worse, a ClassCastException at runtime.

How to avoid?

  • Add explicit types if type inference “stumbles”.
  • Don’t be afraid to write <String> or parameterize lambdas: (String s) -> ....
  • Check type compatibility during conversions.

A typical case with Optional

Optional<String> opt = Optional.of("hello");
opt.map(s -> s.length()); // result — Optional<Integer>

If you expected Optional<String> but got Optional<Integer>, check what your function returns.

5. Side effects in lambdas and parallelism

Parallel streams (parallelStream()) plus side effects are a dangerous combination.

Example

List<Integer> numbers = IntStream.range(0, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();

numbers.parallelStream().forEach(n -> results.add(n)); // DANGEROUS!

What can happen?

  • Data loss or duplication.
  • ConcurrentModificationException or “mysterious” bugs.

How to do it right?

  • Use thread-safe collections: ConcurrentLinkedQueue, CopyOnWriteArrayList.
  • Even better—avoid side effects altogether and collect the result via collect(...).
List<Integer> results = numbers.parallelStream()
    .map(n -> n)
    .collect(Collectors.toList());

6. Loss of readability: “stream spaghetti” and long chains

Functional style is good until a chain turns into a “supermarket receipt”.

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());

Tips:

  • Split chains into logical blocks.
  • Extract complex lambdas into separate methods with clear names.
  • Add comments if needed—even in Stream code.

7. Poor variable and function names

Overly short names (x, y, z) hinder understanding.

list.stream()
    .map(x -> x.trim())
    .filter(y -> y.length() > 3)
    .map(z -> z.toUpperCase())
    .forEach(System.out::println);

Use meaningful names, especially if a lambda is multi-line or expresses non-trivial logic.

8. Errors with null and Optional

The Stream API and functional interfaces don’t like nulls. Passing null into a lambda or stream is a common cause of NullPointerException.

List<String> list = Arrays.asList("a", null, "b");
list.stream()
    .map(String::toUpperCase) // Boom! NPE on the second element
    .forEach(System.out::println);

How to do it right?

  • Filter nulls upfront: .filter(Objects::nonNull).
  • Use Optional to explicitly represent absence of a value.

9. Issues with the return type in composed functions

When using compose and andThen, it’s easy to mix up the order of function application and the expected types.

Function<String, Integer> parse = Integer::parseInt;
Function<Integer, Integer> square = x -> x * x;

Function<String, Integer> parseAndSquare = parse.andThen(square);
// Works: first parse, then square

Function<String, Integer> squareThenParse = parse.compose(square);
// Error! square takes Integer, but parse expects String

Moral: always verify the order of application and type compatibility.

10. Problems with checked exceptions in lambdas

Functional interfaces from the java.util.function package do not allow throwing checked exceptions (for example, IOException). If you need code inside a lambda that throws them, handle the exception manually.

Function<String, String> readFile = path -> {
    try {
        return Files.readString(Path.of(path));
    } catch (IOException e) {
        throw new RuntimeException(e); // Or handle it differently
    }
};

Otherwise, the compiler won’t let you use such a function in a stream or collection.

1
Task
JAVA 25 SELF, level 49, lesson 4
Locked
Calculating the total revenue for the "Cosmic Constructors" startup 🚀
Calculating the total revenue for the "Cosmic Constructors" startup 🚀
1
Task
JAVA 25 SELF, level 49, lesson 4
Locked
Message Moderation on "Galactic Forum" 💬
Message Moderation on "Galactic Forum" 💬
1
Survey/quiz
Functional Programming, level 49, lesson 4
Unavailable
Functional Programming
Functional Programming
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION