In my last blog entry, I used a small piece of code I was working on to demonstrate how pre-JDK 8 code using external iteration could be converted to use streams with a powerful standard collector. I also highlighted the problem that can arise when exceptions can be thrown from within a Lambda expression used in a stream.
Although I described two simple approaches to dealing with exceptions, in this case, I chose in my final code not to use the exception generating methods, as I didn’t need them. It did, however, get me thinking about what the best solution to the problem would be if I did need to use exception generating methods.
The initial problem was to parse a HTTP parameter string, separated using & and = (for example “action=verify&key=10101&key=22222&key=10101”), into a collection. Let’s look at the code again and include the URLDecoder.decode()
method, which can throw an exception. We’ll use a local method to deal with those exceptions
private Map<String, List<String>> parseQuery(String query) { return (query == null) ? null : Arrays.stream(query.split("[&]")) .collect(groupingBy(s -> splitKeyValue(s, 0), mapping(s -> splitKeyValue(s, 1), toList()))); }
private String splitKeyValue(String keyValue, int part) { try { return URLDecoder.decode((keyValue.split("[=]"))[part], System.getProperty("file.encoding")); } catch (UnsupportedEncodingException uee) { return null; } }
This will work, but we’re plagued here by the curse of the null pointer. If you pass in a query that is null you’ll get back a null rather than a reference to an empty Map. If the encoding is not supported you’ll get back an instance of a Map, but the only key-value pair will be null and null. The obvious problem with this situation is the need to perform null checks on both the Map reference returned by parseQuery()
and its contents if non-null.
We can do much better and we only need to add seven lines of code. Here’s how.
The easy bit is to replace the ternary return statement in parseQuery()
so we guarantee that we always return an instance of a Map (I’ll admit I should really have done this in the original blog post. My bad for trying to be too smart and only use three lines of code):
private Map<String, List> parseQuery(String query) { if (query == null) return new Map<String, List>(); return Arrays.stream(query.split("[&]")) .collect(groupingBy(s -> splitKeyValue(s, 0), mapping(s -> splitKeyValue(s, 1), toList()))); }
Now how do we deal with the stream? The typical flow for a stream is the well-known filter-map-reduce pattern. In this case, we change it slightly to map-filter-reduce.
Firstly, the mapping needs to create a stream of objects that encapsulate the key-value pairs extracted from the string. Unfortunately, Java does not provide a nice Tuple<U, V>
; class, which would be perfect in this case. Of course, we could always create our own simple KeyValue<U, V>
class and use that, but I don’t like creating classes unless there’s a good reason to do so. Fortunately, hidden in the depths of the Collections API there is an inner interface of Map, Map.Entry<K, V>
. There is a concrete implementation of this, AbstractMap.SimpleImmutableEntry
, which suits our needs perfectly (thanks to Nicolai Parlog for pointing this out to me; I’m afraid I still haven’t memorised all 4,000-plus classes in the standard Java libraries).
The most important thing is that the Function we use in our map()
method must handle all situations where an exception could be thrown. Since we can’t do anything else we return a null instead of an Entry
object. It’s important to include logging (or some other processing) of this type of event so that we have some pathology if we need to track down bugs that are causing the exceptions.
We’ll modify our splitKeyValue()
method, thus:
private AbstractMap.SimpleImmutableEntry<String, String> splitKeyValue(String keyValue) { String enc = System.getProperty("file.encoding"); String[] parts = keyValue.split(“[=]”); try { return new AbstractMap.SimpleImmutableEntry<>( URLDecoder.decode(parts[0], enc), URLDecoder.decode(parts[1], enc)); } catch (UnsupportedEncodingException uee) { logger.warning(“Exception details of processing key-value pair”); return null; } }
The first part of our stream then becomes:
return Arrays.stream(query.split("[&]")) .map(this::splitKeyValue)
This will result is a stream of either valid Entry
objects or nulls. To avoid putting a null key in our final map we just employ a simple filter to remove them
return Arrays.stream(query.split("[&]")) .map(this::splitKeyValue) .filter(e -> e != null)
Finally, we feed this stream into collect()
and use the same approach as the original except that we retrieve the keys and values using method references on Entry
rather than by splitting the key-value string.
return Arrays.stream(query.split("[&]")) .map(this::splitKeyValue) .filter(e -> e != null) .collect(groupingBy(Map.Entry::getKey, mapping(Map.Entry::getValue, toList())));
Although exceptions can be awkward in streams using an approach like this can at least provide a way of using methods that throw exceptions in our Lambda expressions yet avoid having to null-check our results.