Panduan Lengkap untuk Mengganti Ekspresi di Jawa 12


switch lama yang baik switch ada di Jawa sejak hari pertama. Kita semua menggunakannya dan terbiasa dengannya - terutama kebiasaannya. (Apakah ada orang lain yang kesal karena break ?) Tetapi sekarang semuanya mulai berubah: di Java 12, sakelar alih-alih operator telah menjadi ekspresi:


 boolean result = switch(ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); // as we'll see in "Exhaustiveness", `default` is not necessary default -> throw new IllegalArgumentException("Seriously?!"); }; 

Switch sekarang memiliki kemampuan untuk mengembalikan hasil kerjanya, yang dapat ditugaskan ke variabel; Anda juga dapat menggunakan sintaks gaya lambda, yang memungkinkan Anda untuk menyingkirkan pass-through untuk semua case di mana tidak ada pernyataan break .


Dalam panduan ini, saya akan memberi tahu Anda tentang semua yang perlu Anda ketahui tentang beralih ekspresi di Jawa 12.


Pratinjau


Menurut spesifikasi awal bahasa , ekspresi saklar baru mulai diimplementasikan di Jawa 12.


Ini berarti bahwa konstruk kontrol ini dapat diubah dalam versi spesifikasi bahasa yang akan datang.


Untuk mulai menggunakan versi baru switch Anda perlu menggunakan opsi baris perintah --enable-preview baik saat kompilasi maupun saat startup program (Anda juga harus menggunakan --release 12 saat mengkompilasi - catatan oleh penerjemah).


Jadi perlu diingat bahwa switch , sebagai ekspresi, saat ini tidak memiliki sintaks akhir di Java 12.


Jika Anda memiliki keinginan untuk bermain dengan semua ini sendiri, maka Anda dapat mengunjungi proyek demo Java X saya di github .


Masalah dengan pernyataan dalam switch


