14

I am dealing with a Map<String,String> that has null entries in the key and/or value:

Map<String, String> headers = new HashMap<>();
headers.put("SomE", "GreETing");
headers.put("HELLO", null);
headers.put(null, "WOrLd");

headers.keySet().stream().forEach(k -> System.out.println(k + " => " + copy.get(k)));

I get the following output:

SomE => GreETing
HELLO => null
null => WOrLd

I need to transform the map, so all the non-null values are converted to lowercase, like so:

some => greeting
hello => null
null => world

I am trying to use Java 8 streams API, but the following code is throwing NullPointerException:

Map<String,String> copy
    = headers.entrySet()
             .stream()
             .collect(
                 Collectors.toMap(
                     it -> it.getKey() != null ? it.getKey().toLowerCase() : null,
                     it -> it.getValue() != null ? it.getValue().toLowerCase() : null));

copy.keySet().stream().forEach(k -> System.out.println(k + " => " + copy.get(k)));

If I comment out the last two map entries, the program executes, so there must be an issue with how Collectors.toMap works when keys or values are null. How do I use the streams API to work around this?

3
  • maybe use optional: docs.oracle.com/javase/8/docs/api/java/util/Optional.html Commented Mar 2, 2017 at 4:45
  • toMap method throws that NPE for you have a null value, thanks @dkatzel Commented Mar 2, 2017 at 4:46
  • 3
    Actually key can be null. value can't be null Commented Mar 2, 2017 at 4:47

4 Answers 4

15

The problem is toMap() invokes the underlying Map implementation being built's merge() function which does not allow values to be null

from the javadoc for Map#merge (emphasis mine)

If the specified key is not already associated with a value or is associated with null, associates it with the given non-null value. Otherwise, replaces the associated value with the results of the given remapping function, or removes if the result is null.

So using Collectors.toMap() will not work.

You can do this without stream just fine:

Map<String,String> copy = new HashMap<>();

for(Entry<String, String> entry : headers.entrySet()){
    copy.put(entry.getKey() !=null ? entry.getKey().toLowerCase() : null, 
             entry.getValue() !=null ? entry.getValue().toLowerCase() : null
            );
}
Sign up to request clarification or add additional context in comments.

Comments

13

Use Collect:

final Function<String, String> fn= str -> str == null ? null : str.toLowerCase();
Map<String, String> copy = headers.entrySet().stream()
   .collect(HashMap::new,
            (m, e) -> m.put(fn.apply(e.getKey()), fn.apply(e.getValue())), 
            Map::putAll);

Or with abacus-common

Map<String, String> copy = Stream.of(headers)
   .collect(HashMap::new, 
     (m, e) -> m.put(N.toLowerCase(e.getKey()), N.toLowerCase(e.getValue())));

updated on 2/4, Or:

Map<String, String> copy = EntryStream.of(headers)
   .toMap(entry -> N.toLowerCase(entry.getKey()), entry -> N.toLowerCase(entry.getValue()));

1 Comment

This is a good answer too. I am happy to get 3 working pieces of code. Now I need to work on understanding streams and lambdas better and these answers will help!
1

You cannot use Collectors.toMap() without getting a NPE since you have a null value present in your map, as explained by @dkatzel already, but I still wanted to use Stream API;

Map<String, String> headers = new HashMap<>();
headers.put("good", "AsDf");
headers.put("SomE", "GreETing");
headers.put("HELLO", null);
headers.put(null, "WOrLd");

new HashSet<>(headers.entrySet()).stream()
    .peek(entry -> entry.setValue(Objects.isNull(entry.getValue()) ? null : entry.getValue().toLowerCase()))
    .filter(entry -> !Objects.isNull(entry.getKey()) && !entry.getKey().equals(entry.getKey().toLowerCase()))
    .forEach(entry -> {
        headers.put(entry.getKey().toLowerCase(), entry.getValue());
        headers.remove(entry.getKey());
    });

System.out.println(headers);

Prints out;

{null=world, some=greeting, hello=null, good=asdf}

4 Comments

I am interested in this example but I could not get the code to print anything. Can you elaborate or give me a working code snippet?
@WebUser it does not return anything, it directly modifies the original headers map you have, try to check its entries after you run this code!
Thanks! I would have voted for your answer if it had come in first. I am happy with the answer from @dkatzel and I don't want to revoke my best answer vote now. I did upvote yours and I hope others do too.
Terribly unmaintainable.
0

If you have complicated checks and still want to use Collectors.toMap() without nulls

// convert data to a final key, value pairs using Pair.of(key, value)
.map(data -> {
    SomeKey key = makeKey(data.getKey());
    SomeVal val = makeValue(data.getValue());
    
    // validate and return 
    if(key.foo != null && val.bar != null) {
        return Pair.of(key.foo, val.bar);
    }
    return null;
})
.filter(Objects::nonNull)   // remove the ones that don't satisy criteria
.collect(Collectors.toMap(pair -> pair.getLeft(), pair -> pair.getRight(),  // then collect final key/value data without NPE checks
    (left, right) -> {
        left.merge(right); // can return null
        return left;
    }
)));

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.