Java8: Stream / Map-Reduce / Collector kullanarak HashMap <X, Y> ile HashMap <X, Z>


210

Basit bir Java "dönüşümü" bilen Listden Y-> Zyani:

List<String> x;
List<Integer> y = x.stream()
        .map(s -> Integer.parseInt(s))
        .collect(Collectors.toList());

Şimdi temelde bir Harita ile aynı şeyi yapmak istiyorum, yani:

INPUT:
{
  "key1" -> "41",    // "41" and "42"
  "key2" -> "42      // are Strings
}

OUTPUT:
{
  "key1" -> 41,      // 41 and 42
  "key2" -> 42       // are Integers
}

Çözelti String-> ile sınırlı olmamalıdır Integer. Sadece olduğu gibi Listyukarıdaki örnekte, ben herhangi bir yöntem (veya yapıcı) çağırmak istiyorum.

Yanıtlar:


373
Map<String, String> x;
Map<String, Integer> y =
    x.entrySet().stream()
        .collect(Collectors.toMap(
            e -> e.getKey(),
            e -> Integer.parseInt(e.getValue())
        ));

Liste kodu kadar iyi değil. Map.EntryBir map()çağrıda yeni yapılar oluşturamazsınız , böylece çalışma collect()çağrıya karışır .


59
Sen yerini alabilir e -> e.getKey()ile Map.Entry::getKey. Ama bu bir zevk / programlama tarzı.
Holger

5
Aslında bu bir performans meselesi, lambda 'stiline' göre biraz daha üstün olduğunu düşündürüyor
Jon Burgin

36

İşte Sotirios Delimanolis'in cevabında bazı varyasyonlar var , ki bu da (+1) ile başlamak için oldukça iyiydi. Aşağıdakileri göz önünde bulundur:

static <X, Y, Z> Map<X, Z> transform(Map<? extends X, ? extends Y> input,
                                     Function<Y, Z> function) {
    return input.keySet().stream()
        .collect(Collectors.toMap(Function.identity(),
                                  key -> function.apply(input.get(key))));
}

Burada birkaç nokta var. Birincisi jeneriklerde joker karakter kullanımı; bu işlevi biraz daha esnek hale getirir. Örneğin, çıktı haritasının, girdi haritasının anahtarının bir üst sınıfı olan bir anahtara sahip olmasını istiyorsanız bir joker karakter gerekir:

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<CharSequence, Integer> output = transform(input, Integer::parseInt);

(Haritanın değerleri için de bir örnek var, ancak gerçekten de anlaşıldı ve Y için sınırlı joker karaktere sahip olmanın sadece kenar durumlarda yardımcı olduğunu itiraf ediyorum.)

İkinci nokta, giriş haritasının üzerinde akışı çalıştırmak yerine, üzerinde entrySetkoştum keySet. Bu, kodu harita girişi yerine haritadan almak zorunda kalmanın pahasına, bence biraz daha temiz hale getirir. Bu arada, başlangıçta key -> keyilk argüman olarak vardı toMap()ve bu bir nedenle bir tür çıkarım hatası ile başarısız oldu. (X key) -> keyÇalıştığı gibi değiştirildi Function.identity().

Yine başka bir varyasyon şöyledir:

static <X, Y, Z> Map<X, Z> transform1(Map<? extends X, ? extends Y> input,
                                      Function<Y, Z> function) {
    Map<X, Z> result = new HashMap<>();
    input.forEach((k, v) -> result.put(k, function.apply(v)));
    return result;
}

Bu Map.forEach(), akışlar yerine kullanır . Bu daha da basit, sanırım, çünkü haritalarla kullanmak için biraz sakar olan koleksiyonculara dağılıyor. Bunun nedeni, Map.forEach()anahtar ve değeri ayrı parametreler olarak verirken akış yalnızca bir değere sahiptir - ve anahtarın veya harita girişinin bu değer olarak kullanılıp kullanılmayacağını seçmeniz gerekir. Eksi tarafta, diğer yaklaşımların zengin, akışlı iyiliği yoktur. :-)


11
Function.identity()havalı görünebilir, ancak ilk çözüm her giriş için bir harita / karma arama gerektirdiğinden, diğer tüm çözümlerin bulunmadığı için tavsiye etmem.
Holger

13

Bunun gibi genel bir çözüm

public static <X, Y, Z> Map<X, Z> transform(Map<X, Y> input,
        Function<Y, Z> function) {
    return input
            .entrySet()
            .stream()
            .collect(
                    Collectors.toMap((entry) -> entry.getKey(),
                            (entry) -> function.apply(entry.getValue())));
}

Misal

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<String, Integer> output = transform(input,
            (val) -> Integer.parseInt(val));

Jenerikler kullanarak güzel bir yaklaşım. Bence biraz geliştirilebilir - cevabımı görün.
Stuart Marks

13

Guava'nın işlevi Maps.transformValuesaradığınız şeydir ve lambda ifadeleriyle güzel çalışır:

Maps.transformValues(originalMap, val -> ...)

Ben bu yaklaşımı seviyorum, ama bir java.util.Function geçmek için dikkatli olun. Com.google.common.base.Function işlevini beklediğinden, Eclipse yararsız bir hata veriyor - İşlev, İşlev için geçerli değil diyor ki, kafa karıştırıcı olabilir: "transformValues ​​yöntemi (Harita <K, V1>, İşlev <? Super V1 , V2>) türündeki haritalar bağımsız değişkenler için geçerli değildir (Harita <Foo, Bar>, İşlev <Bar, Baz>) "
mskfisher

Bir geçmeniz gerekiyorsa java.util.Function, iki seçeneğiniz vardır. 1. Java tipi çıkarımın bunu çözmesine izin vermek için lambda kullanarak sorunu önleyin. 2. Tür çıkarımının bulabileceği yeni bir lambda üretmek için javaFunction :: Apply gibi bir yöntem referansı kullanın.
Joe


5

Benim StreamEx standart akışı API geliştirir kütüphane bir sağlar EntryStreamhangi haritalar dönüştürmek için daha uygundur sınıfını:

Map<String, Integer> output = EntryStream.of(input).mapValues(Integer::valueOf).toMap();

4

Her zaman öğrenme amacıyla var olan bir alternatif, özel toplayıcınızı Collector.of () aracılığıyla oluşturmaktır, ancak toMap () JDK toplayıcısı burada özlüdür (+1 burada ).

Map<String,Integer> newMap = givenMap.
                entrySet().
                stream().collect(Collector.of
               ( ()-> new HashMap<String,Integer>(),
                       (mutableMap,entryItem)-> mutableMap.put(entryItem.getKey(),Integer.parseInt(entryItem.getValue())),
                       (map1,map2)->{ map1.putAll(map2); return map1;}
               ));

Bu özel toplayıcı ile bir temel olarak başladım ve en azından stream () yerine parallelStream () kullanıldığında, binaryOperator'ın daha benzer bir şeye yeniden yazılması gerektiğini map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1veya değerlerin azalırken kaybolacağını eklemek istedim .
user691154

3

Üçüncü taraf kitaplıklarını kullanmanın bir sakıncası yoksa, cyclops-tepki tepkisi, Harita da dahil olmak üzere tüm JDK Koleksiyonu türleri için uzantılara sahiptir . Haritayı 'harita' operatörünü kullanarak doğrudan dönüştürebiliriz (varsayılan olarak harita haritadaki değerlere göre hareket eder).

   MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                                .map(Integer::parseInt);

bimap, anahtarları ve değerleri aynı anda dönüştürmek için kullanılabilir

  MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                               .bimap(this::newKey,Integer::parseInt);

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.