Java: hal-hal yang mungkin tampak aneh bagi pengembang yang berpengalaman

Waktu yang baik hari ini!

Artikel ini ditulis setelah publikasi “Things You [Mungkin] Tidak Tahu Tentang Java” oleh penulis lain, yang akan saya klasifikasikan sebagai “untuk pemula”. Membaca dan berkomentar tentang itu, saya menyadari bahwa ada beberapa hal yang cukup menarik yang saya pelajari, sudah pemrograman di java selama lebih dari satu tahun. Mungkin hal-hal ini akan terasa aneh bagi orang lain.

Fakta-fakta yang, dari sudut pandang saya, mungkin berguna bagi pemula, saya hapus dalam "spoiler". Beberapa hal mungkin masih menarik untuk yang lebih berpengalaman. Sebagai contoh, saya sendiri tidak tahu sampai saat menulis artikel bahwa Boolean.hashCode (true) == 1231 dan Boolean.hashCode (false) == 1237.

untuk pemula
  • Boolean.hashCode (true) == 1231
  • Boolean.hashCode (false) == 1237
  • Float.hashCode (value) == Float.floatToIntBits (value)
  • Double.hashCode (value) - xor dari halfwords 32-bit pertama dan kedua Double.doubleToLongBits (value)

Object.hashCode () tidak lagi menjadi alamat suatu objek dalam memori


Penafian: Ini adalah detail jvm dari Oracle (HotSpot).

Sekali waktu ini begitu.
Dari jdk1.2.1 / docs / api / java / lang / Object.html # hashCode ():
Sebanyak yang cukup praktis, metode hashCode yang didefinisikan oleh class Object mengembalikan integer yang berbeda untuk objek yang berbeda. (Ini biasanya diterapkan dengan mengubah alamat internal objek menjadi integer, tetapi teknik implementasi ini tidak diperlukan oleh bahasa pemrograman JavaTM.)

Kemudian mereka menolaknya. Ini adalah apa yang dikatakan javadoc untuk jdk 12 .

vladimir_dolzhenko menyarankan agar perilaku lama dapat dikembalikan menggunakan -XX: hashCode = 4. Dan perubahan perilaku itu sendiri hampir dari versi java 1.2.

Integer.valueOf (15) == Integer.valueOf (15); Integer.valueOf (128)! = Integer.valueOf (128)


Penafian: ini adalah bagian dari jls .

Jelas bahwa ketika membandingkan dua pembungkus dengan Operator == (! =), Tidak ada autoboxing terjadi. Secara umum, ini adalah persamaan pertama yang membingungkan. Faktanya adalah bahwa untuk nilai integer i: -129 <i <128 Objek pembungkus integer di-cache. Oleh karena itu, untuk saya dari rentang ini, Integer.valueOf (i) tidak membuat objek baru setiap kali, tetapi mengembalikan yang sudah dibuat. Untuk saya yang tidak termasuk dalam kisaran ini, Integer.valueOf (i) selalu membuat objek baru. Oleh karena itu, jika Anda tidak memonitor secara cermat apa yang sebenarnya dan bagaimana tepatnya dibandingkan, Anda dapat menulis kode yang tampaknya berfungsi dan bahkan ditutupi dengan tes, tetapi pada saat yang sama mengandung "rake".

Dalam jvm dari Oracle (HotSpot), batas atas caching dapat diubah melalui properti "java.lang.Integer.IntegerCache.high" .

dalam beberapa kasus, nilai-nilai bidang statis akhir primitif atau string dari kelas lain diselesaikan pada waktu kompilasi


Kedengarannya membingungkan, dan pernyataannya agak panjang. Artinya adalah ini. Jika kita memiliki kelas yang mendefinisikan konstanta tipe atau string primitif sebagai bidang statis akhir dengan inisialisasi langsung,
class AnotherClass { public final static String CASE_1 = "case_1"; public final static String CASE_2 = "case_2"; } 

