web-dev-qa-db-ja.com

Collectors.toMapマージ関数でキーを取得するにはどうすればよいですか?

Collectors.toMap()中に重複するキーエントリが見つかった場合、マージ関数(o1, o2)が呼び出されます。

質問:重複の原因となったキーを取得するにはどうすればよいですか?

String keyvalp = "test=one\ntest2=two\ntest2=three";

Pattern.compile("\n")
    .splitAsStream(keyval)
    .map(entry -> entry.split("="))
    .collect(Collectors.toMap(
        split -> split[0],
        split -> split[1],
        (o1, o2) -> {
            //TODO how to access the key that caused the duplicate? o1 and o2 are the values only
            //split[0]; //which is the key, cannot be accessed here
        },
    HashMap::new));

マージ関数内で、keyに基づいて決定したいのですが、マッピングをキャンセルするか、続行してそれらの値を引き継ぎます。

14
membersound

カスタムコレクターを使用するか、別のアプローチを使用する必要があります。

Map<String, String> map = new Hashmap<>();
Pattern.compile("\n")
    .splitAsStream(keyval)
    .map(entry -> entry.split("="))
    .forEach(arr -> map.merge(arr[0], arr[1], (o1, o2) -> /* use arr[0]));

カスタムコレクターの作成はかなり複雑です。 TriConsumer(キーと2つの値)が必要ですが、これはJDKにはありません。そのため、を使用する組み込み関数はないと確信しています。 ;)

6
Peter Lawrey

マージ関数を省略すると、組み込み関数と同じ問題であるマージ関数がキーを取得する機会がありません。

解決策は、Map.mergeに依存しない別のtoMap実装を使用することです。

public static <T, K, V> Collector<T, ?, Map<K,V>>
    toMap(Function<? super T, ? extends K> keyMapper,
          Function<? super T, ? extends V> valueMapper) {
    return Collector.of(HashMap::new,
        (m, t) -> {
            K k = keyMapper.apply(t);
            V v = Objects.requireNonNull(valueMapper.apply(t));
            if(m.putIfAbsent(k, v) != null) throw duplicateKey(k, m.get(k), v);
        },
        (m1, m2) -> {
            m2.forEach((k,v) -> {
                if(m1.putIfAbsent(k, v)!=null) throw duplicateKey(k, m1.get(k), v);
            });
            return m1;
        });
}
private static IllegalStateException duplicateKey(Object k, Object v1, Object v2) {
    return new IllegalStateException("Duplicate key "+k+" (values "+v1+" and "+v2+')');
}

(これは基本的に、マージ関数を使用しないJava 9のtoMapの実装が行うことです)

したがって、コードで行う必要があるのは、toMap呼び出しをリダイレクトし、マージ関数を省略することだけです。

String keyvalp = "test=one\ntest2=two\ntest2=three";

Map<String, String> map = Pattern.compile("\n")
        .splitAsStream(keyvalp)
        .map(entry -> entry.split("="))
        .collect(toMap(split -> split[0], split -> split[1]));

(または、同じクラスでも静的インポートでもない場合はContainingClass.toMap)<\ sup>

コレクターは、元のtoMapコレクターのように並列処理をサポートしますが、処理する要素が多い場合でも、ここで並列処理のメリットを得る可能性はほとんどありません。

正しく取得できた場合、実際のキーに基づいてマージ関数で古い値または新しい値のいずれかのみを選択する場合は、次のようなキーPredicateを使用して行うことができます。

public static <T, K, V> Collector<T, ?, Map<K,V>>
    toMap(Function<? super T, ? extends K> keyMapper,
          Function<? super T, ? extends V> valueMapper,
          Predicate<? super K> useOlder) {
    return Collector.of(HashMap::new,
        (m, t) -> {
            K k = keyMapper.apply(t);
            m.merge(k, valueMapper.apply(t), (a,b) -> useOlder.test(k)? a: b);
        },
        (m1, m2) -> {
            m2.forEach((k,v) -> m1.merge(k, v, (a,b) -> useOlder.test(k)? a: b));
            return m1;
        });
}
Map<String, String> map = Pattern.compile("\n")
        .splitAsStream(keyvalp)
        .map(entry -> entry.split("="))
        .collect(toMap(split -> split[0], split -> split[1], key -> condition));

このコレクターをカスタマイズする方法はいくつかあります…

4
Holger

もちろん、単純で些細なトリックがあります。「キーマッパー」関数でキーを保存し、「マージ」関数でキーを取得します。したがって、コードは次のようになります(キーが整数であると想定)。

final AtomicInteger key = new AtomicInteger(); 
...collect( Collectors.toMap( 
   item -> { key.set(item.getKey()); return item.getKey(); }, // key mapper 
   item -> ..., // value mapper
   (v1, v2) -> { log(key.get(), v1, v2); return v1; } // merge function
);

注:これは並列処理には適していません。

0
levko