ฉันจะใช้ฟังก์ชัน computeIfAbsent ใหม่ได้อย่างไร


115

ฉันต้องการใช้Map.computeIfAbsentเป็นอย่างมากแต่มันนานเกินไปแล้วตั้งแต่ lambdas อยู่ในระดับปริญญาตรี

เกือบจะโดยตรงจากเอกสาร: มันเป็นตัวอย่างของวิธีการทำสิ่งต่างๆแบบเก่า:

Map<String, Boolean> whoLetDogsOut = new ConcurrentHashMap<>();
String key = "snoop";
if (whoLetDogsOut.get(key) == null) {
  Boolean isLetOut = tryToLetOut(key);
  if (isLetOut != null)
    map.putIfAbsent(key, isLetOut);
}

และวิธีใหม่:

map.computeIfAbsent(key, k -> new Value(f(k)));

แต่ในตัวอย่างของพวกเขาฉันคิดว่าฉันไม่ค่อย "เข้าใจ" ฉันจะแปลงรหัสเพื่อใช้วิธีแลมด้าใหม่ในการแสดงสิ่งนี้ได้อย่างไร


ฉันไม่แน่ใจว่าคุณไม่เข้าใจอะไรจากตัวอย่างที่นั่น?
Louis Wasserman

2
"k" คืออะไร? เป็นตัวแปรที่ถูกกำหนดหรือไม่? "ค่าใหม่" เป็นอย่างไรบ้าง - สิ่งนั้นมาจาก java 8 หรือเป็นตัวแทนของวัตถุที่ฉันต้องการกำหนดหรือแทนที่ whoLetDogsOut.computeIfAbsent (key, k -> new Boolean (tryToLetOut (k))) ไม่ได้คอมไพล์ฉันเลยขาดอะไรไป ...
Benjamin H

สิ่งที่ไม่รวบรวม? เกิดข้อผิดพลาดอะไร
axtavt

Temp.java:26: ข้อผิดพลาด: เริ่มต้นที่ผิดกฎหมายของนิพจน์ whoLetDogsOut.computeIfAbsent (คีย์ k -> บูลีนใหม่ (tryToLetOut (k))); (ชี้ไปที่ ">")
Benjamin H

รวบรวมได้ดีสำหรับฉัน ตรวจสอบให้แน่ใจว่าคุณใช้คอมไพเลอร์ Java 8 จริงๆ คุณสมบัติอื่น ๆ ของ Java 8 ทำงานได้หรือไม่
axtavt

คำตอบ:


96

สมมติว่าคุณมีรหัสต่อไปนี้:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Test {
    public static void main(String[] s) {
        Map<String, Boolean> whoLetDogsOut = new ConcurrentHashMap<>();
        whoLetDogsOut.computeIfAbsent("snoop", k -> f(k));
        whoLetDogsOut.computeIfAbsent("snoop", k -> f(k));
    }
    static boolean f(String s) {
        System.out.println("creating a value for \""+s+'"');
        return s.isEmpty();
    }
}

จากนั้นคุณจะเห็นข้อความcreating a value for "snoop"หนึ่งครั้งเหมือนกับการเรียกครั้งที่สองซึ่งcomputeIfAbsentมีค่าสำหรับคีย์นั้นอยู่แล้ว kในการแสดงออกของแลมบ์ดาk -> f(k)เป็นเพียง placeolder (พารามิเตอร์) สำหรับคีย์ซึ่งแผนที่จะส่งผ่านไปแลมบ์ดาของคุณสำหรับการคำนวณค่า ดังนั้นในตัวอย่างคีย์จะถูกส่งไปยังการเรียกใช้ฟังก์ชัน

หรือคุณสามารถเขียน: whoLetDogsOut.computeIfAbsent("snoop", k -> k.isEmpty());เพื่อให้ได้ผลลัพธ์เดียวกันโดยไม่ต้องใช้วิธีตัวช่วย (แต่คุณจะไม่เห็นผลลัพธ์การดีบัก) และง่ายกว่านั้นเนื่องจากเป็นการมอบอำนาจอย่างง่ายให้กับวิธีการที่มีอยู่ที่คุณสามารถเขียนได้: การwhoLetDogsOut.computeIfAbsent("snoop", String::isEmpty);มอบหมายนี้ไม่จำเป็นต้องเขียนพารามิเตอร์ใด ๆ

เพื่อให้ใกล้เคียงกับตัวอย่างในคำถามของคุณมากขึ้นคุณสามารถเขียนเป็นwhoLetDogsOut.computeIfAbsent("snoop", key -> tryToLetOut(key));(ไม่สำคัญว่าคุณจะตั้งชื่อพารามิเตอร์kหรือkey) หรือเขียนwhoLetDogsOut.computeIfAbsent("snoop", MyClass::tryToLetOut);ว่าtryToLetOutเป็นstaticหรือwhoLetDogsOut.computeIfAbsent("snoop", this::tryToLetOut);ถ้าtryToLetOutเป็นวิธีการอินสแตนซ์


114

เมื่อเร็ว ๆ นี้ฉันก็เล่นด้วยวิธีนี้เหมือนกัน ฉันเขียนอัลกอริทึมที่บันทึกไว้เพื่อคำนวณตัวเลขฟีโบนักชีซึ่งสามารถใช้เป็นภาพประกอบอื่นเกี่ยวกับวิธีใช้วิธีนี้ได้

