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.
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?
We will consider how to implement each of these.
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.
Keep vavr available.
build.gradle
dependencies {
...
implementation 'io.vavr:vavr:0.9.3'
}
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.
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
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.
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.
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] **
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.
-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