Leverage Either for individual exception handling in the Java Stream API

Java's Stream API is very useful, but exception handling is tedious.

There is a way to replace it with a runtime exception in lambda or use Optional, but I want to handle it a little more properly. I want to use Either as a monad because it's a big deal. As a result, Either is not implemented by default so far, but it seems that it can be done by using the vavr library. It seems that the name of the library that was formerly called javaslang has changed.

Either is a container that holds two elements in Right / Left like Tuple, and by holding a normal value in Right and an error in Left, you can connect processing in a chain like Optional. Optional and Either monads learned from Java8 Stream.

So, I would like to organize how to utilize Either with Stream API.

Individual exception handling patterns in Stream

If you do something to each element of the stream and an exception occurs within that process, what do you want to do with that exception? For the time being, do you think of the following three patterns?

  1. I want to ignore all the elements that raised the exception and extract only the successful elements
  2. I want to catch the first exception
  3. I want to catch all the exceptions that have occurred

We will consider how to implement each of these.

0. Advance preparation

Sample code

Here is a sample code to think about this problem. Takes a string list as an argument and creates an Integer list with Integer # parseInt in between.

Test.java


public void test() {
    List<String> list = Arrays.asList("123", "abc", "45", "def", "789");

    List<Integer> result = list.stream()   //Make it Stream
            .map(this::parseInt)           //Convert individual elements
            .collect(Collectors.toList()); //Return to List as termination
}

private Integer parseInt(String data) {
    try {
        return Integer.parseInt(value);
    } catch (Exception ex) {
        return null; //I'm squeezing the error and making it null
    }
}

This sample squeezes the error and returns null, so [123, null, 45, null, 789] You will get a list like that.

Import vavr

Keep vavr available.

build.gradle


dependencies {
    ...
    implementation 'io.vavr:vavr:0.9.3'
}

1. Extract only successful elements

Either is not needed in this case as it does not handle the error.

FilterTest.java


public void testFilterSuccess() {
    List<String> data = Arrays.asList("123", "abc", "45", "def", "789");

    // List<Integer>
    var result = data.stream()
            .map(this::parseIntOpt)
            .filter(Optional::isPresent).map(Optional::get)
            .collect(Collectors.toList());
}

private Optional<Integer> parseIntOpt(String value) {
    try {
        return Optional.of(Integer.parseInt(value));
    } catch (Exception ex) {
        return Optional.empty();
    }
}

Filter only the elements that have values with filter, and extract with map to finish.

2. Extract the first exception

The main subject is from here.

First, modify the element processing so that it returns Either.

EitherTest.java


private Either<Exception, Integer> parseIntEither(String value) {
    try {
        return Either.right(Integer.parseInt(value));
    } catch (Exception ex) {
        return Either.left(ex);
    }
}

Modified the sample to use this function

EitherTest.java


public void testFirstException() {
    List<String> data = Arrays.asList("123", "abc", "45", "def", "789");

    // List<Either<Exception, Integer>>
    var result = data.stream()
            .map(this::parseIntEither)
            .collect(Collectors.toList()));
}

At this stage it is still a list of Either. If there is an error, we want to extract the first error, and if it is normal, we want to get List <Integer>, so we will perform further conversion. Here, we use Either # sequenceRight, which is a static method implemented in vavr.

EitherTest.java


// Either<Exception, Seq<Integer>>
var result = Either.sequenceRight(data.stream()
        .map(this::parseIntEither)
        .collect(Collectors.toList()));

