Java 8 telah hadir, saatnya mempelajari sesuatu yang baru. Java 7 dan Java 6 hanyalah versi yang sedikit dimodifikasi, tetapi Java 8 akan mengalami peningkatan besar. Mungkin Java 8 terlalu besar? Hari ini saya akan memberikan penjelasan menyeluruh tentang abstraksi baru CompletableFuture di JDK 8. Seperti kita ketahui bersama, Java 8 akan dirilis dalam waktu kurang dari setahun, jadi artikel ini didasarkan pada JDK 8 build 88 dengan dukungan lambda. CompletableFuture extends Future menyediakan metode, operator unary dan mempromosikan asinkronisitas dan model pemrograman berbasis peristiwa yang tidak berhenti pada versi Java yang lebih lama. Jika Anda membuka JavaDoc dari CompletableFuture Anda akan terkejut. Ada sekitar lima puluh metode (!), dan beberapa di antaranya sangat menarik dan sulit dipahami, misalnya:
Salin kode sebagai berikut: public <U,V> CompletableFuture<V> kemudianCombineAsync(
Masa Depan yang Dapat Diselesaikan<?
BiFungsi<?super T,?super U,?meluas V>fn,
pelaksana pelaksana)
Jangan khawatir, teruslah membaca. CompletableFuture mengumpulkan semua karakteristik ListenableFuture di Guava dan SettableFuture. Selain itu, ekspresi lambda bawaan membawanya lebih dekat ke masa depan Scala/Akka. Ini mungkin terdengar terlalu bagus untuk menjadi kenyataan, tapi baca terus. CompletableFuture memiliki dua aspek utama yang lebih unggul dari panggilan balik/konversi asinkron Future di ol, yang memungkinkan nilai CompletableFuture disetel dari thread mana pun kapan saja.
1. Ekstrak dan ubah nilai paket
Seringkali masa depan mewakili kode yang berjalan di thread lain, tetapi hal ini tidak selalu terjadi. Terkadang Anda ingin membuat Masa Depan untuk menunjukkan bahwa Anda mengetahui apa yang akan terjadi, seperti kedatangan pesan JMS. Jadi, Anda memiliki Masa Depan tetapi tidak ada potensi pekerjaan asinkron di masa depan. Anda hanya ingin selesai (diselesaikan) ketika pesan JMS mendatang tiba, yang didorong oleh suatu peristiwa. Dalam hal ini, Anda cukup membuat CompletableFuture untuk kembali ke klien Anda, dan complete() akan membuka kunci semua klien yang menunggu Masa Depan selama menurut Anda hasil Anda tersedia.
Pertama, Anda cukup membuat CompletableFuture baru dan memberikannya kepada klien Anda:
Salin kode sebagai berikut: public CompletableFuture<String> ask() {
final CompletableFuture<String> masa depan = new CompletableFuture<>();
//...
kembali di masa depan;
}
Perhatikan bahwa masa depan ini tidak ada hubungannya dengan Callable, tidak ada kumpulan thread, dan tidak berfungsi secara asinkron. Jika kode klien sekarang memanggil Ask().get() maka kode tersebut akan diblokir selamanya. Jika register menyelesaikan panggilan balik, register tersebut tidak akan pernah berpengaruh. Jadi apa kuncinya? Sekarang Anda dapat mengatakan:
Salin kode sebagai berikut: future.complete("42")
...Saat ini, semua klien Future.get() akan mendapatkan hasil string, dan akan berlaku segera setelah panggilan balik selesai. Ini sangat nyaman ketika Anda ingin merepresentasikan tugas Masa Depan, dan tidak perlu menghitung tugas dari beberapa thread eksekusi. CompletableFuture.complete() hanya dapat dipanggil satu kali, panggilan berikutnya akan diabaikan. Namun ada juga pintu belakang bernama CompletableFuture.obtrudeValue(...) yang menimpa nilai sebelumnya dari Future baru, jadi harap gunakan dengan hati-hati.
Terkadang Anda ingin melihat apa yang terjadi ketika sinyal gagal, seperti yang Anda ketahui bahwa objek Future dapat menangani hasil atau pengecualian yang dikandungnya. Jika Anda ingin meneruskan beberapa pengecualian lebih lanjut, Anda dapat menggunakan CompletableFuture.completeExceptionally(ex) (atau menggunakan metode yang lebih canggih seperti obtrudeException(ex) untuk mengganti pengecualian sebelumnya). completeExceptionally() juga membuka kunci semua klien yang menunggu, tetapi kali ini memberikan pengecualian dari get(). Berbicara tentang get(), ada juga metode CompletableFuture.join() dengan sedikit perubahan dalam penanganan kesalahan. Tapi secara keseluruhan, semuanya sama. Terakhir, ada metode CompletableFuture.getNow(valueIfAbsent) yang tidak memblokir tetapi akan mengembalikan nilai default jika Masa Depan belum selesai, sehingga sangat berguna ketika membangun sistem yang kuat di mana kita tidak ingin menunggu terlalu lama.
Metode statis terakhir adalah menggunakan CompleteFuture(value) untuk mengembalikan objek Future yang telah selesai, yang mungkin sangat berguna saat menguji atau menulis beberapa lapisan adaptor.
2. Buat dan dapatkan CompletableFuture
Oke, jadi membuat CompletableFuture secara manual adalah satu-satunya pilihan kita? tidak pasti. Sama seperti Futures biasa, kita dapat mengaitkan tugas-tugas yang ada, dan CompletableFuture menggunakan metode pabrik:
Copy kode kodenya sebagai berikut:
static <U> CompletableFuture<U> supplyAsync(Pemasok<U> pemasok);
static <U> CompletableFuture<U> supplyAsync(Pemasok<U> pemasok, pelaksana Pelaksana);
static CompletableFuture<Void> runAsync(Runnable runnable);
static CompletableFuture<Void> runAsync(Runnable yang dapat dijalankan, eksekutor pelaksana);
Metode tanpa parameter Executor diakhiri dengan...Async dan akan menggunakan ForkJoinPool.commonPool() (global, kumpulan umum yang diperkenalkan di JDK8), yang berlaku untuk sebagian besar metode di kelas CompletableFuture. runAsync() mudah dimengerti, perhatikan bahwa ia memerlukan Runnable, sehingga ia mengembalikan CompletableFuture<Void> karena Runnable tidak mengembalikan nilai. Jika Anda perlu menangani operasi asinkron dan mengembalikan hasil, gunakan Pemasok<U>:
Copy kode kodenya sebagai berikut:
final CompletableFuture<String> masa depan = CompletableFuture.supplyAsync(Pemasok baru<String>() {
@Mengesampingkan
String publik dapatkan() {
//...berjalan lama...
kembalikan "42";
}
}, pelaksana);
Tapi jangan lupa, ada ekspresi lambda di Java 8!
Copy kode kodenya sebagai berikut:
finalCompletableFuture<String> masa depan = CompletableFuture.supplyAsync(() -> {
//...berjalan lama...
kembalikan "42";
}, pelaksana);
atau:
Copy kode kodenya sebagai berikut:
final CompletableFuture<String> masa depan =
CompletableFuture.supplyAsync(() -> longRunningTask(params), eksekutor);
Meskipun artikel ini bukan tentang lambda, saya cukup sering menggunakan ekspresi lambda.
3. Konversi dan tindakan di CompletableFuture (lalu Terapkan)
Saya bilang CompletableFuture lebih baik dari Future tapi tahukah Anda kenapa? Sederhananya, karena CompletableFuture adalah atom dan faktor. Bukankah apa yang saya katakan bermanfaat? Scala dan JavaScript memungkinkan Anda mendaftarkan panggilan balik asinkron ketika masa depan selesai, dan kita tidak perlu menunggu dan memblokirnya hingga siap. Kita cukup mengatakan: ketika Anda menjalankan fungsi ini, hasilnya akan muncul. Selain itu, kita dapat menumpuk fungsi-fungsi ini, menggabungkan beberapa masa depan bersama-sama, dll. Misalnya, jika kita mengonversi dari String ke Integer, kita dapat mengonversi dari CompletableFuture ke CompletableFuture<Integer tanpa asosiasi. Hal ini dilakukan melalui kemudianApply():
Copy kode kodenya sebagai berikut:
<U> CompletableFuture<U> laluTerapkan(Fungsi<? super T,? extends U> fn);
<U> CompletableFuture<U> laluApplyAsync(Fungsi<? super T,? extends U> fn);
<U> CompletableFuture<U> kemudianApplyAsync(Fungsi<? super T,? extends U> fn, Eksekutor eksekutor);<p></p>
<p>Seperti yang disebutkan... versi Async menyediakan sebagian besar operasi di CompletableFuture, jadi saya akan melewatkannya di bagian selanjutnya. Ingat, metode pertama akan memanggil metode di thread yang sama tempat penyelesaian masa depan, sedangkan dua metode lainnya akan memanggilnya secara asinkron di kumpulan thread yang berbeda.
Mari kita lihat alur kerja kemudianApply():</p>
<p><pra>
Masa Depan yang Dapat Diselesaikan<String> f1 = //...
Masa Depan yang Dapat Diselesaikan<Bilangan Bulat> f2 = f1.thenApply(Bilangan Bulat::parseInt);
Masa Depan yang Dapat Diselesaikan<Double> f3 = f2.thenApply(r -> r * r * Math.PI);
</p>
Atau dalam pernyataan:
Copy kode kodenya sebagai berikut:
Masa Depan yang Dapat Diselesaikan<Ganda> f3 =
f1.thenApply(Bilangan Bulat::parseInt).thenApply(r -> r * r * Math.PI);
Di sini, Anda akan melihat konversi suatu barisan, dari String ke Integer ke Double. Namun yang terpenting, transformasi ini tidak terjadi secara instan dan tidak berhenti. Transformasi ini tidak terjadi secara instan atau berhenti. Mereka hanya mengingat program yang mereka jalankan ketika f1 asli selesai. Jika transformasi tertentu sangat memakan waktu, Anda dapat menyediakan Executor Anda sendiri untuk menjalankannya secara asinkron. Perhatikan bahwa operasi ini setara dengan peta unary di Scala.
4. Jalankan kode yang sudah selesai (thenAccept/thenRun)
Copy kode kodenya sebagai berikut:
CompletableFuture<Void> kemudianAccept(Konsumen<? super T> blok);
CompletableFuture<Void> laluRun(Tindakan yang dapat dijalankan);
Ada dua metode tahap "akhir" yang umum dalam saluran pipa di masa depan. Mereka disiapkan ketika Anda menggunakan nilai masa depan. Ketika kemudianAccept() memberikan nilai akhir, makaRunnable mengeksekusi Runnable, yang bahkan tidak memiliki cara untuk menghitung nilainya. Misalnya:
Copy kode kodenya sebagai berikut:
future.thenAcceptAsync(dbl -> log.debug("Hasil: {}", dbl), eksekutor);
log.debug("Melanjutkan");
...Variabel async juga tersedia dalam dua cara, pelaksana implisit dan eksplisit, dan saya tidak akan terlalu menekankan metode ini.
Metode kemudianAccept()/thenRun() tidak memblokir (meskipun tidak ada eksekutor eksplisit). Mereka seperti pendengar/penanganan peristiwa, yang akan dijalankan selama jangka waktu tertentu ketika Anda menghubungkannya ke masa depan. Pesan “Melanjutkan” akan langsung muncul, meskipun kedepannya malah belum selesai.
5. Penanganan kesalahan pada satu CompletableFuture
Sejauh ini kita hanya membahas hasil perhitungannya saja. Bagaimana dengan pengecualian? Bisakah kita menanganinya secara asinkron? tentu!
Copy kode kodenya sebagai berikut:
Masa Depan yang Dapat Diselesaikan<String> aman =
future.Exceptionally(ex -> "Kami mempunyai masalah: " + ex.getMessage());
Ketika secara luar biasa() menerima suatu fungsi, masa depan asli akan dipanggil untuk memberikan pengecualian. Kami akan memiliki kesempatan untuk mengubah pengecualian ini menjadi beberapa nilai yang kompatibel dengan tipe Masa Depan untuk dipulihkan. Konversi safeFurther tidak lagi memunculkan pengecualian namun akan mengembalikan nilai String dari fungsi yang menyediakan fungsionalitas tersebut.
Pendekatan yang lebih fleksibel adalah handle() untuk menerima fungsi yang menerima hasil atau pengecualian yang benar:
Copy kode kodenya sebagai berikut:
CompletableFuture<Integer> safe = future.handle((ok, ex) -> {
jika (oke != batal) {
return Integer.parseInt(ok);
} kalau tidak {
log.warn("Masalah", mis);
kembali -1;
}
});
handle() selalu dipanggil, dan hasil serta pengecualiannya tidak nol. Ini adalah strategi serba bisa.
6. Gabungkan dua CompletableFutures bersama-sama
CompletableFuture sebagai salah satu proses asinkron sangat bagus tetapi ini benar-benar menunjukkan betapa hebatnya ketika beberapa masa depan digabungkan dengan berbagai cara.
7. Gabungkan (tautkan) kedua masa depan ini (thenCompose())
Terkadang Anda ingin menjalankan nilai masa depan (bila sudah siap), namun fungsi ini juga mengembalikan masa depan. CompletableFuture cukup fleksibel untuk memahami bahwa hasil fungsi kita sekarang harus digunakan sebagai masa depan tingkat atas, dibandingkan dengan CompletableFuture<CompletableFuture>. Metode kemudianCompose() setara dengan flatMap Scala:
Copy kode kodenya sebagai berikut:
<U> CompletableFuture<U> kemudianTulis(Fungsi<? super T,CompletableFuture<U>> fn);
...Variasi asinkron juga tersedia. Dalam contoh berikut, amati dengan cermat jenis dan perbedaan antarathenApply()(map) danthenCompose()(flatMap).
Copy kode kodenya sebagai berikut:
Masa Depan yang Dapat Diselesaikan<Dokumen> docFuture = //...
Masa Depan yang Dapat Diselesaikan<Masa Depan yang Dapat Diselesaikan<Ganda>> f =
docFuture.thenApply(ini::calculateRelevance);
Masa Depan yang Dapat Diselesaikan<Double> relevansiMasa Depan =
docFuture.thenCompose(ini::calculateRelevance);
//...
private CompletableFuture<Double> calculRelevance(Dokumen dokumen) //...
kemudianCompose() adalah metode penting yang memungkinkan pembuatan pipeline yang kuat dan asinkron tanpa memblokir dan menunggu langkah-langkah perantara.
8. Nilai konversi dua futures (thenCombine())
Ketika ThenCompose() digunakan untuk merangkai masa depan yang bergantung pada ThenCombine yang lain, ketika keduanya selesai maka akan menggabungkan dua masa depan yang independen:
Copy kode kodenya sebagai berikut:
<U,V> CompletableFuture<V> laluCombine(CompletableFuture<? extends U> lainnya, BiFunction<? super T,? super U,? extends V> fn)
...Variabel async juga tersedia, dengan asumsi Anda memiliki dua CompletableFutures, satu memuat Pelanggan dan yang lainnya memuat Toko terbaru. Mereka benar-benar independen satu sama lain, tetapi ketika sudah selesai Anda ingin menggunakan nilainya untuk menghitung Rute. Berikut adalah contoh yang dapat dihilangkan:
Copy kode kodenya sebagai berikut:
CompletableFuture<Pelanggan> customerFuture = loadCustomerDetails(123);
CompletableFuture<Toko> shopFuture = Toko terdekat();
Masa Depan yang Dapat Diselesaikan<Rute> ruteMasa Depan =
customerFuture.thenCombine(shopFuture, (cust, toko) -> findRoute(cust, toko));
//...
Rute pribadi findRoute(Pelanggan pelanggan, Toko toko) //...
Harap dicatat bahwa di Java 8 Anda cukup mengganti referensi ke metode ini::findRoute dengan (cust, shop) -> findRoute(cust, shop):
Copy kode kodenya sebagai berikut:
customerFuture.thenCombine(shopFuture, ini::findRoute);
Seperti yang Anda ketahui, kami memiliki customerFuture dan shopFuture. Kemudian ruteFuture membungkusnya dan "menunggu" hingga selesai. Jika sudah siap, fungsi yang kami sediakan akan dijalankan untuk menggabungkan semua hasil (findRoute()). RouteFuture ini akan selesai ketika dua masa depan dasar selesai dan findRoute() juga selesai.
9. Tunggu hingga semua CompletableFutures selesai
Jika alih-alih membuat CompletableFuture baru yang menghubungkan kedua hasil ini, kita hanya ingin diberi tahu ketika hasil tersebut sudah selesai, kita dapat menggunakan serangkaian metode kemudianAcceptBoth()/runAfterBoth(), (...Variabel asinkron juga tersedia). Cara kerjanya mirip dengan kemudianAccept() dan kemudianRun(), namun menunggu dua masa depan, bukan satu:
Copy kode kodenya sebagai berikut:
<U> CompletableFuture<Void> laluAcceptBoth(CompletableFuture<? extends U> lainnya, BiConsumer<? super T,? super U> blok)
CompletableFuture<Void> runAfterBoth(CompletableFuture<?> lainnya, Tindakan yang dapat dijalankan)
Bayangkan contoh di atas, alih-alih membuat CompletableFuture baru, Anda hanya ingin mengirim beberapa peristiwa atau segera menyegarkan GUI. Hal ini dapat dengan mudah dicapai: kemudianAcceptBoth():
Copy kode kodenya sebagai berikut:
customerFuture.thenAcceptBoth(shopFuture, (cust, toko) -> {
rute akhir rute = findRoute(cust, toko);
//segarkan GUI dengan rute
});
Saya harap saya salah, tetapi mungkin beberapa orang akan bertanya pada diri sendiri: mengapa saya tidak bisa memblokir kedua masa depan ini saja? Menyukai:
Copy kode kodenya sebagai berikut:
Masa Depan<Pelanggan> customerFuture = loadCustomerDetails(123);
Masa Depan<Toko> shopFuture = Toko terdekat();
findRoute(customerFuture.get(), shopFuture.get());
Tentu saja Anda bisa melakukannya. Namun poin yang paling penting adalah CompletableFuture memungkinkan asynchronousness. Ini adalah model pemrograman berbasis peristiwa daripada memblokir dan menunggu hasil. Jadi secara fungsional, kedua bagian kode di atas setara, tetapi bagian terakhir tidak perlu menempati thread untuk mengeksekusinya.
10. Tunggu CompletableFuture pertama untuk menyelesaikan tugas
Hal menarik lainnya adalah CompletableFutureAPI dapat menunggu hingga masa depan pertama (berbeda dengan semua) selesai. Ini sangat berguna bila Anda memiliki hasil dari dua tugas berjenis sama. Anda hanya peduli pada waktu respons, dan tidak ada tugas yang diprioritaskan. Metode API (… Variabel asinkron juga tersedia):
Copy kode kodenya sebagai berikut:
CompletableFuture<Void> terima(CompletableFuture<? extends T> lainnya, Konsumen<? super T> blok)
CompletableFuture<Void> runAfterEither(CompletableFuture<?> lainnya, tindakan yang dapat dijalankan)
Misalnya, Anda mempunyai dua sistem yang dapat diintegrasikan. Yang satu mempunyai waktu respons rata-rata yang lebih kecil tetapi standar deviasinya tinggi, sedangkan yang lainnya umumnya lebih lambat namun lebih dapat diprediksi. Untuk mendapatkan yang terbaik dari kedua dunia (kinerja dan prediktabilitas), Anda dapat memanggil kedua sistem secara bersamaan dan menunggu sistem mana yang selesai terlebih dahulu. Biasanya ini akan menjadi sistem pertama, namun ketika kemajuan menjadi lambat, sistem kedua dapat diselesaikan dalam waktu yang dapat diterima:
Copy kode kodenya sebagai berikut:
Masa Depan yang Dapat Diselesaikan<String> fast = FetchFast();
CompletableFuture<String> dapat diprediksi = FetchPredictably();
fast.acceptEither(dapat diprediksi, s -> {
System.out.println("Hasil: " + s);
});
s mewakili String yang diperoleh dari FetchFast() atau FetchPredictably(). Kita tidak perlu tahu atau peduli.
11. Konversikan sistem pertama sepenuhnya
applyToEither() dianggap sebagai pendahulu dari AcceptEither(). Ketika dua masa depan akan selesai, yang terakhir hanya memanggil beberapa cuplikan kode dan applyToEither() akan mengembalikan masa depan yang baru. Ketika dua masa depan awal ini selesai, maka masa depan yang baru juga akan selesai. API-nya agak mirip (... Variabel asinkron juga tersedia):
Salin kode sebagai berikut:<U> CompletableFuture<U> applyToEither(CompletableFuture<? extends T> other, Function<? super T,U> fn)
Fungsi fn tambahan ini dapat diselesaikan ketika masa depan pertama dipanggil. Saya tidak yakin apa tujuan dari metode khusus ini, lagipula orang cukup menggunakan: fast.applyToEither(predictable).thenApply(fn). Karena kita terjebak dengan API ini, tetapi kita tidak benar-benar membutuhkan fungsionalitas tambahan untuk aplikasi tersebut, saya cukup menggunakan placeholder Function.identity():
Copy kode kodenya sebagai berikut:
Masa Depan yang Dapat Diselesaikan<String> fast = FetchFast();
CompletableFuture<String> dapat diprediksi = FetchPredictably();
Masa Depan yang Dapat Diselesaikan<String> firstDone =
fast.applyToEither(dapat diprediksi, Fungsi.<String>identitas());
Masa depan yang diselesaikan pertama dapat dijalankan. Perhatikan bahwa dari sudut pandang klien, kedua masa depan sebenarnya tersembunyi di balik firstDone. Klien hanya menunggu hingga masa depan selesai dan menggunakan applyToEither() untuk memberi tahu klien ketika dua tugas pertama selesai.
12. CompletableFuture dengan beberapa kombinasi
Kita sekarang tahu cara menunggu dua masa depan selesai (menggunakan kemudianCombine()) dan yang pertama selesai (applyToEither()). Namun, bisakah hal ini mencapai sejumlah masa depan? Memang, gunakan metode pembantu statis:
Copy kode kodenya sebagai berikut:
static CompletableFuture<Void< allOf(CompletableFuture<?<... cfs)
static CompletableFuture<Objek< anyOf(CompletableFuture<?<... cfs)
allOf() menggunakan serangkaian masa depan dan mengembalikan masa depan (menunggu semua rintangan) ketika semua potensi masa depan telah selesai. Di sisi lain anyOf() akan menunggu potensi masa depan tercepat. Silakan lihat jenis umum masa depan yang dikembalikan. Kami akan fokus pada masalah ini di artikel berikutnya.
Meringkaskan
Kami menjelajahi seluruh API CompletableFuture. Saya yakin ini tidak akan terkalahkan, jadi di artikel berikutnya kita akan melihat implementasi perayap web sederhana lainnya menggunakan metode CompletableFuture dan ekspresi lambda Java 8. Kami juga akan melihat CompletableFuture