เราสามารถเริ่มต้นด้วยการกำหนดแผนที่และใส่ค่าลงในกรณีฐาน ได้แก่fibonnaci(0)และfibonacci(1):

private static Map<Integer,Long> memo = new HashMap<>();
static {
   memo.put(0,0L); //fibonacci(0)
   memo.put(1,1L); //fibonacci(1)
}

และสำหรับขั้นตอนอุปนัยสิ่งที่เราต้องทำคือกำหนดฟังก์ชัน Fibonacci ของเราใหม่ดังนี้:

public static long fibonacci(int x) {
   return memo.computeIfAbsent(x, n -> fibonacci(n-2) + fibonacci(n-1));
}

ดังที่คุณเห็นวิธีcomputeIfAbsentนี้จะใช้นิพจน์แลมบ์ดาที่ให้มาเพื่อคำนวณหมายเลขฟีโบนักชีเมื่อไม่มีตัวเลขอยู่ในแผนที่ สิ่งนี้แสดงถึงการปรับปรุงที่สำคัญเหนืออัลกอริทึมการเรียกซ้ำแบบต้นไม้แบบดั้งเดิม


18
การแปลงบรรทัดเดียวเป็นการเขียนโปรแกรมแบบไดนามิกที่ดี เนียนมาก.
Benjamin H

3
คุณอาจได้รับการโทรซ้ำน้อยลงหากคุณมีการโทร (n-2) ก่อน?
Thorbjørn Ravn Andersen

10
คุณควรระมัดระวังให้มากขึ้นเมื่อใช้ computeIfAbsent แบบเรียกซ้ำ ดูรายละเอียดเพิ่มเติมได้ที่stackoverflow.com/questions/28840047/…
Ajit Kumar

12
รหัสนี้ส่งผลให้HashMapภายในของเสียหายเช่นเดียวกับในbugs.openjdk.java.net/browse/JDK-8172951และจะล้มเหลวConcurrentModificationExceptionใน Java 9 ( bugs.openjdk.java.net/browse/JDK-8071667 )
Piotr Findeisen

23
เอกสารกล่าวอย่างแท้จริงว่าฟังก์ชันการทำแผนที่ไม่ควรแก้ไขแผนที่นี้ในระหว่างการคำนวณดังนั้นคำตอบนี้จึงผิดอย่างชัดเจน
fps

41

ตัวอย่างอื่น. เมื่อสร้างแผนที่ที่ซับซ้อนเมธอด computeIfAbsent () จะแทนที่เมธอด get () ของแผนที่ ผ่านการเชื่อมโยงการเรียก computeIfAbsent () เข้าด้วยกันคอนเทนเนอร์ที่ขาดหายไปจะถูกสร้างขึ้นทันทีโดยใช้นิพจน์แลมบ์ดาที่ให้มา:

  // Stores regional movie ratings
  Map<String, Map<Integer, Set<String>>> regionalMovieRatings = new TreeMap<>();

  // This will throw NullPointerException!
  regionalMovieRatings.get("New York").get(5).add("Boyhood");

  // This will work
  regionalMovieRatings
    .computeIfAbsent("New York", region -> new TreeMap<>())
    .computeIfAbsent(5, rating -> new TreeSet<>())
    .add("Boyhood");

31

หลายแผนที่

นี้จะเป็นประโยชน์จริงๆถ้าคุณต้องการที่จะสร้างMultimap โดยไม่ต้อง resorting กับGoogle ฝรั่งMultiMapห้องสมุดสำหรับการดำเนินงานของ

ตัวอย่างเช่นสมมติว่าคุณต้องการจัดเก็บรายชื่อนักเรียนที่ลงทะเบียนเรียนวิชาหนึ่ง

วิธีแก้ไขปกติสำหรับสิ่งนี้โดยใช้ไลบรารี JDK คือ:

Map<String,List<String>> studentListSubjectWise = new TreeMap<>();
List<String>lis = studentListSubjectWise.get("a");
if(lis == null) {
    lis = new ArrayList<>();
}
lis.add("John");

//continue....

Mutltimapเพราะมันมีรหัสสำเร็จรูปบางคนมักจะใช้ฝรั่ง

การใช้ Map.computeIfAbsent เราสามารถเขียนเป็นบรรทัดเดียวโดยไม่ต้องใช้ Multimap guava ได้ดังนี้

studentListSubjectWise.computeIfAbsent("a", (x -> new ArrayList<>())).add("John");

Stuart Marks & Brian Goetz พูดคุยได้ดีเกี่ยวกับเรื่องนี้ https://www.youtube.com/watch?v=9uTVXxJjuco


อีกวิธีหนึ่งในการสร้างมัลติแมปใน Java 8 (และกระชับมากขึ้น) ก็คือการทำเช่นstudentListSubjectWise.stream().collect(Collectors.GroupingBy(subj::getSubjName, Collectors.toList());นี้จะสร้างแผนที่หลายประเภทMap<T,List<T>ใน JDK ซึ่งมีความกระชับมากขึ้นเท่านั้น
Zombies
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.