Index

Exception Handling in Streams

Java Streams

14.1 Handling Checked Exceptions in Lambdas

Java Streams do not directly support lambdas that throw checked exceptions (e.g., IOException, SQLException). Since standard functional interfaces like Function, Predicate, and Consumer don't declare any checked exceptions, trying to throw one inside a lambda causes a compilation error.

This restriction often becomes problematic when integrating I/O or other checked-exception-producing logic into stream pipelines.

Why Lambdas and Checked Exceptions Clash

// Compilation error!
List<String> paths = List.of("file1.txt", "file2.txt");

paths.stream()
     .map(path -> Files.readString(Path.of(path))) // IOException not allowed
     .forEach(System.out::println);

Strategy 1: Wrap Checked Exception in a RuntimeException

A simple workaround is to wrap the checked exception in an unchecked one (RuntimeException). This shifts the handling responsibility downstream.

import java.nio.file.*;
import java.util.*;
import java.util.stream.*;

public class WrapRuntimeExample {
    public static void main(String[] args) {
        List<String> files = List.of("file1.txt", "file2.txt");

        files.stream()
             .map(path -> {
                 try {
                     return Files.readString(Path.of(path));
                 } catch (Exception e) {
                     throw new RuntimeException(e); // wrap checked exception
                 }
             })
             .forEach(System.out::println);
    }
}

💡 Pros: Simple to implement. ⚠️ Cons: Loses specific error typing and forces downstream handling or crashes at runtime.

Strategy 2: Use a Utility Method for Exception Handling

To promote reuse and cleaner code, you can create a helper function that converts a lambda that throws checked exceptions into a standard functional interface.

import java.io.*;
import java.nio.file.*;
import java.util.function.*;
import java.util.stream.*;

public class WithHandlerExample {

    // Functional interface that allows checked exceptions
    @FunctionalInterface
    interface CheckedFunction<T, R> {
        R apply(T t) throws Exception;
    }

    // Helper to wrap checked exception
    public static <T, R> Function<T, R> handleChecked(CheckedFunction<T, R> func) {
        return t -> {
            try {
                return func.apply(t);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };
    }

    public static void main(String[] args) {
        Stream<String> fileNames = Stream.of("file1.txt", "file2.txt");

        fileNames
            .map(handleChecked(path -> Files.readString(Path.of(path))))
            .forEach(System.out::println);
    }
}

💡 Pros: Reusable and clean. ⚠️ Cons: Still propagates as runtime exceptions, but improves composability.

Other Strategies

Summary

Handling checked exceptions in streams requires extra effort because lambdas can't throw them directly. You can:

These techniques allow you to maintain robust, readable pipelines even in the presence of checked exceptions.

Index

14.2 Strategies for Robust Stream Pipelines

When working with real-world data, stream pipelines must be resilient to malformed, missing, or inconsistent input. Streams offer elegant ways to validate, filter, and recover without interrupting the entire pipeline. This section outlines key strategies for writing fault-tolerant and robust stream-based code.

Filter Invalid or Null Data Early

The filter() operation can remove invalid or null elements before they cause errors downstream.

import java.util.*;
import java.util.stream.*;

public class FilterNullExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", null, "Bob", "", "Carol");

        names.stream()
             .filter(Objects::nonNull)         // filter out nulls
             .filter(s -> !s.isBlank())        // filter out empty/blank
             .map(String::toUpperCase)
             .forEach(System.out::println);
    }
}

💡 Tip: Defensive filtering early in the pipeline prevents runtime exceptions during transformations.

Use Optional to Represent Absence

Optional allows you to express missing or invalid values without nulls. Use it with flatMap() to seamlessly handle optional returns.

import java.util.List;
import java.util.Optional;

public class OptionalParseExample {

    public static Optional<Integer> parseIntSafe(String s) {
        try {
            return Optional.of(Integer.parseInt(s));
        } catch (NumberFormatException e) {
            return Optional.empty();
        }
    }

    public static void main(String[] args) {
        List<String> numbers = List.of("42", "invalid", "17", "NaN");

        numbers.stream()
               .map(OptionalParseExample::parseIntSafe)
               .flatMap(Optional::stream) // skips Optional.empty()
               .forEach(System.out::println);
    }
}

