Java8: HashMap <X, Y> ke HashMap <X, Z> menggunakan Stream / Map-Reduce / Collector

210

Saya tahu cara "mengubah" Java sederhana Listdari Y-> Z, yaitu:

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

Sekarang pada dasarnya saya ingin melakukan hal yang sama dengan Peta, yaitu:

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

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

Solusinya tidak terbatas pada String-> Integer. Sama seperti pada Listcontoh di atas, saya ingin memanggil metode apa pun (atau konstruktor).

Benjamin M
sumber

Jawaban:

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

Ini tidak sebagus kode daftar. Anda tidak dapat membuat baru Map.Entrydalam map()panggilan sehingga pekerjaan itu dicampur ke dalam collect()panggilan.

John Kugelman
sumber
59
Anda bisa menggantinya e -> e.getKey()dengan Map.Entry::getKey. Tapi itu masalah selera / gaya pemrograman.
Holger
5
Sebenarnya ini adalah masalah kinerja, saran Anda menjadi sedikit lebih unggul dari 'gaya' lambda
Jon Burgin
36

Berikut adalah beberapa variasi jawaban Sotirios Delimanolis , yang cukup baik untuk memulai dengan (+1). Pertimbangkan yang berikut ini:

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

Beberapa poin di sini. Pertama adalah penggunaan wildcard dalam obat generik; ini membuat fungsinya agak lebih fleksibel. Wildcard akan diperlukan jika, misalnya, Anda ingin peta keluaran memiliki kunci yang merupakan superclass dari kunci peta input:

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

(Ada juga contoh untuk nilai-nilai peta, tetapi ini benar-benar dibuat-buat, dan saya akui bahwa memiliki wildcard terbatas untuk Y hanya membantu dalam kasus tepi.)

Poin kedua adalah bahwa alih-alih menjalankan streaming di atas peta input entrySet, saya malah menjalankannya di atas keySet. Ini membuat kode sedikit lebih bersih, saya pikir, dengan biaya harus mengambil nilai dari peta alih-alih dari entri peta. Kebetulan, saya awalnya memiliki key -> keyargumen pertama toMap()dan ini gagal dengan kesalahan inferensi jenis karena beberapa alasan. Mengubahnya (X key) -> keyberfungsi, seperti yang terjadi Function.identity().

Variasi lain adalah sebagai berikut:

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

Ini menggunakan Map.forEach()bukannya stream. Ini bahkan lebih sederhana, saya pikir, karena itu dibagikan kepada kolektor, yang agak canggung untuk digunakan dengan peta. Alasannya adalah bahwa Map.forEach()memberikan kunci dan nilai sebagai parameter terpisah, sedangkan aliran hanya memiliki satu nilai - dan Anda harus memilih apakah akan menggunakan kunci atau entri peta sebagai nilai itu. Di sisi minusnya, ini tidak memiliki kekayaan, aliran kebaikan dari pendekatan lain. :-)

Stuart Marks
sumber
11
Function.identity()mungkin terlihat keren tetapi karena solusi pertama membutuhkan pencarian peta / hash untuk setiap entri sedangkan semua solusi lainnya tidak, saya tidak akan merekomendasikan hal ini.
Holger
13

Solusi generik seperti itu

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

Contoh

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));
Sotirios Delimanolis
sumber
Pendekatan yang bagus menggunakan obat generik. Saya pikir itu dapat ditingkatkan sedikit - lihat jawaban saya.
Stuart Marks
13

Fungsi Guava Maps.transformValuesadalah apa yang Anda cari, dan berfungsi baik dengan ekspresi lambda:

Maps.transformValues(originalMap, val -> ...)
Alex Krauss
sumber
Saya suka pendekatan ini, tapi hati-hati jangan sampai melewatkannya java.util.Fungsi. Karena mengharapkan com.google.common.base.Fungsi, Eclipse memberikan kesalahan yang tidak membantu - ia mengatakan Fungsi tidak berlaku untuk Fungsi, yang dapat membingungkan: "Metode mentransformasikan Nilai (Peta <K, V1>, Fungsi <? Super V1 , V2>) dalam tipe Maps tidak berlaku untuk argumen (Peta <Foo, Bar>, Function <Bar, Baz>) "
mskfisher
Jika Anda harus lulus java.util.Function, Anda memiliki dua opsi. 1. Hindari masalah dengan menggunakan lambda untuk membiarkan inferensi tipe Java mengatasinya. 2. Gunakan referensi metode seperti javaFunction :: apply untuk menghasilkan lambda baru yang dapat ditemukan oleh tipe inferensi.
Joe
5

Pustaka StreamEx saya yang meningkatkan API aliran standar menyediakan EntryStreamkelas yang lebih sesuai untuk mengubah peta:

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

Alternatif yang selalu ada untuk tujuan pembelajaran adalah membangun kolektor khusus Anda melalui Collector.of () toMap () JDK kolektor di sini singkat (+1 di sini ).

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;}
               ));
Ira
sumber
Saya mulai dengan kolektor khusus ini sebagai basis dan ingin menambahkan itu, setidaknya ketika menggunakan parallelStream () alih-alih stream (), binaryOperator harus ditulis ulang ke sesuatu yang lebih mirip map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1atau nilai-nilai akan hilang ketika dikurangi.
user691154
3

Jika Anda tidak keberatan menggunakan perpustakaan pihak ketiga, lib -siklop-reaksi saya memiliki ekstensi untuk semua jenis Koleksi JDK , termasuk Peta . Kami hanya dapat mengubah peta secara langsung menggunakan operator 'peta' (secara default peta bertindak berdasarkan nilai-nilai di peta).

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

bimap dapat digunakan untuk mengubah kunci dan nilai pada saat yang sama

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