web-dev-qa-db-ja.com

Collectors.groupingByはnullキーを受け入れません

Java 8では、これは機能します:

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

しかし、これはそうではありません:

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

Mapsはnullキーを許可し、List.class.getSuperclass()はnullを返します。ただし、Collectors.groupingByは、Collectors.Javaの907行目でNPEを出力します。

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

この行を次のように変更して、独自のコレクターを作成すると機能します。

K key = classifier.apply(t);  

私の質問は:

1)Collectors.groupingByのJavadocは、nullキーをマップすべきでないとは言っていません。この動作は何らかの理由で必要ですか?

2)独自のコレクターを作成しなくても、nullキーを受け入れる別の簡単な方法はありますか?

37
MarcG

最初の質問については、skiwiがNPEを投げるべきではないことに同意します。彼らがそれを変えてくれることを願っています(あるいは、少なくともそれをjavadocに追加してください)。一方、2番目の質問に答えるために、Collectors.toMap の代わりに 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;
        }));

または、それをカプセル化する:

/** 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;
            });
    }

そして、次のように使用します:

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

Rolflは、より複雑な別の回答を提供しました。これにより、独自のマップおよびリストサプライヤを指定できます。私はそれをテストしていません。

9
MarcG

同じような問題がありました。 groupingByは分類子から返された値に対してObjects.requireNonNullを実行するため、これは失敗しました。

    Map<Long, List<ClaimEvent>> map = events.stream()
      .filter(event -> eventTypeIds.contains(event.getClaimEventTypeId()))
      .collect(groupingBy(ClaimEvent::getSubprocessId));

Optionalを使用すると、これは機能します。

    Map<Optional<Long>, List<ClaimEvent>> map = events.stream()
      .filter(event -> eventTypeIds.contains(event.getClaimEventTypeId()))
      .collect(groupingBy(event -> Optional.ofNullable(event.getSubprocessId())));
41
Erling

GroupingByの前にフィルターを使用する

GroupingByの前にnullインスタンスを除外します。

MyObjectlist.stream().filter(p -> p.getSomeInstance() != null).collect(Collectors.groupingBy(MyObject::getSomeInstance));
6
Ilan M

まず第一に、あなたはたくさんの生のオブジェクトを使用しています。これは良い考えではありませんすべて、最初に以下を変換します:

  • ClassからClass<?>、つまり。生の型の代わりに、未知のクラスを持つパラメータ化された型。
  • 強制的にHashMapにキャストする代わりに、HashMapをコレクターに提供する必要があります。

最初に、NPEを気にせずに正しくタイプされたコード:

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

ここで、強力なキャストを取り除き、代わりに正しく実行します。

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

ここでは、分類子のみを取得するgroupingByを、分類子、サプライヤ、コレクターを取得するものに置き換えます。基本的にこれは以前と同じですが、現在は正しく入力されています。

あなたは本当に正しいです、javadocではNPEをスローすることは述べられておらず、私が望むものを提供することが許可されているので、それをスローするべきではないと思いますnullキーを許可する場合、許可する必要があります。

今のところ、これをもっと簡単にする他の方法はありません。もっと詳しく調べてみます。

5
skiwi

最初の質問に、ドキュメントから:

返されるMapまたはListオブジェクトの型、可変性、直列化可能性、またはスレッドセーフ性についての保証はありません。

すべてのMap実装でnullキーが許可されているわけではないため、おそらくこれを追加して、マップの最も一般的な許容定義を減らして、タイプを選択する際の柔軟性を最大限に高めます。

2番目の質問に対して、サプライヤーが必要なだけです。ラムダは機能しませんか?私はまだJava 8に精通しています。おそらく、より賢い人がより良い答えを追加できるかもしれません。

3
Jason Sperske

少し時間がかかり、この問題を消化しようと思いました。手動で行った場合に期待すること、およびgroupingBy実装が実際に行うことについて、SSCEをまとめました。

私はこれが答えだとは思いませんが、「なぜそれが問題なのか疑問に思う」ことです。また、必要に応じて、このコードをハックして、nullフレンドリーなコレクターを作成してください。

編集:汎用的な実装:

/** groupingByNF - NullFriendly - allows you to specify your own Map and List supplier. */
private static final <T,K> Collector<T,?,Map<K,List<T>>> groupingByNF (
        final Supplier<Map<K,List<T>>> mapsupplier,
        final Supplier<List<T>> listsupplier,
        final Function<? super T,? extends K> classifier) {

    BiConsumer<Map<K,List<T>>, T> combiner = (m, v) -> {
        K key = classifier.apply(v);
        List<T> store = m.get(key);
        if (store == null) {
            store = listsupplier.get();
            m.put(key, store);
        }
        store.add(v);
    };

    BinaryOperator<Map<K, List<T>>> finalizer = (left, right) -> {
        for (Map.Entry<K, List<T>> me : right.entrySet()) {
            List<T> target = left.get(me.getKey());
            if (target == null) {
                left.put(me.getKey(), me.getValue());
            } else {
                target.addAll(me.getValue());
            }
        }
        return left;
    };

    return Collector.of(mapsupplier, combiner, finalizer);

}

