Bagaimana cara menggunakan fungsi computeIfAbsent yang baru?

115

Saya sangat ingin menggunakan Map.computeIfAbsent tetapi sudah terlalu lama sejak lambdas di undergrad.

Hampir langsung dari dokumen: ini memberikan contoh cara lama untuk melakukan sesuatu:

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

Dan cara baru:

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

Tetapi dalam contoh mereka, saya pikir saya tidak cukup "mengerti". Bagaimana saya mengubah kode untuk menggunakan cara lambda baru untuk mengekspresikannya?

Benjamin H.
sumber
Saya tidak yakin apa yang Anda tidak mengerti dari contoh di sana?
Louis Wasserman
2
Apa itu "k"? Apakah variabel sedang didefinisikan? Bagaimana dengan "Nilai baru" - apakah itu sesuatu dari java 8, atau mewakili objek yang perlu saya definisikan atau timpa? whoLetDogsOut.computeIfAbsent (key, k -> new Boolean (tryToLetOut (k))) tidak dapat dikompilasi, jadi saya kehilangan sesuatu ...
Benjamin H
Apa sebenarnya yang tidak bisa dikompilasi? Kesalahan apa yang dihasilkannya?
axtavt
Temp.java:26: kesalahan: awal ilegal ekspresi whoLetDogsOut.computeIfAbsent (key, k -> new Boolean (tryToLetOut (k))); (menunjuk ke ">")
Benjamin H
Menyusun baik-baik saja untuk saya. Pastikan Anda benar-benar menggunakan compiler Java 8. Apakah fitur Java 8 lainnya berfungsi?
axtavt

Jawaban:

96

Misalkan Anda memiliki kode berikut:

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

Kemudian Anda akan melihat pesan creating a value for "snoop"tepat satu kali karena pada pemanggilan kedua computeIfAbsentsudah ada nilai untuk kunci itu. Dalam kekspresi lambdak -> f(k) hanya placeolder (parameter) untuk kunci yang peta akan lolos ke lambda Anda untuk menghitung nilai. Jadi dalam contoh, kunci dilewatkan ke pemanggilan fungsi.

Atau Anda dapat menulis: whoLetDogsOut.computeIfAbsent("snoop", k -> k.isEmpty());untuk mencapai hasil yang sama tanpa metode helper (tetapi Anda tidak akan melihat keluaran debugging saat itu). Dan bahkan lebih sederhana, karena ini adalah pendelegasian sederhana ke metode yang sudah ada, Anda dapat menulis: whoLetDogsOut.computeIfAbsent("snoop", String::isEmpty);Delegasi ini tidak memerlukan parameter apa pun untuk ditulis.

Untuk lebih mendekati contoh dalam pertanyaan Anda, Anda dapat menuliskannya sebagai whoLetDogsOut.computeIfAbsent("snoop", key -> tryToLetOut(key));(tidak masalah apakah Anda menamai parameter katau key). Atau menulis sebagai whoLetDogsOut.computeIfAbsent("snoop", MyClass::tryToLetOut);jika tryToLetOutadalah staticatau whoLetDogsOut.computeIfAbsent("snoop", this::tryToLetOut);jika tryToLetOutmerupakan metode contoh.

Holger
sumber
114

Baru-baru ini saya juga bermain dengan metode ini. Saya menulis algoritma memoized untuk menghitung bilangan Fibonacci yang dapat berfungsi sebagai ilustrasi lain tentang cara menggunakan metode ini.

Kita bisa mulai dengan mendefinisikan peta dan memasukkan nilai-nilai di dalamnya untuk kasus dasar, yaitu, fibonnaci(0)dan fibonacci(1):

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

Dan untuk langkah induktif yang harus kita lakukan adalah mendefinisikan kembali fungsi Fibonacci kita sebagai berikut:

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

Seperti yang Anda lihat, metode ini computeIfAbsentakan menggunakan ekspresi lambda yang disediakan untuk menghitung angka Fibonacci saat angka tersebut tidak ada di peta. Ini menunjukkan peningkatan yang signifikan atas algoritme rekursif pohon tradisional.

Edwin Dalorzo
sumber
18
Konversi satu baris yang bagus ke pemrograman dinamis. Sangat apik.
Benjamin H
3
Anda mungkin mendapatkan lebih sedikit panggilan rekursif jika Anda memiliki panggilan (n-2) terlebih dahulu?
Thorbjørn Ravn Andersen
10
Anda harus lebih berhati-hati saat menggunakan computeIfAbsent secara rekursif. Untuk detail lebih lanjut silakan periksa stackoverflow.com/questions/28840047/…
Ajit Kumar
12
Kode ini menyebabkan HashMapinternal rusak, seperti di bugs.openjdk.java.net/browse/JDK-8172951 dan akan gagal ConcurrentModificationExceptiondi Java 9 ( bugs.openjdk.java.net/browse/JDK-8071667 )
Piotr Findeisen
23
Dokumen secara harfiah mengatakan bahwa fungsi pemetaan tidak boleh mengubah peta ini selama komputasi , jadi jawaban ini jelas salah.
fps
41

Contoh lain. Saat membuat peta peta yang kompleks, metode computeIfAbsent () adalah pengganti metode get () peta. Melalui rangkaian panggilan computeIfAbsent () bersama-sama, container yang hilang dibuat secara on-the-fly oleh ekspresi lambda yang disediakan:

  // 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");
hexabc
sumber
31

multi-peta

Ini sangat membantu jika Anda ingin membuat multimap tanpa menggunakan pustaka Google Guava untuk implementasinyaMultiMap .

Misalnya, Anda ingin menyimpan daftar siswa yang mendaftar untuk mata pelajaran tertentu.

Solusi normal untuk ini menggunakan perpustakaan JDK adalah:

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

//continue....

Karena memiliki beberapa kode boilerplate, orang cenderung menggunakan Guava Mutltimap.

Dengan menggunakan Map.computeIfAbsent, kita dapat menulis dalam satu baris Multimap tanpa jambu sebagai berikut.

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

Stuart Marks & Brian Goetz melakukan pembicaraan yang baik tentang ini https://www.youtube.com/watch?v=9uTVXxJjuco

nantitv
sumber
Cara lain untuk membuat multimap di Java 8 (dan lebih ringkas) adalah dengan hanya melakukan studentListSubjectWise.stream().collect(Collectors.GroupingBy(subj::getSubjName, Collectors.toList());Ini menghasilkan multi-map tipe Map<T,List<T>di JDK hanya lebih singkat imho.
Zombies