This method applies the List of Either in order, and if there is ʻEither.left (), it holds the value, and if it is all ʻEither.right (), it aggregates it ʻEither.right (Seq ) `Will be returned. All you have to do is retrieve the value from Either.

EitherTest.java


try {
    List<Integer> intList = result.getOrElseThrow(ex -> ex).toJavaList();
    // handle intList
} catch (Exception ex) {
    // handle exception
}

Since Seq is a Sequence class defined in vavr, it is converted to a Java List, but if there is no problem, subsequent processing can be performed with Seq as it is.

So, in summary, the code is as follows.

EitherTest.java


public void testFirstException() {
    List<String> data = Arrays.asList("123", "abc", "45", "def", "789");

    try {
        List<Integer> result = Either.sequenceRight(data.stream()
                .map(this::parseIntEither)
                .collect(Collectors.toList()))
                .getOrElseThrow(ex -> ex).toJavaList();
    } catch (Exception ex) {
        //The error below is thrown
        // For input string: "abc"
    }
}

I feel that I can write it quite neatly.

3. Extract all exceptions that occur

It is the same as 2. until the middle. If you use Either # sequence instead of Either # sequenceRight, it will collect all Exceptions and return Seq.

EitherTest.java


// Either<Seq<Exception>, Seq<Integer>>
var result = Either.sequence(data.stream()
        .map(this::parseIntEither)
        .collect(Collectors.toList()));

Using this, if you put together the collected Exception list with fold, you can write as follows.

EitherTest.java


public void testAllException() {
    List<String> data = Arrays.asList("123", "abc", "45", "def", "789");

    try {
        List<Integer> result = Either.sequence(data.stream()
                .map(this::parseIntEither)
                .collect(Collectors.toList()))
                .getOrElseThrow(exSeq -> exSeq.fold(new Exception("Multiple Exception"),
                        (root, value) -> {
                            root.addSuppressed(value);
                            return root;
                        }))
                .toJavaList();
    } catch (Exception ex) {
        // ex.getSuppressed()To"abc,"def"Exception holding 2 Exceptions is thrown
    }
}

Now you can also aggregate the Exceptions for each element.

Extra: Extract Exception List and Integer List respectively (additional)

Either is a class that always takes either the Left or Right state, so if there is an Exception, it is isLeft (), otherwise it is isRight (). Therefore, if there is an Exception, it is not possible to collect the normal value while extracting the Exception. I'm not sure if there is such a use, but I will also consider how to separate List <Either <Exception, Integer >> into List <Exception> and List <Integer> for study purposes. ..

The approach is to terminate Stream

collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner)

Collect to Tuple2 <List <Exception>, List <Integer >> of vavr using.

TupleTest.java


//Tuple2<List<Exception>, List<Integer>>
var result = data.stream()
        .map(this::parseIntEither)
        .collect(() -> Tuple.of(new ArrayList<Exception>(), new ArrayList<Integer>()),
                (accumulator, value) -> {
                    if (value.isLeft()) {
                        accumulator._1.add(value.getLeft());
                    } else {
                        accumulator._2.add(value.get());
                    }
                },
                (left, right) -> {
                    left._1.addAll(right._1);
                    left._2.addAll(right._2);
                });

It's not very beautiful if you write so far. .. If you really want to implement it, you may want to create a separate class that implements the Collector interface.

** [Addition] **

In the comments, he taught me how to write very simply.

collect(Collectors.collectingAndThen(
  Collectors.groupingBy(Either::isLeft), 
  map -> Tuple.of(map.get(true),map.get(false))
);

Collectors.groupingBy groups the elements of Stream by the specified process and returns a Map of (grouping key-> element list). In this case, you get Map <Boolean, Seq <Either <Exception, Integer >>> by grouping by whether Either :: isLeft is true or false.

In addition, Collectors.collectingAndThen can describe the collect process in the first argument and the subsequent process in the second argument. So the second argument takes the value from the map and converts it to Tuple.

If you organize it like the other examples, the code will be as follows.

TupleTest2.java


var result = data.stream()
        .map(this::parseIntEither)
        .collect(Collectors.collectingAndThen(
                Collectors.groupingBy(Either::isLeft),
                map -> Tuple.of(
                        Either.sequence(map.get(true)).getLeft().toJavaList(),
                        Either.sequence(map.get(false)).get().toJavaList())
        ));

The code is clearly cleaner. The Collectors method is convenient, isn't it? I want to keep it down.

** [Addition here] **

Summary

I got the feeling that the exception handling of Stream, which was annoying, could be written quite neatly. I was worried about how to organize Either neatly for a while, but as a matter of course, a convenient method was implemented in vavr. vavr convenient.

reference

-How to handle exceptions coolly with Stream or Optional of Java8 -[JAVA] STREAM and exception handling are incompatible -Optional monads and Either monads learned from Java8 Stream.

Recommended Posts

Leverage Either for individual exception handling in the Java Stream API
Try using the Stream API in Java
ChatWork4j for using the ChatWork API in Java
Java Stream API in 5 minutes
Exception handling techniques in Java
[java8] To understand the Stream API
Parsing the COTOHA API in Java
Call the Windows Notification API in Java
Java exception handling?
Java Stream API
[Java] Exception handling
☾ Java / Exception handling
Java exception handling
Java exception handling
[Java] Handling of JavaBeans in the method chain
Try using the COTOHA API parsing in Java
Questions in java exception handling throw and try-catch
[Java] Stream API / map
Use Java lambda expressions outside of the Stream API
Error logging and exception handling that you can't often see in the Java area
A note for Initializing Fields in the Java tutorial
Java8 Stream API practice
Zabbix API in Java
I translated [Clone method for Java arrays] as the Clone method in Java arrays.
[Must-see for apprentice java engineer] How to use Stream API
Tips for using Salesforce SOAP and Bulk API in Java
[Java] Practice of exception handling [Exception]
Java Stream API cheat sheet
[Java] About try-catch exception handling
[Ruby] Exception handling in functions
Java exception handling usage rules
[Java] Stream API --Stream termination processing
[Java] Stream API --Stream intermediate processing
[Java] Introduction to Stream API
Use Redis Stream in Java
Java application for beginners: stream
[Java] Stream API intermediate operation
Let's consider the meaning of "stream" and "collect" in Java's Stream API.
Sample code to call the Yahoo! Local Search API in Java
Access the network interface in Java
[Introduction to Java] About Stream API
[In-house study session] Java exception handling (2017/04/26)
Specify the java location in eclipse.ini
Unzip the zip file in Java
Generate CloudStack API URL in Java
Hit Zaim's API (OAuth 1.0) in Java
I tried using Java8 Stream API
Hit the Docker API in Rust
Settings for SSL debugging in Java
Tips for handling enums in thymeleaf
JPA (Java Persistence API) in Eclipse
Do you use Stream in Java?
Tips for handling pseudo-elements in Selenium
Call the super method in Java
For the time being, run the war file delivered in Java with Docker
I called the COTOHA API parser 100 times in Java to measure performance.
[Socket communication (Java)] Impressions of implementing Socket communication in practice for the first time
[Java] Generate a narrowed list from multiple lists using the Stream API
To you who were told "Don't use Stream API" in the field
Programming for the first time in my life Java 1st Hello World