/** groupingByNF - NullFriendly - otherwise similar to Java8 Collections.groupingBy */
private static final <T,K> Collector<T,?,Map<K,List<T>>> groupingByNF (Function<? super T,? extends K> classifier) {
    return groupingByNF(HashMap::new, ArrayList::new, classifier);
}

次のコードを検討してください(コードは、String.length()に基づいてストリング値をグループ化します(入力ストリングがヌルの場合はヌルです))。

public static void main(String[] args) {

    String[] input = {"a", "a", "", null, "b", "ab"};

    // How we group the Strings
    final Function<String, Integer> classifier = (a) -> {return a != null ? Integer.valueOf(a.length()) : null;};

    // Manual implementation of a combiner that accumulates a string value based on the classifier.
    // no special handling of null key values.
    BiConsumer<Map<Integer,List<String>>, String> combiner = (m, v) -> {
        Integer key = classifier.apply(v);
        List<String> store = m.get(key);
        if (store == null) {
            store = new ArrayList<String>();
            m.put(key, store);
        }
        store.add(v);
    };

    // The finalizer merges two maps together (right into left)
    // no special handling of null key values.
    BinaryOperator<Map<Integer, List<String>>> finalizer = (left, right) -> {
        for (Map.Entry<Integer, List<String>> me : right.entrySet()) {
            List<String> target = left.get(me.getKey());
            if (target == null) {
                left.put(me.getKey(), me.getValue());
            } else {
                target.addAll(me.getValue());
            }
        }
        return left;
    };

    // Using a manual collector
    Map<Integer, List<String>> manual = Arrays.stream(input).collect(Collector.of(HashMap::new, combiner, finalizer));

    System.out.println(manual);

    // using the groupingBy collector.        
    Collector<String, ?, Map<Integer, List<String>>> collector = Collectors.groupingBy(classifier);

    Map<Integer, List<String>> result = Arrays.stream(input).collect(collector);

    System.out.println(result);
}

上記のコードは出力を生成します:

{0=[], null=[null], 1=[a, a, b], 2=[ab]}
Exception in thread "main" Java.lang.NullPointerException: element cannot be mapped to a null key
  at Java.util.Objects.requireNonNull(Objects.Java:228)
  at Java.util.stream.Collectors.lambda$groupingBy$135(Collectors.Java:907)
  at Java.util.stream.Collectors$$Lambda$10/258952499.accept(Unknown Source)
  at Java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.Java:169)
  at Java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.Java:948)
  at Java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.Java:512)
  at Java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.Java:502)
  at Java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.Java:708)
  at Java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.Java:234)
  at Java.util.stream.ReferencePipeline.collect(ReferencePipeline.Java:499)
  at CollectGroupByNull.main(CollectGroupByNull.Java:49)
3
rolfl