Saya baru-baru ini menemukan situasi yang menggantikan Object dengan var di program Java 10 melempar pengecualian saat runtime. Saya menjadi tertarik pada berapa banyak cara berbeda untuk mencapai efek ini, dan saya membahas masalah ini kepada masyarakat:
Ternyata Anda dapat mencapai efek dengan berbagai cara. Meskipun semuanya agak rumit, menarik untuk mengingat berbagai seluk-beluk bahasa dengan contoh tugas seperti itu. Mari kita lihat metode apa yang ditemukan.
Anggota
Di antara responden ada banyak yang terkenal dan tidak terlalu banyak orang. Ini adalah Sergey bsideup Egorov, karyawan penting, pembicara, salah satu pencipta Testcontainers. Ini adalah Victor Polishchuk , terkenal karena laporan tentang perusahaan berdarah. Juga dicatat Nikita Artyushov dari Google; Dmitry Mikhailov dan Maccimo . Tapi saya sangat senang dengan kedatangan Wouter Coekaerts . Dia dikenal karena artikelnya tahun lalu , di mana dia berjalan melalui sistem tipe Java dan mengatakan betapa putus asa itu rusak. Beberapa artikel ini jbaruch dan saya bahkan digunakan dalam rilis keempat Java Puzzlers .
Tugas dan solusi
Jadi, esensi dari tugas kita adalah ini: ada program Java di mana ada deklarasi variabel dari bentuk Object x = ...
(standar jujur java.lang.Object
, tanpa subtitusi jenis). Program mengkompilasi, menjalankan, dan mencetak sesuatu seperti "Oke." Kami mengganti Object
dengan var
, membutuhkan inferensi tipe otomatis, setelah itu program terus mengkompilasi, tetapi crash saat diluncurkan dengan pengecualian.
Solusi dapat secara kasar dibagi menjadi dua kelompok. Yang pertama, setelah mengganti dengan var, variabel menjadi primitif (yaitu, awalnya autoboxing). Tipe kedua tetap objek, tetapi lebih spesifik daripada Object
. Di sini Anda dapat menyorot subkelompok menarik yang menggunakan obat generik.
Tinju
Bagaimana cara membedakan suatu objek dari primitif? Ada banyak cara berbeda. Cara termudah adalah memeriksa identitas. Solusi ini diusulkan oleh Nikita:
Object x = 1000; if (x == new Integer(1000)) throw new Error(); System.out.println("Ok");
Ketika x
adalah objek, tentu saja tidak bisa sama dengan referensi ke objek new Integer(1000)
. Dan jika itu adalah primitif, maka menurut aturan bahasa new Integer(1000)
segera terungkap menjadi primitif juga, dan angka dibandingkan sebagai primitif.
Cara lain adalah metode kelebihan beban. Anda dapat menulis sendiri, tetapi Sergey datang dengan opsi yang lebih elegan: gunakan perpustakaan standar. Metode List.remove
, yang kelebihan beban dan dapat menghapus salah satu elemen dengan indeks jika primitif dilewati, atau elemen dengan nilai jika objek dilewatkan. Ini berulang kali menyebabkan bug dalam program nyata jika Anda menggunakan List<Integer>
. Untuk tugas kami, solusinya mungkin terlihat seperti ini:
Object x = 1000; List<?> list = new ArrayList<>(); list.remove(x); System.out.println("Ok");
Sekarang kami mencoba untuk menghapus elemen yang tidak ada 1000 dari daftar kosong, ini hanya tindakan yang tidak berguna. Tetapi jika kita mengganti Object
dengan var
, kita akan memanggil metode lain yang menghapus elemen dengan indeks 1000. Dan ini sudah mengarah ke IndexOutOfBoundsException
.
Metode ketiga adalah operator konversi tipe. Kita dapat berhasil mengkonversi primitif lain ke tipe primitif, tetapi suatu objek dikonversi hanya jika ada pembungkus di atas tipe yang sama dengan yang akan kita konversi (maka anbox akan terjadi). Sebenarnya, kita membutuhkan efek sebaliknya: pengecualian dalam kasus primitif, dan bukan dalam kasus objek, tetapi menggunakan try-catch ini mudah dicapai, yang digunakan Viktor:
Object x = 40; try { throw new Error("Oops :" + (char)x); } catch (ClassCastException e) { System.out.println("Ok"); }
Di sini, ClassCastException
adalah perilaku yang diharapkan, kemudian program keluar secara normal. Tetapi setelah menggunakan var
pengecualian ini menghilang, dan kami melempar sesuatu yang lain. Saya ingin tahu apakah ini terinspirasi oleh kode asli dari perusahaan berdarah? ..
Opsi konversi tipe lain diusulkan oleh Wouter. Anda dapat menggunakan logika aneh dari operator ?:
. Benar, kodenya hanya memberikan hasil yang berbeda, jadi Anda harus memodifikasinya sehingga ada pengecualian. Jadi, menurut saya, cukup elegan:
Object x = 1.0; System.out.println(String.valueOf(false ? x : 100000000000L).substring(12) + "Ok");
Perbedaan antara metode ini adalah kita tidak menggunakan nilai x
secara langsung, tetapi tipe x
memengaruhi tipe ekspresi false ? x : 100000000000L
false ? x : 100000000000L
. Jika x
adalah Object
, maka jenis seluruh ekspresi adalah Object
, dan kemudian kita hanya memiliki tinju, String.valueOf()
akan String.valueOf()
string sebesar 100000000000
, yang substring(12)
adalah string kosong. Jika Anda menggunakan var
, maka tipe x
menjadi double
, dan karena itu tipenya false ? x : 100000000000L
false ? x : 100000000000L
juga double
, yaitu 100000000000L
akan berubah menjadi 1.0E11
, di mana ia jauh lebih sedikit dari 12 karakter, jadi memanggil hasil substring
dalam StringIndexOutOfBoundsException
.
Akhirnya, kami mengambil keuntungan dari fakta bahwa suatu variabel sebenarnya dapat diubah setelah pembuatan. Dan dalam variabel objek, tidak seperti primitif, Anda dapat meletakkan null
. Menempatkan null
dalam variabel itu mudah, ada banyak cara. Namun di sini, Wouter juga mengambil pendekatan kreatif menggunakan metode Integer.getInteger
konyol:
Object x = 1; x = Integer.getInteger("moo"); System.out.println("Ok");
Tidak semua orang tahu bahwa metode ini membaca properti sistem yang disebut moo
dan, jika ada, mencoba mengubahnya menjadi angka, jika tidak maka akan mengembalikan null
. Jika tidak ada properti, kami diam-diam menetapkan null
ke objek, tetapi jatuh dari NullPointerException
ketika mencoba untuk menetapkannya ke primitif (anboxing otomatis terjadi di sana). Tentu saja bisa lebih mudah. Versi kasar x = null;
itu tidak merangkak - tidak dikompilasi, tetapi kompiler akan menelannya sekarang:
Object x = 1; x = (Integer)null; System.out.println("Ok");
Jenis objek
Misalkan Anda tidak bisa lagi bermain dengan primitif. Apa lagi yang bisa Anda pikirkan?
Pertama, metode kelebihan beban paling sederhana yang diusulkan oleh Dmitry:
public static void main(String[] args) { Object x = "Ok"; sayWhat(x); } static void sayWhat(Object x) { System.out.println(x); } static void sayWhat(String x) { throw new Error(); }
Menghubungkan metode kelebihan beban di Jawa terjadi secara statis, pada tahap kompilasi. Metode sayWhat sayWhat(Object)
, tetapi jika kita menyimpulkan tipe x
secara otomatis, maka String
sayWhat(String)
, dan oleh karena itu metode sayWhat(String)
lebih spesifik akan dihubungkan.
Cara lain untuk membuat panggilan ambigu di Jawa adalah dengan menggunakan argumen variabel (varargs). Wouter mengingat ini lagi:
Object x = new Object[] {}; Arrays.asList(x).get(0); System.out.println("Ok");
Ketika tipe variabel adalah Object
, kompiler berpikir itu adalah argumen variabel dan membungkus array dalam array lain dari satu elemen, jadi get()
berhasil memenuhi. Jika Anda menggunakan var
, tipe Object[]
ditampilkan, dan tidak akan ada pembungkus tambahan. Dengan cara ini kita mendapatkan daftar kosong, dan panggilan get()
akan gagal.
Maccimo memilih hardcore: ia memutuskan untuk memanggil println
melalui API MethodHandle:
Object x = "Ok"; MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual( PrintStream.class, "println", MethodType.methodType(void.class, Object.class)); mh.invokeExact(System.out, x);
Metode invokeExact
dan beberapa metode lain dari java.lang.invoke
memiliki apa yang disebut "tanda tangan polimorfik". Meskipun dideklarasikan sebagai metode vararg invokeExact(Object... args)
, itu tidak terjadi dalam pengemasan array standar. Sebagai gantinya, tanda tangan dihasilkan dalam bytecode yang cocok dengan jenis argumen yang sebenarnya dilewati. Metode invokeExact
dirancang untuk pemanggilan metode penanganan yang sangat cepat, sehingga metode ini tidak melakukan transformasi argumen standar seperti casting atau tinju. Jenis metode pegangan diharapkan sesuai persis dengan tanda tangan panggilan. Ini dicentang pada saat runtime, dan seperti dalam kasus var
pertandingan rusak, kita mendapatkan WrongMethodTypeException
.
Generik
Tentu saja, parameterisasi jenis dapat menambahkan binar ke tugas apa pun di Jawa. Dmitry membawa solusi yang mirip dengan kode yang awalnya saya temui. Keputusan Dmitry sangat jelas, jadi saya akan menunjukkan:
public static void main(String[] args) { Object x = foo(new StringBuilder()); System.out.println(x); } static <T> T foo(T x) { return (T)"Ok"; }
Tipe T
adalah output sebagai StringBuilder
, tetapi dalam kode ini kompiler tidak diharuskan untuk memasukkan pemeriksaan tipe pada dial-peer ke dalam bytecode. Sudah cukup baginya bahwa StringBuilder
dapat ditugaskan ke Object
, yang berarti semuanya baik-baik saja. Tidak ada yang menentang fakta bahwa metode dengan nilai pengembalian StringBuilder
benar-benar mengembalikan string jika Anda menetapkan hasilnya ke variabel tipe Object
toh. Kompiler dengan jujur memperingatkan bahwa Anda memiliki pemain yang tidak diperiksa, yang berarti bahwa ia mencuci tangannya. Namun, ketika mengganti x
dengan var
tipe x
juga ditampilkan sebagai StringBuilder
, dan itu tidak mungkin lagi tanpa pengecekan tipe, karena menugaskan sesuatu yang lain ke variabel tipe StringBuilder
tidak berharga. Akibatnya, setelah berubah menjadi var
program dengan aman crash dengan ClassCastException
.
Wouter menyarankan varian dari solusi ini menggunakan metode standar:
Object o = ((List<String>)(List)List.of(1)).get(0); System.out.println("Ok");
Akhirnya, opsi lain dari Wouter:
Object x = ""; TreeSet<?> set = Stream.of(x) .collect(toCollection(() -> new TreeSet<>((a, b) -> 0))); if (set.contains(1)) { System.out.println("Ok"); }
Di sini, tergantung pada penggunaan var
atau Object
tipe aliran ditampilkan sebagai Stream<Object>
atau sebagai Stream<String>
. Dengan demikian, tipe TreeSet
dan tipe lambda pembanding ditampilkan. Dalam kasus var
, string harus datang ke lambda, jadi ketika membuat representasi runtime lambda, konversi tipe secara otomatis dimasukkan, yang memberikan ClassCastException
ketika mencoba untuk melemparkan unit ke string.
Secara umum, hasilnya sangat membosankan. Jika Anda dapat menemukan metode yang berbeda secara mendasar untuk memecah var
, kemudian tulis di komentar.