Handling exceptions with streams

Viewed 1110

I have a Map<String,List<String>> and want it to turn into Map<String,List<Long>> because each String in the list represents a Long :

Map<String,List<String>> input = ...;
Map<String,List<Long>> output= 
input.entrySet()
       .stream()
       .collect(toMap(Entry::getKey, e -> e.getValue().stream()
                                                      .map(Long::valueOf)
                                                      .collect(toList()))
               );

My main issue is each String may not represent correctly a Long; there may be some issue. Long::valueOf may raise exceptions. If this is the case, I want to return a null or empty Map<String,List<Long>>

Because I want to iterate after over this output map. But I cannot accept any error conversion; not even a single one. Any idea as to how I can return an empty output in case of incorrect String -> Long conversion?

4 Answers

How about an explicit catch over the exception:

private Map<String, List<Long>> transformInput(Map<String, List<String>> input) {
    try {
        return input.entrySet()
                .stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().stream()
                        .map(Long::valueOf)
                        .collect(Collectors.toList())));
    } catch (NumberFormatException nfe) {
        // log the cause
        return Collections.emptyMap();
    }
}

I personally like to provide an Optional input around number parsing:

public static Optional<Long> parseLong(String input) {
    try {
        return Optional.of(Long.parseLong(input));
    } catch (NumberFormatException ex) {
        return Optional.empty();
    }
}

Then, using your own code (and ignoring bad input):

Map<String,List<String>> input = ...;
Map<String,List<Long>> output= 
input.entrySet()
       .stream()
       .collect(toMap(Entry::getKey, e -> e.getValue().stream()
                                                      .map(MyClass::parseLong)
                                                      .filter(Optional::isPresent)
                                                      .map(Optional::get)
                                                      .collect(toList()))
               );

Additionally, consider a helper method to make this more succinct:

public static List<Long> convertList(List<String> input) {
    return input.stream()
        .map(MyClass::parseLong).filter(Optional::isPresent).map(Optional::get)
        .collect(Collectors.toList());
}

public static List<Long> convertEntry(Map.Entry<String, List<String>> entry) {
    return MyClass.convertList(entry.getValue());
}

Then you can filter the results in your stream's collector:

Map<String, List<Long>> converted = input.entrySet().stream()
    .collect(Collectors.toMap(Entry::getKey, MyClass::convertEntry));

You could also keep the empty Optional objects in your lists, and then by comparing their index in the new List<Optional<Long>> (instead of List<Long>) with the original List<String>, you can find the string which caused any erroneous inputs. You could also simply log these failures in MyClass#parseLong

However, if your desire is to not operate on any bad input at all, then surrounding the entire stream in what you're attempting to catch (per Naman's answer) is the route I would take.

You can create a StringBuilder for key with exception and check if ele is numeric as below,

 public static Map<String, List<Long>> transformInput(Map<String, List<String>> input) {
    StringBuilder sb = new StringBuilder();
    try {
    return input.entrySet()
            .stream()
            .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().stream()
                    .map(ele->{
                        if (!StringUtils.isNumeric(ele)) {
                            sb.append(e.getKey()); //add exception key
                            throw new NumberFormatException();
                        }
                        return Long.valueOf(ele);
                    })
                    .collect(Collectors.toList())));
} catch (NumberFormatException nfe) {
    System.out.println("Exception key "+sb);
    return Collections.emptyMap();
}
}

Hope it helps.

May be you can write an helper method that can check for numeric in the string and filter them out from the stream and also null values then finally collect to the Map.

// StringUtils.java
public static boolean isNumeric(String string) {
    try {
        Long.parseLong(string);
        return true;
    } catch(NumberFormatException e) {
        return false;
    }
}

This will take care everything.

And use this in your stream.

Map<String, List<Long>> newMap = map.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> mapToLongValues(entry.getValue())));

public List<Long> mapToLongValues(List<String> strs) {
    return strs.stream()
        .filter(Objects::nonNull)
        .filter(StringUtils::isNumeric)
        .map(Long::valueOf)
        .collect(Collectors.toList());
}
Related