Sebelum kita beralih ke tinjauan umum inovasi, mari kita cepat mengevaluasi satu situasi. Misalkan kita dihadapkan dengan boulean ternary "mengerikan" dan ingin mengubahnya menjadi boulean biasa. Inilah salah satu cara untuk melakukan ini:


 boolean result; switch(ternaryBool) { case TRUE: result = true; // don't forget to `break` or you're screwed! break; case FALSE: result = false; break; case FILE_NOT_FOUND: // intermediate variable for demo purposes; // wait for it... var ex = new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); throw ex; default: // ... here we go: // can't declare another variable with the same name var ex2 = new IllegalArgumentException("Seriously?!"); throw ex2; } 

Setuju bahwa ini sangat merepotkan. Seperti banyak opsi sakelar lain yang ditemukan di "nature", contoh di atas hanya menghitung nilai variabel dan menetapkannya, tetapi implementasinya dilewati (nyatakan result pengidentifikasi dan gunakan nanti), diulangi ( break saya 'dan selalu hasil dari copy-paste) dan rawan kesalahan (lupa cabang lain? Oh!). Jelas ada sesuatu untuk diperbaiki.


Mari kita coba selesaikan masalah ini dengan menempatkan sakelar pada metode terpisah:


 private static boolean toBoolean(Bool ternaryBool) { switch(ternaryBool) { case TRUE: return true; case FALSE: return false; case FILE_NOT_FOUND: throw new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); // without default branch, the method wouldn't compile default: throw new IllegalArgumentException("Seriously?!"); } } 

Ini jauh lebih baik: tidak ada variabel dummy, tidak ada break mengacaukan kode dan pesan penyusun tentang tidak adanya default (bahkan jika ini tidak perlu, seperti dalam kasus ini).


Tetapi, jika Anda memikirkannya, kami tidak diharuskan membuat metode hanya untuk menghindari fitur bahasa yang canggung. Dan ini bahkan tanpa mempertimbangkan bahwa refactoring seperti itu tidak selalu memungkinkan. Tidak, kami membutuhkan solusi yang lebih baik!


Memperkenalkan ekspresi saklar!


Seperti yang saya tunjukkan di awal artikel, dimulai dengan Java 12 ke atas, Anda dapat memecahkan masalah di atas sebagai berikut:


 boolean result = switch(ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); // as we'll see in "Exhaustiveness", `default` is not necessary default -> throw new IllegalArgumentException("Seriously?!"); }; 

Saya pikir ini cukup jelas: jika ternartBool adalah TRUE , maka result 'akan disetel ke true (dengan kata lain, TRUE berubah menjadi true ). FALSE menjadi false .


Dua pikiran segera muncul:


  • switch mungkin memiliki hasil;
  • ada apa dengan panah?

Sebelum mempelajari detail fitur sakelar baru, pada awalnya saya akan berbicara tentang dua aspek utama ini.


Ekspresi atau pernyataan


Anda mungkin terkejut bahwa peralihan sekarang menjadi ekspresi. Tapi apa yang dia lakukan sebelumnya?


Sebelum Java 12, sebuah saklar adalah operator - sebuah konstruksi imperatif yang mengontrol aliran kontrol.


Pikirkan perbedaan antara versi lama dan baru dari saklar sebagai perbedaan antara if dan operator ternary. Mereka berdua memeriksa kondisi logis dan melakukan percabangan tergantung pada hasilnya.


Perbedaannya adalah bahwa if hanya mengeksekusi blok yang sesuai, sedangkan operator ternary mengembalikan beberapa hasil:


 if(condition) { result = doThis(); } else { result = doThat(); } result = condition ? doThis() : doThat(); 

Hal yang sama berlaku untuk switch : sebelum Java 12, jika Anda ingin menghitung nilai dan menyimpan hasilnya, Anda akan menugaskannya ke variabel (dan kemudian break ), atau mengembalikannya dari metode yang dibuat khusus untuk switch .


Sekarang, seluruh ekspresi pernyataan switch dievaluasi (cabang yang sesuai dipilih untuk dieksekusi), dan hasil perhitungan dapat ditugaskan ke variabel.


Perbedaan lain antara ekspresi dan pernyataan adalah bahwa pernyataan switch , karena merupakan bagian dari pernyataan, harus diakhiri dengan tanda titik koma, tidak seperti pernyataan switch klasik.


Panah atau titik dua


Contoh pengantar menggunakan sintaks gaya lambda baru dengan panah antara label dan bagian yang berjalan. Penting untuk dipahami bahwa untuk ini, tidak perlu menggunakan switch sebagai ekspresi. Faktanya, contoh di bawah ini setara dengan kode yang diberikan di awal artikel:


 boolean result = switch(ternaryBool) { case TRUE: break true; case FALSE: break false; case FILE_NOT_FOUND: throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); default: throw new IllegalArgumentException("Seriously?!!?"); }; 

Perhatikan bahwa sekarang Anda dapat menggunakan break dengan suatu nilai! Ini sangat cocok dengan switch gaya lama yang menggunakan break tanpa makna. Jadi dalam hal apa panah berarti ekspresi daripada operator, mengapa ada di sini? Hanya sintaks hipster?


Secara historis, tanda titik dua hanya menandai titik masuk ke blok pernyataan. Dari titik ini, eksekusi semua kode di bawah ini dimulai, bahkan ketika label lain ditemukan. Sebagai switch kami mengetahui hal ini sebagai melintas ke case berikutnya (fall-through): label case menentukan ke mana aliran kontrol melompat. Untuk melengkapinya, Anda perlu break atau return .


Pada gilirannya, menggunakan panah berarti hanya blok di sebelah kanannya yang akan dieksekusi. Dan tidak ada "gagal."


Lebih lanjut tentang evolusi saklar


Banyak tag pada kasing


Sejauh ini, setiap case hanya case satu label. Tetapi sekarang semuanya telah berubah - satu case dapat sesuai dengan beberapa label:


 String result = switch(ternaryBool) { case TRUE, FALSE -> "sane"; // `default, case FILE_NOT_FOUND -> ...` does not work // (neither does other way around), but that makes // sense because using only `default` suffices default -> "insane"; }; 

Perilaku harus jelas: TRUE dan FALSE menghasilkan hasil yang sama - ungkapan "waras" dievaluasi.


Ini adalah inovasi yang lumayan bagus yang menggantikan banyak penggunaan case ketika diperlukan untuk mengimplementasikan transisi pass-through ke case berikutnya.


Jenis Di Luar Enum


Semua switch contoh dalam artikel ini menggunakan enum . Bagaimana dengan tipe lain? Pernyataan dan switch juga dapat berfungsi dengan String , int , (periksa dokumentasi ) short , byte , char , dan pembungkusnya. Sejauh ini tidak ada yang berubah di sini, meskipun gagasan untuk menggunakan tipe data seperti float dan long masih valid (dari paragraf kedua hingga paragraf terakhir).


Lebih lanjut tentang panah


Mari kita lihat dua properti khusus untuk bentuk panah dari catatan pemisah:


  • kurangnya transisi ujung ke ujung ke case selanjutnya;
  • blok operator.

Tidak ada pass-through ke case selanjutnya


Inilah yang dikatakan JEP 325 tentang ini:


Desain saat ini dari switch di Jawa terkait erat dengan bahasa seperti C dan C ++ dan mendukung semantik end-to-end secara default. Meskipun cara tradisional pengendalian ini sering berguna untuk menulis kode tingkat rendah (seperti parser untuk pengkodean biner), karena switch digunakan dalam kode tingkat yang lebih tinggi, kesalahan pendekatan ini mulai lebih besar daripada fleksibilitasnya.

Saya sepenuhnya setuju dan menyambut kesempatan untuk menggunakan sakelar tanpa perilaku default:


 switch(ternaryBool) { case TRUE, FALSE -> System.out.println("Bool was sane"); // in colon-form, if `ternaryBool` is `TRUE` or `FALSE`, // we would see both messages; in arrow-form, only one // branch is executed default -> System.out.println("Bool was insane"); } 

Penting untuk mengetahui bahwa ini tidak ada hubungannya dengan apakah Anda menggunakan sakelar sebagai ekspresi atau pernyataan. Faktor penentu di sini adalah panah terhadap usus besar.


Blok Operator


Seperti dalam kasus lambdas, panah dapat menunjuk ke salah satu operator (seperti di atas) atau blok yang disorot dengan kurung kurawal:


 boolean result = switch(Bool.random()) { case TRUE -> { System.out.println("Bool true"); // return with `break`, not `return` break true; } case FALSE -> { System.out.println("Bool false"); break false; } case FILE_NOT_FOUND -> { var ex = new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); throw ex; } default -> { var ex = new IllegalArgumentException("Seriously?!"); throw ex; } }; 

Blok yang harus dibuat untuk operator multi-line memiliki keuntungan tambahan (yang tidak diperlukan saat menggunakan titik dua), yang berarti bahwa untuk menggunakan nama variabel yang sama di cabang yang berbeda, switch tidak memerlukan pemrosesan khusus.


Jika Anda merasa tidak biasa keluar dari blok menggunakan break daripada return , maka jangan khawatir - ini juga membingungkan saya dan tampak aneh. Tapi kemudian saya memikirkannya dan sampai pada kesimpulan bahwa itu masuk akal, karena ia mempertahankan gaya lama dari switch , yang menggunakan break tanpa nilai.


Pelajari lebih lanjut tentang pergantian pernyataan


Dan last but not least, spesifikasi menggunakan switch sebagai ekspresi:


  • banyak ekspresi;
  • pengembalian awal ( return awal);
  • cakupan semua nilai.

Harap perhatikan bahwa tidak masalah bentuk apa yang digunakan!


Beragam ekspresi


Beralih ekspresi adalah beberapa ekspresi. Ini berarti bahwa mereka tidak memiliki tipe mereka sendiri, tetapi dapat menjadi salah satu dari beberapa tipe. Paling sering, ekspresi lambda digunakan sebagai ekspresi seperti: s -> s + " " , mungkin Function<String, String> , tetapi mungkin juga Function<Serializable, Object> atau UnaryOperator<String> .


Menggunakan ekspresi saklar, suatu tipe ditentukan oleh interaksi antara di mana saklar digunakan dan tipe cabangnya. Jika ekspresi sakelar ditetapkan ke variabel yang diketik, diteruskan sebagai argumen, atau digunakan dalam konteks di mana tipe persisnya diketahui (ini disebut tipe target), maka semua cabangnya harus cocok dengan tipe itu. Inilah yang telah kami lakukan sejauh ini:


 String result = switch (ternaryBool) { case TRUE, FALSE -> "sane"; default -> "insane"; }; 

Sebagai hasilnya, switch ditugaskan ke variabel result dari tipe String . Oleh karena itu, String adalah tipe target, dan semua cabang harus mengembalikan hasil dari tipe String .


Hal yang sama terjadi di sini:


 Serializable serializableMessage = switch (bool) { case TRUE, FALSE -> "sane"; // note that we don't throw the exception! // but it's `Serializable`, so it matches the target type default -> new IllegalArgumentException("insane"); }; 

Apa yang akan terjadi sekarang?


 // compiler infers super type of `String` and // `IllegalArgumentException` ~> `Serializable` var serializableMessage = switch (bool) { case TRUE, FALSE -> "sane"; // note that we don't throw the exception! default -> new IllegalArgumentException("insane"); }; 

(Untuk penggunaan jenis var, baca di artikel terakhir kami 26 rekomendasi untuk menggunakan jenis var di Jawa - catatan oleh penerjemah)


Jika tipe target tidak diketahui, karena fakta bahwa kami menggunakan var, tipe tersebut dihitung dengan menemukan supertipe paling spesifik dari tipe yang dibuat oleh cabang.


Kembali lebih awal


Konsekuensi dari perbedaan antara ekspresi dan switch adalah bahwa Anda dapat menggunakan return untuk keluar dari switch :


 public String sanity(Bool ternaryBool) { switch (ternaryBool) { // `return` is only possible from block case TRUE, FALSE -> { return "sane"; } default -> { return "This is ridiculous!"; } }; } 

... Anda tidak dapat menggunakan return di dalam ekspresi ...


 public String sanity(Bool ternaryBool) { String result = switch (ternaryBool) { // this does not compile - error: // "return outside of enclosing switch expression" case TRUE, FALSE -> { return "sane"; } default -> { return "This is ridiculous!"; } }; } 

Ini masuk akal apakah Anda menggunakan panah atau titik dua.


Mencakup semua opsi


Jika Anda menggunakan switch sebagai operator, maka tidak masalah jika semua opsi dicakup atau tidak. Tentu saja, Anda dapat melewatkan case secara tidak sengaja, dan kode tidak akan berfungsi dengan benar, tetapi kompiler tidak peduli - Anda, IDE Anda dan alat analisis kode Anda akan dibiarkan begitu saja.


Berpindah ekspresi memperparah masalah ini. Di mana harus beralih pergi jika label yang diinginkan hilang? Satu-satunya jawaban yang dapat diberikan Java adalah mengembalikan null untuk tipe referensi dan nilai default untuk primitif. Ini akan menyebabkan banyak kesalahan dalam kode utama.


Untuk mencegah hasil seperti itu, kompiler dapat membantu Anda. Untuk pernyataan pergantian, kompiler akan bersikeras bahwa semua opsi yang mungkin tercakup. Mari kita lihat contoh yang mungkin menyebabkan kesalahan kompilasi:


 // compile error: // "the switch expression does not cover all possible input values" boolean result = switch (ternaryBool) { case TRUE -> true; // no case for `FALSE` case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); }; 

Solusi berikut ini menarik: menambahkan cabang default pasti akan memperbaiki kesalahan, tetapi ini bukan satu-satunya solusi - Anda masih dapat menambahkan case untuk FALSE .


 // compiles without `default` branch because // all cases for `ternaryBool` are covered boolean result = switch (ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); }; 

Ya, kompiler akhirnya akan dapat menentukan apakah semua nilai enum tertutup (apakah semua opsi sudah habis), dan tidak menetapkan nilai default yang tidak berguna! Mari kita duduk sejenak dalam ucapan terima kasih sunyi.


Meskipun, ini masih menimbulkan satu pertanyaan. Bagaimana jika seseorang mengambil dan mengubah Bool gila menjadi Boolean angka empat dengan menambahkan nilai keempat? Jika Anda mengkompilasi ulang ekspresi sakelar untuk Bool yang diperluas, Anda akan mendapatkan kesalahan kompilasi (ekspresi tidak lagi lengkap). Tanpa kompilasi ulang, ini akan berubah menjadi masalah run-time. Untuk menangkap masalah ini, kompiler pergi ke cabang default , yang berperilaku sama dengan yang kami gunakan sejauh ini, melempar pengecualian.


Di Java 12, merentang semua nilai tanpa cabang default hanya berfungsi untuk enum , tetapi ketika switch menjadi lebih kuat di versi Java yang akan datang, ia juga bisa bekerja dengan tipe arbitrer. Jika label case tidak hanya dapat memverifikasi kesetaraan, tetapi juga membuat perbandingan (misalnya, _ <5 -> ...) - ini akan mencakup semua opsi untuk jenis numerik.


Berpikir


Kami belajar dari artikel bahwa Java 12 mengubah switch menjadi ekspresi, memberikannya fitur baru:


  • sekarang satu case dapat sesuai dengan beberapa label;
  • Bentuk tanda panah baru case … -> … mengikuti sintaks ekspresi lambda:
    • operator atau blok tunggal diperbolehkan;
    • melewati ke case berikutnya dicegah;
  • sekarang seluruh ekspresi dievaluasi sebagai nilai, yang kemudian dapat ditugaskan ke variabel atau diteruskan sebagai bagian dari pernyataan yang lebih besar;
  • multi ekspresi: jika tipe target diketahui, maka semua cabang harus sesuai dengannya. Kalau tidak, tipe tertentu didefinisikan yang cocok dengan semua cabang;
  • break dapat mengembalikan nilai dari blok;
  • untuk ekspresi switch menggunakan enum , kompiler memeriksa ruang lingkup semua nilainya. Jika default ada, cabang ditambahkan yang melempar pengecualian.

Di mana itu akan membawa kita? Pertama, karena ini bukan versi terakhir dari switch , Anda masih punya waktu untuk meninggalkan umpan balik pada milis Amber jika Anda tidak setuju dengan sesuatu.


Kemudian, dengan asumsi saklar tetap seperti saat ini, saya pikir bentuk panah akan menjadi opsi default baru. Tanpa melalui bagian ke case berikutnya dan dengan ekspresi lambda singkat (sangat alami untuk memiliki kasus dan satu pernyataan dalam satu baris), switch terlihat jauh lebih kompak dan tidak mempengaruhi keterbacaan kode. Saya yakin bahwa saya hanya akan menggunakan titik dua jika saya perlu melewati bagian ini.


Apa yang kamu pikirkan Puas dengan apa yang terjadi?

Source: https://habr.com/ru/post/id443464/


All Articles