ketika digunakan di kelas lain,
 class TheClass { // ... public static int getCaseNumber(String caseName) { switch (caseName) { case AnotherClass.CASE_1: return 1; case AnotherClass.CASE_2: return 2; default: throw new IllegalArgumentException("value of the argument caseName does not belong to the allowed value set"); } } } 

nilai konstanta ini ("case_1", "case_2") diselesaikan pada waktu kompilasi. Dan mereka dimasukkan ke dalam kode sebagai nilai, dan bukan sebagai tautan. Yaitu, jika kita menggunakan konstanta seperti itu dari perpustakaan, dan kemudian kita mendapatkan versi baru dari perpustakaan di mana nilai-nilai konstanta telah berubah, kita harus mengkompilasi ulang proyek. Jika tidak, nilai konstanta lama dapat terus digunakan dalam kode.

Perilaku ini diamati di semua tempat di mana ekspresi konstan (misalnya, switch / case) harus digunakan, atau kompiler diperbolehkan untuk mengubah ekspresi menjadi konstan dan ia dapat melakukannya.

Bidang ini tidak dapat digunakan dalam ekspresi konstan segera setelah kami menghapus inisialisasi langsung dengan mentransfer inisialisasi ke blok statis.

untuk pemula

Dalam kondisi tertentu, pemulung tidak akan pernah lari.


Akibatnya, finalisasi () tidak akan diluncurkan. Oleh karena itu, Anda tidak boleh menulis kode yang bergantung pada fakta bahwa finalize () akan selalu berfungsi. Ya, dan jika objek masuk ke sampah sebelum akhir program, kemungkinan besar tidak akan dikumpulkan oleh kolektor.

Metode finalize () untuk objek tertentu dapat dipanggil sekali dan hanya sekali.


Pada finalize (), kita dapat membuat objek terlihat kembali, dan pemulung tidak akan "menghapus" kali ini. Ketika objek ini jatuh ke sampah lagi, itu akan "dikompilasi" tanpa memanggil finalize (). Jika pengecualian dilemparkan ke finalize () dan objek masih tidak terlihat oleh siapa pun, maka akan "dirakit". Finalisasi () tidak akan dipanggil lagi.

aliran tempat finalisasi () akan dipanggil tidak diketahui sebelumnya


Hanya dijamin bahwa utas ini akan bebas dari kunci yang terlihat oleh program utama.

keberadaan metode finalisasi () yang ditimpa pada objek memperlambat proses pengumpulan sampah


Apa yang ada di permukaan adalah kebutuhan untuk mengecek ketersediaan objek - sekali sebelum memanggil finalize (), sekali di salah satu dari pengumpulan pengumpulan sampah berikut.

sangat sulit untuk mengatasi kebuntuan dalam menyelesaikan ()


Dalam finalisasi non-sepele (), kunci mungkin diperlukan, yang, mengingat spesifik yang dijelaskan di atas, sangat sulit untuk di-debug.

Object.finalize () karena versi 9 java ditandai sebagai usang!


Yang tidak mengejutkan, mengingat spesifik yang dijelaskan di atas.

inisialisasi malas tunggal klasik: diperlukan penguncian ganda


Ada kesalahpahaman tentang topik ini bahwa pendekatan berikut (periksa idiom), yang terlihat sangat logis, selalu berhasil:
 public class UnsafeDCLFactory { private Singleton instance; public Singleton get() { if (instance == null) { // read 1, check 1 synchronized (this) { if (instance == null) { // read 2, check 2 instance = new Singleton(); } } } return instance; // read 3 } } 

Kami melihat apakah objek dibuat (baca 1, centang 1). Jika demikian, kembalikan. Jika tidak, maka atur kunci, pastikan objek tidak dibuat, buat objek (kunci dilepas), dan kembalikan objek.

Pendekatan tidak berfungsi karena alasan berikut. (baca 1, periksa 1) dan (baca 3) tidak sinkron. Menurut konsep model memori java, perubahan yang dilakukan pada utas lain mungkin tidak dapat dilihat oleh utas kami hingga kami menyinkronkan. Terima kasih mk2 untuk komentarnya, berikut adalah deskripsi masalahnya yang benar:
Ya, read1 dan read3 tidak disinkronkan, tetapi masalahnya bukan pada utas lainnya. Dan fakta bahwa bacaan yang tidak disinkronkan dapat disusun ulang, mis. read1! = null, tetapi read3 == null. Dan pada saat yang sama, karena "instance = new Singleton ();" kita bisa mendapatkan referensi ke objek sebelum benar-benar dibangun, dan ini benar-benar masalah sinkronisasi dengan utas lain, tetapi bukan read1 dan read3, tetapi read3 dan akses untuk anggota contoh.
Itu diperlakukan baik dengan menambahkan sinkronisasi selama membaca, atau dengan menandai variabel di mana tautan ke singleton hidup, volatile. (Solusi dengan volatile hanya berfungsi dengan java 5+. Sebelum itu, java memiliki model memori dengan ketidakpastian dalam situasi ini.) Ini adalah versi yang berfungsi (dengan optimasi tambahan - variabel lokal `res` ditambahkan untuk mengurangi jumlah bacaan dari bidang volatile).
 public class SafeLocalDCLFactory { private volatile Singleton instance; public Singleton getInstance() { Singleton res = instance; // read 1 if (res == null) { // check 1 synchronized (this) { res = instance; // read 2 if (res == null) { // check 2 res = new Singleton(); instance = res; } } } return res; } } 

Kode diambil dari sini , dari situs Alexei Shipilev. Rincian lebih lanjut tentang masalah ini dapat ditemukan di sana.

"Idiom inisialisasi sesuai permintaan" - inisialisasi "malas" yang sangat indah dari singleton


java menginisialisasi kelas (objek Kelas) hanya seperlunya dan, tentu saja, hanya sekali. Dan Anda bisa memanfaatkan ini! Mekanisme idiom pemegang inisialisasi atas permintaan tidak hanya itu. (Kode ini dari sini .)
 public class Something { private Something() {} private static class LazyHolder { static final Something INSTANCE = new Something(); } public static Something getInstance() { return LazyHolder.INSTANCE; } } 

Kelas LazyHolder hanya akan diinisialisasi saat Something.getInstance () pertama kali dipanggil. Jvm akan memastikan bahwa ini hanya terjadi sekali dan, apalagi, sangat efisien - jika kelas sudah diinisialisasi, tidak akan ada overhead. Dengan demikian, LazyHolder.INSTANCE juga akan diinisialisasi sekali, "malas" dan thread-safe.
sepotong spec tentang overhead
Jika prosedur inisialisasi ini selesai secara normal dan objek Class sepenuhnya diinisialisasi dan siap untuk digunakan, maka permohonan prosedur inisialisasi tidak lagi diperlukan dan dapat dihilangkan dari kode - misalnya, dengan menambalnya atau membuat ulang kode .
Sumber

Secara umum, lajang tidak dianggap praktik terbaik.

Materi belum berakhir. Jadi, jika tangan "mencapai" dan apa yang sudah ditulis akan diminati, saya akan menulis lebih banyak tentang topik ini.

Terima kasih atas komentar yang membangun. Beberapa tempat dalam artikel diperluas berkat sergey-gornostaev , vladimir_dolzhenko , OlehKurpiak , mk2 .

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


All Articles