- Java 21, yang dirilis pada 19 September 2023, membawa ekspresi pola fungsional yang lebih dekat ke Kotlin, Rust, dan C# ke dalam Java melalui record patterns dan switch pattern matching
- Fondasi untuk menangani algebraic data types di Java 21 terbentuk dari akumulasi switch expressions di Java 14, records dan
instanceof pattern matching di Java 16, serta sealed classes di Java 17
- records membuat data lebih aman untuk diurai berkat batasan seperti final, referensi tak berubah, dan getter baku, sementara record pattern memungkinkan data bertingkat diambil langsung di dalam switch
- sealed classes/interfaces hanya membuka subtipe yang diizinkan sehingga bisa membentuk model yang mendekati sum type; dengan menggabungkan sealed interface dan record, variasi seperti RGB, CMYK, YUV, dan HSL bisa dibatasi
- switch di Java 21 juga mendukung case
null dan guard when, tetapi accessor record yang salah atau exception saat guard dieksekusi dapat berujung pada java.lang.MatchException
Pattern matching yang distabilkan di Java 21
- Java 21 dirilis pada 19 September 2023 dan mendukung record patterns di switch block dan switch expression
- Sintaks ini dinilai sebagai titik balik yang memungkinkan Java mengekspresikan pola pemrograman fungsional dengan cara yang mirip Kotlin, Rust, dan C#
- Perubahan sintaks utama di versi-versi Java terbaru berpuncak pada pattern matching di Java 21
- Java 14: switch expressions distabilkan
- Java 16: records,
instanceof pattern matching distabilkan
- Java 17: sealed classes distabilkan
- Java 21: record patterns, switch pattern matching distabilkan
- Kumpulan perubahan ini membuat Java mampu menangani algebraic data types dan pola penggunaan idiomatisnya, yang sebelumnya sulit diekspresikan
Konsep minimum yang dibutuhkan dari teori tipe
- Untuk memahami fitur-fitur Java 21, ada beberapa konsep teori tipe yang perlu diketahui
- bottom/empty type merepresentasikan himpunan nilai yang tidak dapat dihitung, dan dalam bahasa pemrograman umum biasanya berupa himpunan kosong
Nothing di Kotlin tidak bisa memiliki instance karena konstruktornya private
Void di Java memiliki konstruktor private, tetapi masih bisa memuat null, sehingga sulit dianggap sebagai bottom type sejati
void primitif di Java tidak bisa digunakan sebagai tipe variabel, sehingga dalam hal ini perilakunya lebih dekat
- top type adalah himpunan universal yang mencakup semua nilai dari semua tipe
- Di Kotlin, peran ini dipegang oleh
Any
Object di Java sulit disamakan dengan top type di bahasa lain karena primitive terpisah dari model objek
- unit type adalah tipe yang hanya memiliki satu nilai
void di Java bisa diperlakukan seperti unit type untuk nilai balik method, tetapi tidak bisa diteruskan sebagai tipe parameter
Unit di Kotlin didefinisikan sebagai object dan juga bisa digunakan sebagai parameter method
- boolean type memiliki dua nilai,
true dan false, dan meski bisa direpresentasikan sebagai nullable unit type, itu tidak praktis
Product type dan Java records
- product type adalah tipe yang menggabungkan dua atau lebih tipe penyusun, dan jumlah tipe penyusunnya disebut arity atau degree
struct di C adalah contoh product type
- Tipe penyusunnya bisa berulang seperti
int, char *, double, int
- Tipe yang berulang bisa dibedakan jika dipandang sebagai pasangan terurut bersama nama field
- Tuple di Python atau Rust juga bisa dipandang sebagai product type; dalam kasus ini, indeks berperan sebagai nama komponen
- record class yang distabilkan di Java 16 adalah contoh product type yang baik
- Field pada record bersifat final, dan record tidak bisa diwariskan
- State record ditetapkan saat pembuatan dan dipertahankan setelahnya
- Namun, jika record berisi mutable data type, ketakterubahan isi datanya sendiri tidak otomatis terjamin
- Java class biasa dapat mencampurkan state public/private, state tersembunyi akibat pewarisan, field mutable/static, dan getter nonstandar, sehingga komponennya sulit digeneralisasi
- records menjamin struktur yang membuat fitur bahasa seperti pattern matching bisa bekerja dengan stabil melalui batasan berikut
- record secara implisit adalah final class dan tidak bisa diwariskan
- record tidak bisa
extend class selain java.lang.Record
- record component tidak bisa diberi visibility modifier
- referensi component selalu final dan diperlakukan tak berubah
- getter bawaan menggunakan nama field apa adanya, sehingga getter untuk field
a adalah a()
- backing field secara implisit bersifat private dan diakses melalui getter
Mengurai data bertingkat dengan record pattern
- switch pattern di Java 21 mengurai nested record data tanpa perlu mengulang pemeriksaan
instanceof dan cast eksplisit
- Contohnya menggunakan
record A(Record inner), record B(char b), dan record SomeOtherRecord()
- Cara lama mengharuskan cast setelah
if (r instanceof A), lalu mengulang instanceof dan cast lagi untuk nilai di dalamnya
- switch pattern dapat langsung mengekstrak nilai bertingkat seperti
case A(B(char a)) -> String.valueOf(a)
- switch block memiliki struktur yang lebih jelas daripada if-else ladder dan cocok untuk mengambil data yang bersarang dalam dengan cepat
- Untuk menjalankannya langsung di Java 21, letakkan kode di
main.java dan gunakan perintah berikut
java --enable-preview --source 21 main.java
Sum type dan sealed classes/interfaces
- Untuk mengekspresikan pilihan yang terbatas, Java enum bisa digunakan, tetapi representasi warna dengan struktur data berbeda seperti RGB, HSL, YUV, dan CMYK sulit ditangani hanya dengan enum
- Dengan polimorfisme berbasis pewarisan, kita bisa membuat abstract class
Color serta class RGB, CMYK, YUV, dan HSL, tetapi hierarki class biasa bersifat terbuka
- Pengguna library bisa membuat class baru seperti
RYB yang mewarisi Color
- Jika API tidak dirancang untuk diperluas, variasi baru ini dapat menyebabkan crash atau bug halus di kode yang jauh dari sumbernya
- sealed classes digunakan untuk mengekspresikan konsep sum type di Java
- sum type adalah tipe yang pada suatu waktu hanya bisa menjadi salah satu dari komponennya
- Ini juga disebut tagged union type
- Dengan modifier
sealed dan klausa permits, hanya class tertentu yang boleh mewarisi
public sealed class Color permits RGB, CMYK, YUV, HSL {
}
- Dalam hierarki sealed class, pewaris langsung maupun tidak langsung harus memiliki salah satu dari
sealed, non-sealed, atau final; jika tidak, akan terjadi compile error
sealed: hanya tipe yang disebut dalam permits yang bisa mewarisi
non-sealed: bisa diwarisi seperti class biasa
final: menjadi leaf pada pohon pewarisan dan tidak bisa diperluas lagi
Menggunakan sealed interface bersama record
- Destructuring pada switch pattern bekerja untuk records, tetapi record tidak bisa mewarisi class selain
Record
- Solusinya adalah menggunakan sealed interface
- sealed interface bekerja mirip dengan sealed class
- record dan enum juga bisa
implement sealed interface
- Dalam contoh,
Color dibuat sebagai sealed interface dan RGB, CMYK, YUV, serta HSL diimplementasikan sebagai record
public sealed interface Color permits RGB, CMYK, YUV, HSL {
String getDescription();
}
record RGB(int red, int green, int blue) implements Color {
public String getDescription() {
return "RGB Color: (" + red + ", " + green + ", " + blue + ")";
}
}
- Setelah itu, nilai dari tiap record bisa langsung diekstrak di dalam switch
switch (color) {
case RGB(int red, int green, int blue) -> {
}
case CMYK(double cyan, double magenta, double yellow, double black) -> {
}
case YUV(int y, int u, int v) -> {
}
case HSL hsl -> {
System.out.println(hsl.getDescription());
}
case null -> {
System.out.println("How did color become null?!");
}
}
- Java 21 memungkinkan penanganan case
null di switch block dan expression, sehingga tidak perlu pemeriksaan null terpisah sebelum switch
- Jika
Color adalah sealed type, Java dapat mengetahui apakah semua case sudah ditangani, sehingga exhaustive switch dimungkinkan tanpa default case
Guard clause dan when
- Java 21 mendukung guard clause untuk menambahkan kondisi tambahan pada switch arm
- Guard clause mengintegrasikan kondisi ke dalam case label dengan keyword
when
switch (color) {
case RGB(int red, int green, int blue) when red > 200 -> {
System.out.println("Very red.");
}
case RGB rgb when rgb.green > 100 -> {
System.out.println("Sort of green...");
}
case RGB rgb -> {
System.out.println("Not that red...");
}
}
- Sebelumnya, kita harus menambahkan lagi
if (red > 200) di dalam isi case RGB(...)
- Java mencocokkan case pertama yang bernilai true secara eager, jadi case yang lebih spesifik harus ditempatkan lebih dulu, lalu diikuti case yang kurang spesifik
- Setelah RGB case dengan guard,
case RGB rgb biasa tetap diperlukan untuk menjaga exhaustiveness
Kapan MatchException terjadi
- Pattern matching di Java 21 juga melibatkan
java.lang.MatchException
- Jika record accessor melempar exception, switch pattern akan gagal dan dapat memunculkan
MatchException
record R(int i) {
public int i() {
return i / 0;
}
}
static void exampleAnR(R r) {
switch(r) {
case R(var i): System.out.println(i);
}
}
- Pada contoh di atas, accessor
i() melempar ArithmeticException, sehingga switch block melempar MatchException
- Menurut JEP 441, record accessor yang selalu melempar exception adalah kasus yang sangat tidak lazim, dan exhaustive pattern switch yang melempar
MatchException juga sangat jarang terjadi
- Bahkan pada exhaustive switch, exception bisa muncul jika tidak ada satu pun variant yang ditentukan cocok dengan selector
- JEP 441 menjelaskan kegagalan pencocokan pada exhaustive switch untuk enum sebagai situasi ketika enum class berubah setelah switch dikompilasi
- Exception saat guard clause dieksekusi juga dapat menyebabkan
MatchException
static void example(Object obj) {
switch (obj) {
case R r when (r.i / 0 == 1): System.out.println("It's an R!");
default: break;
}
}
Cakupan yang tersisa
- Dengan menggabungkan records, sealed types, switch pattern matching, dan guard clause di Java 21, building block pemrograman fungsional bisa diterapkan ke dalam kode Java
- Beberapa topik seperti cara generics berinteraksi dengan switch patterns tidak dibahas
- Tulisan berikutnya akan membahas quirks dan contoh praktis yang bisa digunakan untuk memperbaiki cara menulis kode Java
1 komentar
Komentar Hacker News
Fitur terbesar Java 21 adalah rilis virtual thread: https://openjdk.org/jeps/444
Entah kenapa ini tidak disebutkan di artikel. Kalau ada fitur yang bisa menarik pengembang Go yang sudah ada ke Java, mungkin inilah dia, dan sepertinya juga bisa meyakinkan orang-orang yang tidak menyukai pola konkurensi bergaya reaktif
Aplikasi dan library Java terlalu sulit untuk dinalar dan dipahami dibandingkan Go, karena pewarisan, packaging, OOP, build tool, dan sebagainya
Go itu sederhana serta mudah dipahami, dibaca, dan dipelihara. Packaging-nya mirip cara menata file dalam satu folder di komputer, dan tooling-nya juga terintegrasi ke dalam bahasa. Tidak terasa seperti baru cukup enak dipakai kalau ada IDE seperti IntelliJ
Mungkin sekarang sudah berubah, tetapi sebagian besar library Java yang saya lihat belakangan ini masih tampak seperti itu
Ada banyak hal yang saya sukai dari JVM dan ekosistem tooling-nya, tetapi menulis kode Java sekarang kurang menarik bagi saya. JRuby memberi sebagian keunggulan dari kedua sisi
Presentasinya ada di sini, dan demo virtual thread muncul sekitar menit ke-45
https://youtu.be/pzm6I4liJlg?si=GtxQ4MThEaNDfC67
Bahkan
sync.Mappun bukan sesuatu sepertiConcurrentMapserbaguna milik Java, melainkan dikhususkan untuk dua kasus penggunaan tertentu. Java memiliki set konkurensi, queue, barrier, phaser, fork-join pool, dan sebagainya. Meski ada goroutine, container seperti ini tetap cukup berguna; setidaknya fork-join bukan implementasi yang sesederhana itu. Memakai mutex di mana-mana terasa terlalu level rendahSaya tahu ada implementasi pihak ketiga, tetapi konkurensi terlalu sulit dibuat dengan benar, jadi saya ragu mengadopsi paket pihak ketiga kecuali sudah pada tingkat matang dengan banyak pengguna dan pengembang di belakangnya, seperti JCTools milik Java atau Google Guava
Executor.newVirtualThreadPerTaskExecutordangomenunjukkan dengan baik alasan utama mengapa pengembang Go tidak akan pindah ke JavaKoreksinya, sebenarnya lebih mendekati
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(...) }ketimbanggoMenurut saya judul posting blog itu kurang tepat. Subjudul tersembunyinya adalah "Algebraic data types in Java", dan itu jauh lebih sesuai untuk menjelaskan isinya. Judul yang lebih baik mungkin Algebraic data types in Java 21
Mungkin karena judulnya, cukup banyak komentar di sini keluar dari topik. Saya ingin melihat lebih banyak pembahasan tentang algebraic data type, kelebihan dan kekurangan implementasi Java, serta perbandingan teknis dengan bahasa lain
Kode Java yang sudah ada tidak akan hilang, jadi kalau kode seperti ini tercampur secara acak, saya bertanya-tanya apakah itu benar-benar akan membuatnya lebih baik
Fitur sealed classes yang dijelaskan di sini terasa benar-benar keliru
Logikanya, kalau ada interface biasa, siapa pun bisa membuat class baru yang mengimplementasikannya, dan kode seperti
if (x instanceof Foo) { ... } else if (x instanceof Bar) { ... }akan rusak saat runtime jika seseorang menambahkan class baru. Jadi dengan fitur sealed interface baru, jika tidak ada yang bisa membuat class baru yang mengimplementasikan interface itu, pernyataaniftidak akan rusakTapi bukankah pemrograman berorientasi objek sudah memikirkan dan menyelesaikan masalah seperti ini? Saya tahu OOP sekarang sudah tidak sepopuler dulu, tetapi Java adalah bahasa berorientasi objek
Solusinya adalah menambahkan method ke interface dan membuat semua class mengimplementasikannya. Dengan begitu, alih-alih mencantumkan semua pilihan dalam if/switch raksasa, cukup panggil method tersebut
Cara ini lebih baik daripada mencegah kode diperluas; justru memungkinkan ekstensi. Implementasi baru hanya perlu mengimplementasikan method itu, dan karena compiler memaksanya, tidak mungkin terlewat tanpa sengaja
Contoh ruang warna dalam artikel (RGB, CMYK, dll.) adalah contoh tandingan yang sangat bagus. Jika saya menulis kode yang memakai ruang warna, pengguna atau pelanggan mungkin perlu memakai ruang warna aneh dan langka yang tidak saya pikirkan. Saya tidak ingin membuat kode yang hanya mendukung ruang warna yang dicantumkan dalam if/switch raksasa, dan karena struktur seperti itu menjadi tidak bisa diperluas
Sealed classes menyelesaikan masalah ini. Namun sebagai gantinya muncul masalah baru: "bagaimana jika kita membutuhkan lebih banyak class turunan dan tidak bisa mengetahui semuanya di awal?" Pada akhirnya, pertanyaannya adalah apakah ada cara untuk mencapai keduanya
Masalah ini disebut expression problem [1]
Ada bahasa bertipe statis yang bisa memecahkan expression problem, dan Java termasuk salah satunya [2]. Namun cara melakukannya di Java masih sangat rumit dan merepotkan sehingga hampir tidak digunakan. Jika ingin tetap berada di dunia Haskell atau JVM, Scala jauh lebih baik untuk hal ini
[1] https://en.wikipedia.org/wiki/Expression_problem
[2] https://koerbitz.me/posts/Solving-the-Expression-Problem-in-...
Method interface dan virtual call sangat tidak fleksibel ketika kita ingin menambahkan operasi baru, bukan class baru. Bahkan untuk menambahkan satu operasi baru saja, kita harus mendatangi semua implementasi dan menambahkan method baru, dan bisa merusak implementasi yang tidak kita punya aksesnya. Method-method yang tidak saling terkait juga harus didefinisikan dalam satu class, sehingga keterbacaan kode sangat memburuk, dan virtual call juga tidak gratis sehingga berdampak pada performa
Dalam kasus ini, sealed class jauh lebih mudah diperluas. Cukup tambahkan switch baru di satu tempat, dan kompatibilitas ke belakang pun tidak rusak
Inilah expression problem yang terkenal itu
https://pkolaczk.github.io/in-defense-of-switch/
instanceof, dan saya juga pernah melihat Bob Martin membahasnya panjang lebar, tetapi saya tidak setujuUntuk melakukan dispatch polimorfik seperti ini, objek harus menangani sendiri banyak concern
Dalam video game,
Carbisa memiliki.render(),.collide(),.playSound(). Nanti ketikaDogditambahkan, cukup implementasikan tiga method ini, danRenderer,PhysicsEngine,SoundEnginetidak perlu diubah atau dikompilasi ulang. Programmer lain juga bisa menambahkan entitas tanpa memasukkan bug ke kode berharga saya. Kelihatannya bagusNamun sekarang
CardanDogharus mengetahui semuanya tentang grafis, fisika, dan suara. Dan entitas tidak ada secara terisolasi. Mobil dan anjing harus dirender dalam urutan yang benar dan bisa saling menutupi. Tabrakan juga harus diperiksa satu sama lain. Seperti yang saya alami dalam game jam sungguhan, bisa juga terjadi situasi di mana orang yang menangani suara harus masuk ke dalam semua objek hanya untuk menambahkan perilaku suaraJauh lebih baik bekerja di dalam
Physics.collideAll()saat memikirkan fisika dan melakukan penanganan khusus denganinstanceofjika perlu, lalu bekerja di dalamGraphics.renderAll()saat memikirkan grafisDalam pengembangan web backend Java sehari-hari pun mirip. Saat menentukan cara mengubah objek Java menjadi respons HTTP di REST controller, lebih baik melihat semuanya dalam satu method dan memetakan
{instanceof Forbidden}ke 403 serta{instanceof NotFound}ke 404. Saya tidak ingin memasukkangetCode()atau hal khusus REST ke dalam class Java itu sendiriStringbersifatfinal, bahkan bisa saja dianggap bahwafinalseharusnya menjadi default dan hanya class yang memang mengizinkan subclassing yang harus dinyatakan secara eksplisit sebagaiopenDalam pemrograman fungsional, contoh klasik sum type adalah list. Di sini hanya ada
Element(T head, List tail)danNil(). Tidak ada alasan untuk memperluasnya, dan jika benar-benar diperluas, saat digabungkan dengan semua fungsi yang menangani list, hasilnya bisa menjadi kode yang salahSelain itu, visitor pattern yang mirip dengan pattern matching sangat verbose dan bergantung pada hack yang memanfaatkan semantik dispatch method biasa di Java. Dalam hal ini, menurut saya pattern matching berkali-kali lebih mudah dibaca
Misalnya, kita bisa membayangkan interface keamanan yang memvalidasi token keamanan
Jika itu interface biasa, mudah saja mengimplementasikannya untuk mengabaikan token (mengizinkan semuanya), mencuri token, atau memasukkan backdoor. Jika class seperti itu diinjeksikan ke tempat pemeriksaan keamanan dilakukan, keamanan bisa ditembus
Dengan sealed interface, implementasi baru yang tidak disetujui tidak bisa ada. Jika kita menerima objek yang mengklaim mengimplementasikan interface itu, ada jaminan bahwa objek tersebut adalah salah satu implementasi terverifikasi yang benar-benar melakukan pemeriksaan keamanan. Dengan kata lain, satu kategori bug keamanan dan exploit telah dihapus seluruhnya
Ini tulisan yang bagus dari sudut pandang orang yang tahu sum type, tetapi belum begitu memahami sum type di Java
Namun, saya tidak yakin apakah hanya dengan sum type orang akan kembali menyukai Java. Kemungkinan null yang luas masih tetap ada, dan dalam tulisan ini pun beberapa kali muncul
Saya sangat menantikan https://jspecify.dev/ yang diupayakan Google, Meta, Microsoft, dan lainnya untuk menstandarkan anotasi, dimulai dari
@NullableJawaban tulisan itu untuk “mengapa disebut product type?” tidak salah, tetapi jika dijelaskan lebih intuitif dan ringkas, jumlah total nilai yang mungkin pada product type adalah hasil perkalian jumlah nilai yang mungkin dari tipe-tipe penyusunnya
Hal yang sama juga berlaku jika product diganti dengan sum
Menariknya, jumlah total fungsi unik berbentuk
a -> b, jika hanya dilihat dari input dan output, bisa dihitung sebagai eksponen. Yaitu(jumlah nilai yang mungkin dari b) ^ (jumlah nilai yang mungkin dari a)Jika ditulis secara matematis, pada list Bool, sisi kiri adalah jumlah elemen dan sisi kanan adalah jumlah seluruh kemungkinan
0 : 1
1 : 2
2 : 4
3 : 8
4 : 16
5 : 32
Dan seterusnya
Saya menunggu Project Valhalla selesai sehingga akhirnya value type masuk ke Java. Dengan begitu, Java akan memiliki sum type, value type, dan coroutine, sehingga tampaknya akan menjadi salah satu bahasa yang cukup bagus
Java pada dasarnya bukan bahasa yang benar-benar buruk
Masalahnya ada pada orang-orangnya. Rekayasa berlebihan yang masif, terlalu banyak konsep abstrak yang membuat codebase sulit dipahami, sihir kode dalam bentuk anotasi seperti pernyataan GOTO terbalik, serta framework DI adalah masalahnya
Yang perlu diperbaiki bukan bahasanya, melainkan ekosistemnya. Di dalam ekosistem Java, perlu semacam gerakan “reformasi”
Berpindah ke Kotlin, Clojure, atau Scala saja tidak cukup
HammerFactoryFactoryyang menghasilkanHammerFactory. Namun ekosistem Java memicu dan mendorong cara menyelesaikan masalah seperti ini. Menurut saya C# juga miripSatu hal yang benar-benar dibutuhkan Java adalah fungsi independen, atau fungsi dengan namespace. Kadang kita tidak memerlukan class; fungsi di dalam modul atau namespace saja sudah cukup, tetapi entah mengapa itu tidak bisa dilakukan
Penulis menjelaskan mengapa Records dibutuhkan, dengan menunjukkan bahwa sebagian besar objek Java menjadikan semua field sebagai private dan hanya mengaksesnya melalui metode accessor baca/tulis
Namun, tidak ada konvensi yang dipaksakan di tingkat bahasa untuk mendefinisikan accessor, sehingga meskipun getter untuk
foodiberi namagetBar, itu tetap berfungsi, tetapi bisa membingungkan orang yang ingin mengaksesbarScala mendukung pattern matching untuk objek yang mengimplementasikan metode
unapply. Apakah pendekatan ini dianggap berbahaya? Mengapa Java tidak mengikuti jalur ini?unapplymungkin sedang disiapkan, jadi harapan belum sepenuhnya hilangJava selalu merupakan bahasa yang bagus. Yang membuat ingin muntah adalah ekosistem bergaya enterprise. Saya pernah melihat puluhan class dan interface digunakan hanya untuk mengimplementasikan satu baris logika