Search code examples
javahashmapjava-8java-streamcollectors

Collectors.groupingBy doesn't accept null keys


In Java 8, this works:

Stream<Class> stream = Stream.of(ArrayList.class);
HashMap<Class, List<Class>> map = (HashMap)stream.collect(Collectors.groupingBy(Class::getSuperclass));

But this doesn't:

Stream<Class> stream = Stream.of(List.class);
HashMap<Class, List<Class>> map = (HashMap)stream.collect(Collectors.groupingBy(Class::getSuperclass));

Maps allows a null key, and List.class.getSuperclass() returns null. But Collectors.groupingBy emits a NPE, at Collectors.java, line 907:

K key = Objects.requireNonNull(classifier.apply(t), "element cannot be mapped to a null key"); 

It works if I create my own collector, with this line changed to:

K key = classifier.apply(t);  

My questions are:

1) The Javadoc of Collectors.groupingBy doesn't say it shouldn't map a null key. Is this behavior necessary for some reason?

2) Is there another, easier way, to accept a null key, without having to create my own collector?


Solution

  • For the first question, I agree with skiwi that it shouldn't be throwing a NPE. I hope they will change that (or else at least add it to the javadoc). Meanwhile, to answer the second question I decided to use Collectors.toMap instead of Collectors.groupingBy:

    Stream<Class<?>> stream = Stream.of(ArrayList.class);
    
    Map<Class<?>, List<Class<?>>> map = stream.collect(
        Collectors.toMap(
            Class::getSuperclass,
            Collections::singletonList,
            (List<Class<?>> oldList, List<Class<?>> newEl) -> {
            List<Class<?>> newList = new ArrayList<>(oldList.size() + 1);
            newList.addAll(oldList);
            newList.addAll(newEl);
            return newList;
            }));
    

    Or, encapsulating it:

    /** Like Collectors.groupingBy, but accepts null keys. */
    public static <T, A> Collector<T, ?, Map<A, List<T>>>
    groupingBy_WithNullKeys(Function<? super T, ? extends A> classifier) {
        return Collectors.toMap(
            classifier,
            Collections::singletonList,
            (List<T> oldList, List<T> newEl) -> {
                List<T> newList = new ArrayList<>(oldList.size() + 1);
                newList.addAll(oldList);
                newList.addAll(newEl);
                return newList;
                });
        }
    

    And use it like this:

    Stream<Class<?>> stream = Stream.of(ArrayList.class);
    Map<Class<?>, List<Class<?>>> map = stream.collect(groupingBy_WithNullKeys(Class::getSuperclass));
    

    Please note rolfl gave another, more complicated answer, which allows you to specify your own Map and List supplier. I haven't tested it.