💡 Benefit: Avoids crashes by skipping invalid elements gracefully.

Validate Before Transforming

Use conditional logic to validate data before applying expensive or error-prone transformations.

import java.util.List;

public class ValidateBeforeTransform {
    public static void main(String[] args) {
        List<String> values = List.of("12", "-5", "abc", "8");

        values.stream()
              .filter(s -> s.matches("\\d+"))   // only positive integers
              .map(Integer::parseInt)
              .filter(n -> n > 0)              // business rule: must be > 0
              .forEach(System.out::println);
    }
}

Wrap Operations with Try/Catch or Fallbacks

Use helper methods to wrap risky operations and provide default values or error logging.

import java.util.List;

public class FallbackExample {
    public static int safeParse(String s) {
        try {
            return Integer.parseInt(s);
        } catch (NumberFormatException e) {
            return -1; // sentinel or fallback value
        }
    }

    public static void main(String[] args) {
        List<String> inputs = List.of("100", "oops", "250");

        inputs.stream()
              .map(FallbackExample::safeParse)
              .filter(n -> n >= 0)
              .forEach(System.out::println);
    }
}

Summary of Best Practices

Strategy Benefit
filter(Objects::nonNull) Avoids NullPointerException
Early validation Prevents downstream errors
Use of Optional and flatMap() Handles absence clearly
Exception-wrapping helper methods Keeps pipelines clean and safe
Provide fallback/default values Maintains continuity of processing

By applying these strategies, your stream pipelines become more robust, readable, and reliable, even in the face of imperfect or inconsistent data.

Index

14.3 Examples with File I/O and Streams

Working with files in stream pipelines often involves handling checked exceptions, particularly IOException. Java provides the Files.lines(Path) method, which returns a lazily populated Stream<String> for reading lines of text from a file. However, because it can throw an exception, it's important to use proper exception handling techniques such as try-with-resources to ensure safety and resource management.

Using try-with-resources ensures that the file stream is automatically closed, even if an exception occurs during stream processing.

Example 1: Reading and Filtering Log Entries

Suppose you have a log file where each line is a log entry, and you want to extract only the ERROR lines.

import java.io.IOException;
import java.nio.file.*;
import java.util.stream.*;

public class ErrorLogFilter {
    public static void main(String[] args) {
        Path logFile = Path.of("application.log");

        try (Stream<String> lines = Files.lines(logFile)) {
            lines.filter(line -> line.contains("ERROR"))
                 .map(String::trim)
                 .forEach(System.out::println);
        } catch (IOException e) {
            System.err.println("Failed to read log file: " + e.getMessage());
        }
    }
}

Explanation:

Example 2: Reading CSV Records with Fallback

This example processes a CSV file where each line contains a user record (e.g., name,email). Some lines may be malformed.

import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;

public class UserCsvProcessor {
    public static void main(String[] args) {
        Path file = Path.of("users.csv");

        try (Stream<String> lines = Files.lines(file)) {
            List<User> users = lines
                .skip(1) // skip header
                .map(UserCsvProcessor::parseUserSafely)
                .flatMap(Optional::stream) // filter out failed parses
                .collect(Collectors.toList());

            users.forEach(System.out::println);
        } catch (IOException e) {
            System.err.println("Unable to read users.csv: " + e.getMessage());
        }
    }

    static Optional<User> parseUserSafely(String line) {
        try {
            String[] parts = line.split(",");
            if (parts.length < 2) return Optional.empty();
            return Optional.of(new User(parts[0].trim(), parts[1].trim()));
        } catch (Exception e) {
            return Optional.empty(); // skip bad record
        }
    }

    static class User {
        String name;
        String email;

        User(String name, String email) {
            this.name = name;
            this.email = email;
        }

        public String toString() {
            return "User{name='" + name + "', email='" + email + "'}";
        }
    }
}

Key Features:

Summary

When working with file-based streams:

These practices ensure robust stream pipelines when reading from disk or other I/O sources.

Index