Catching exceptions out of 'stream()' or &

2020-06-16 03:52发布

问题:

In the following code, when catching NumberFormatException out of for iteration, the strings in appropriate form appearing in strList before the first bad one (i.e., "illegal_3") have been parsed successfully (i.e., "1" and "2" have been parsed as integers 1 and 2).

public void testCaughtRuntimeExceptionOutOfIteration() {
    List<String> strList = Stream.of("1", "2", "illegal_3", "4", "illegal_5", "6").collect(Collectors.toList());
    List<Integer> intList = new ArrayList<>();

    try{
        for (String str : strList) {
            intList.add(Integer.parseInt(str));
        }
    } catch (NumberFormatException nfe) {
        System.err.println(nfe.getMessage());
    }

    List<Integer> expectedIntList = Stream.of(1, 2).collect(Collectors.toList());
    // passed
    assertEquals("The first two elements have been parsed successfully.", expectedIntList, intList);  
}

However, when replacing for iteration by stream() or parallelStream(), I lose 1 and 2.

public void testCaughtRuntimeExceptionOutOfStream() {
    List<String> strList = Stream.of("1", "2", "illegal_3", "4", "illegal_5", "6").collect(Collectors.toList());
    List<Integer> intList = new ArrayList<>();

    try{
        intList = strList.stream()  // same with "parallelStream()"
                .map(Integer::parseInt)
                .collect(Collectors.toList());
    } catch (NumberFormatException nfe) {
        System.err.println(nfe.getMessage());
    }

    List<Integer> expectedIntList = Stream.of(1, 2).collect(Collectors.toList());
    // failed: expected:<[1,2]>, but was:<[]>
    assertEquals("The first two elements have been parsed successfully.", expectedIntList, intList);  
}

What is the specification of the control flow of exceptions thrown from within stream() or parallelStream()?

How can I get the result of intList = [1,2] (i.e., ignore the ones after the first NumberFormatException is thrown) or even better intList = [1,2,4,6] (i.e., ignore the bad ones with NumberFormatException) with stream() or parallelStream()

回答1:

Why not just wrap lambda-body in try...catch?

Also you can filter null values after map:

    intList = strList.stream()// same with "parallelStream()"
            .map(x -> {
                try {
                    return Integer.parseInt(x);
                } catch (NumberFormatException nfe) {
                    System.err.println(nfe.getMessage());
                }
                return null;
            })
            .filter(x -> x!= null)
            .collect(Collectors.toList());

This will give you desired intList = [1,2,4,6].

Edit: To reduce the "heaviness" of a try/catch in a lamdba you can add a helper method.

static Integer parseIntOrNull(String s) {
    try {
        return Integer.parseInt(s);
    } catch (NumberFormatException nfe) {
        System.err.println(nfe.getMessage());
    }
    return null;
}

intList = strList.stream()
            .map(x -> parseIntOrNull(x))
            .filter(x -> x!= null)
            .collect(Collectors.toList());

Or to avoid using null, you can return a Stream

static Stream<Integer> parseIntStream(String s) {
    try {
        return Stream.of(Integer.parseInt(s));
    } catch (NumberFormatException nfe) {
        System.err.println(nfe.getMessage());
    }
    return Stream.empty();
}

intList = strList.stream()
            .flatMap(x -> parseIntStream(x))
            .collect(Collectors.toList());


回答2:

A method can't both return a value, and throw an exception. That is impossible.

So you can't expect collect() to both return a list, and throw an exception. Since if throws an exception, it can't return a new list.

If your for loop code was actually similar to the stream code, you would have the same problem:

public void testCaughtRuntimeExceptionOutOfIteration() {
    List<String> strList = Stream.of("1", "2", "illegal_3", "4", "illegal_5", "6").collect(Collectors.toList());
    List<Integer> intList = new ArrayList<>();

    try{
        intList = collectToIntegers(strList);
    } catch (NumberFormatException nfe) {
        System.err.println(nfe.getMessage());
    }

    List<Integer> expectedIntList = Stream.of(1, 2).collect(Collectors.toList());
    // fails
    assertEquals("The first two elements have been parsed successfully.", expectedIntList, intList);  
}

private List<Integer> collectToIntegers(List<String> strList) {
    List<Integer> result = new ArrayList<>();
    for (String str : strList) {
       result.add(Integer.parseInt(str));
    }
    return result;
}

In short: don't confuse "creating and returning a new list", with "taking a list and add elements to it".



回答3:

I don't know how many times I encountered a situation where I just wanted to ignore the NumberFormatException. I would probably create a separate re-usable method to parse integer silently and return OptionalInt value.

Here is the utils class

public class IntUtils {
    // ... other utility methods

    public static OptionalInt parseInt(String s, Consumer<? super Exception> exceptionConsumer) {
        try {
            return OptionalInt.of(Integer.parseInt(s));
        } catch (NumberFormatException e) {
            if (exceptionConsumer != null) {
                // let the caller take the decision
                exceptionConsumer.accept(e);
            } else {
                // default behavior
                e.printStackTrace();
            }
        }

        return OptionalInt.empty();
    }

    public static OptionalInt parseInt(String s) {
        return parseInt(s, null);
    }
}

Here is the test method

List<Integer> collect1 = strStream.map(str -> IntUtils.parseInt(str, Exception::printStackTrace))
            .filter(OptionalInt::isPresent)
            .map(OptionalInt::getAsInt).collect(toList());

// or 
List<Integer> collect2 = strStream.map(IntUtils::parseInt)
            .filter(OptionalInt::isPresent)
            .map(OptionalInt::getAsInt).collect(toList());


回答4:

I am using org.apache.commons.lang3.math.NumberUtils:

.mapToInt(s -> NumberUtils.toInt(s, Integer.MIN_VALUE))
.filter(x -> x > Integer.MIN_VALUE)