Java8: HashMap <X, Y> ถึง HashMap <X, Z> โดยใช้ Stream / Map-Reduce / Collector


209

ฉันรู้วิธีการ "แปลง" Java ง่ายๆListจากY-> Zคือ:

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

ตอนนี้ฉันต้องการทำแบบเดียวกันกับแผนที่กล่าวคือ:

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

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

การแก้ปัญหาไม่ควรจะ จำกัด->String IntegerเหมือนในListตัวอย่างด้านบนฉันต้องการเรียกวิธีการใด ๆ (หรือตัวสร้าง)

คำตอบ:


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

มันไม่ดีเท่ารหัสรายการ คุณไม่สามารถสร้างMap.Entrys ใหม่ในmap()สายเพื่อให้งานถูกผสมลงในการcollect()โทร


59
คุณสามารถแทนที่ด้วยe -> e.getKey() Map.Entry::getKeyแต่นั่นเป็นเรื่องของรสนิยม / รูปแบบการเขียนโปรแกรม
Holger

5
อันที่จริงมันเป็นเรื่องของการแสดงการแนะนำของคุณจะดีกว่าสไตล์ของแลมบ์ดาเล็กน้อย
Jon Burgin

36

ต่อไปนี้เป็นคำตอบบางส่วนของคำตอบของ Sotirios Delimanolisซึ่งค่อนข้างดีที่จะเริ่มต้นด้วย (+1) พิจารณาสิ่งต่อไปนี้:

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

สองสามคะแนนที่นี่ ครั้งแรกคือการใช้สัญลักษณ์แทนใน generics; ทำให้ฟังก์ชั่นค่อนข้างยืดหยุ่น คุณจำเป็นต้องใช้สัญลักษณ์แทนหากคุณต้องการให้แผนที่เอาท์พุทมีคีย์ที่เป็นซูเปอร์คลาสของคีย์แผนที่ของอินพุต:

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

(นอกจากนี้ยังมีตัวอย่างสำหรับค่าของแผนที่ แต่มีการวางแผนจริง ๆ และฉันยอมรับว่าการใช้สัญลักษณ์ตัวแทนที่มีขอบเขตสำหรับ Y จะช่วยในกรณีที่เป็นขอบเท่านั้น)

จุดที่สองคือแทนที่จะวิ่งกระแสมากกว่าแผนที่การป้อนข้อมูลของฉันวิ่งมันมากกว่าentrySet keySetฉันคิดว่านี่เป็นโค้ดที่สะอาดขึ้นเล็กน้อยโดยที่ไม่ต้องดึงค่าออกจากแผนที่แทนที่จะมาจากรายการแผนที่ บังเอิญตอนแรกฉันkey -> keyเป็นอาร์กิวเมนต์แรกtoMap()และล้มเหลวด้วยข้อผิดพลาดการอนุมานประเภทด้วยเหตุผลบางอย่าง เปลี่ยนเป็น(X key) -> keyทำงานเหมือนFunction.identity()เดิม

ยังมีรูปแบบอื่นดังนี้:

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

สิ่งนี้ใช้Map.forEach()แทนสตรีม ฉันคิดว่ามันง่ายกว่านี้อีกเพราะว่ามันแจกจ่ายให้กับนักสะสมซึ่งค่อนข้างงุ่มง่ามที่จะใช้กับแผนที่ เหตุผลก็คือMap.forEach()ให้คีย์และค่าเป็นพารามิเตอร์แยกขณะที่กระแสมีเพียงค่าเดียว - และคุณต้องเลือกว่าจะใช้คีย์หรือรายการแผนที่เป็นค่านั้น ในด้านลบนี้ขาดความอุดมสมบูรณ์และความดีงามของวิธีการอื่น ๆ :-)


11
Function.identity()อาจดูเท่ห์ แต่เนื่องจากวิธีแก้ปัญหาแรกนั้นต้องใช้การค้นหาแผนที่ / แฮชสำหรับทุกรายการขณะที่โซลูชันอื่นไม่ทำฉันจึงไม่แนะนำ
Holger

13

วิธีแก้ปัญหาทั่วไปเช่นนั้น

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

ตัวอย่าง

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

