web-dev-qa-db-fra.com

Comment obtenir la clé dans la fonction de fusion Collectors.toMap?

Lorsqu'une entrée de clé en double est trouvée pendant Collectors.toMap(), la fonction de fusion (o1, o2) est appelée.

Question: Comment puis-je obtenir la clé qui a causé la duplication?

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));

Dans la fonction de fusion, je veux décider en fonction de la clé qui, si j'annule le mappage, ou continue et prend en charge ces valeurs.

10
membersound

Vous devez utiliser un collecteur personnalisé ou utiliser une approche différente.

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]));

Écrire un collectionneur personnalisé est un peu plus compliqué. Vous avez besoin d’un TriConsumer (clé et deux valeurs) qui soit similaire, ce qui n’appartient pas au JDK, c’est pourquoi je suis presque sûr qu’il n’ya pas de fonction intégrée qui utilise. ;)

5
Peter Lawrey

La fonction de fusion n'a aucune chance d'obtenir la clé, ce qui est le même problème que la fonction intégrée a lorsque vous omettez la fonction de fusion.

La solution consiste à utiliser une implémentation toMap différente, qui ne repose pas sur Map.merge:

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+')');
}

(C’est essentiellement ce que fera l’implémentation de toMap sans fonction de fusion par Java 9)

Donc tout ce que vous avez à faire dans votre code, est de rediriger l'appel toMap et d'omettre la fonction de fusion:

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]));

(ou ContainingClass.toMap si ce n'est ni dans la même classe ni dans les importations statiques) <\ sup>

Le collecteur prend en charge le traitement parallèle, à l’instar du collecteur toMap original, bien qu’il soit peu probable que le traitement parallèle présente un avantage, même avec plus d’éléments à traiter.

Si, si je vous ai bien compris, vous ne voulez choisir que la valeur la plus ancienne ou la plus récente, dans la fonction de fusion basée sur la clé réelle, vous pouvez le faire avec une clé Predicate comme ceci

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));

Il existe plusieurs façons de personnaliser ce collecteur…

4
Holger

Il y a bien sûr une astuce simple et triviale - enregistrer la clé dans la fonction «mappeur de clé» et obtenir la clé dans la fonction «fusion». Ainsi, le code peut ressembler à ceci (en supposant que la clé est Integer):

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
);

Remarque: ce n'est pas bon pour le traitement parallèle.

0
levko