วิธีการที่ดีในการใช้ยาชื่อสามัญ ฉันคิดว่ามันสามารถปรับปรุงได้บ้าง - ดูคำตอบของฉัน
Stuart Marks

13

ฟังก์ชั่นของ Guava Maps.transformValuesคือสิ่งที่คุณกำลังมองหาและมันทำงานได้ดีกับการแสดงออกแลมบ์ดา:

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

ฉันชอบวิธีนี้ แต่ระวังอย่าส่ง java.util.Function เนื่องจากคาดว่า com.google.common.base.Function Eclipse จะให้ข้อผิดพลาดที่ไม่ช่วยเหลือ - มันบอกว่า Function ไม่สามารถใช้งานได้กับฟังก์ชั่นซึ่งอาจทำให้เกิดความสับสน: "วิธีการเปลี่ยนค่า (แผนที่ <K, V1>, ฟังก์ชั่น <? super V1 , V2>) ในประเภทแผนที่ไม่สามารถใช้ได้กับข้อโต้แย้ง (แผนที่ <Foo, บาร์>, ฟังก์ชั่น <บาร์, Baz>) "
mskfisher

หากคุณต้องผ่าน a java.util.Functionคุณมีสองตัวเลือก 1. หลีกเลี่ยงปัญหานี้โดยใช้แลมบ์ดาเพื่อให้การอนุมานประเภท Java คิดออก 2. ใช้การอ้างอิงเมธอดเช่น javaFunction :: Apply เพื่อสร้างแลมบ์ดาใหม่ที่การอนุมานประเภทสามารถเข้าใจได้
Joe

10

จำเป็นต้องใช้งานได้จริง 100% หรือไม่? ถ้าไม่เป็นเช่นนั้นสิ่งนี้จะสั้นเพียงใด:

Map<String, Integer> output = new HashMap<>();
input.forEach((k, v) -> output.put(k, Integer.valueOf(v));

( หากคุณสามารถอยู่กับความอัปยศและความผิดของการรวมสตรีมกับผลข้างเคียง )


5

ห้องสมุดStreamExของฉันซึ่งปรับปรุง API สตรีมมาตรฐานให้EntryStreamคลาสที่เหมาะสมกับการแปลงแผนที่ดีกว่า:

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

4

ทางเลือกที่มีอยู่เพื่อการเรียนรู้เสมอคือการสร้างตัวรวบรวมที่กำหนดเองของคุณผ่าน Collector.of () แม้ว่า toMap () ตัวรวบรวม JDK ที่นี่จะรวบรัด (+1 ที่นี่ )

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

ฉันเริ่มต้นด้วยตัวสะสมที่กำหนดเองนี้เป็นฐานและต้องการเพิ่มอย่างน้อยเมื่อใช้ parallelStream () แทนที่จะเป็น stream () binaryOperator ควรถูกเขียนใหม่เป็นสิ่งที่คล้ายกับmap2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1หรือค่าจะหายไปเมื่อลดขนาดลง
user691154

3

หากคุณไม่ทราบใช้ห้องสมุดของบุคคลที่ 3 ของฉันไซคลอปส์ทำปฏิกิริยา lib มีส่วนขยายสำหรับทุกJDK เก็บประเภทรวมทั้งแผนที่ เราสามารถเปลี่ยนแผนที่ได้โดยตรงโดยใช้ตัวดำเนินการ 'map' (โดยแผนที่เริ่มต้นจะทำหน้าที่เกี่ยวกับค่าต่างๆในแผนที่)

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

bimap สามารถใช้ในการแปลงคีย์และค่าในเวลาเดียวกัน

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

0

วิธีแก้ปัญหาที่ง่ายและเปิดเผยจะเป็น:

yourMutableMap.replaceAll ((คีย์, val) -> return_value_of_bi_your_function); nb ระวังการปรับเปลี่ยนสถานะแผนที่ของคุณ ดังนั้นนี่อาจไม่ใช่สิ่งที่คุณต้องการ

เชียร์ไปที่: http://www.deadcoderising.com/2017-02-14-java-8-declarative-ways-of-modifying-a-map-using-compute-merge-and-replace/

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.