Perilaku tidak terdefinisi di Jawa

14

Saya membaca pertanyaan ini di SO yang membahas beberapa perilaku umum yang tidak terdefinisi dalam C ++, dan saya bertanya-tanya: apakah Java juga memiliki perilaku yang tidak terdefinisi?

Jika itu masalahnya, lalu apa penyebab umum dari perilaku tidak terdefinisi di Jawa?

Jika tidak, maka fitur Java mana yang membuatnya bebas dari perilaku seperti itu dan mengapa versi C dan C ++ terbaru belum diimplementasikan dengan properti ini?

Delapan
sumber
4
Java didefinisikan dengan sangat kaku. Periksa Spesifikasi Bahasa Jawa.
4
@ user1249, "perilaku tidak terdefinisi" sebenarnya juga didefinisikan dengan sangat kaku.
Pacerier
Kemungkinan sama pada SO: stackoverflow.com/questions/376338/…
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
Apa yang dikatakan Java tentang ketika Anda melanggar "Kontrak"? Seperti yang terjadi ketika Anda overload. Sama dengan tidak bisa .hashCode? docs.oracle.com/javase/7/docs/api/java/lang/… Apakah itu bahasa sehari-hari tidak terdefinisi, tetapi tidak secara teknis dengan cara yang sama seperti C ++?
Mooing Duck

Jawaban:

18

Di Java, Anda dapat mempertimbangkan perilaku program yang disinkronkan secara salah, tidak terdefinisi.

Java 7 JLS menggunakan kata "tidak terdefinisi" sekali, dalam 17.4.8. Eksekusi dan Persyaratan Kausalitas :

Kami menggunakan f|duntuk menunjukkan fungsi yang diberikan dengan membatasi domain fke d. Untuk semua xdi d,, f|d(x) = f(x)dan untuk semua yang xtidak di d, f|d(x)tidak ditentukan ...

Dokumentasi Java API menetapkan beberapa kasus saat hasil tidak ditentukan - misalnya, dalam Tanggal konstruktor (tidak digunakan) (tahun int, bulan int, hari int) :

Hasilnya tidak ditentukan jika argumen yang diberikan di luar batas ...

Javadocs untuk ExecutorService.invokeAll (Koleksi) menyatakan:

Hasil dari metode ini tidak ditentukan jika koleksi yang diberikan dimodifikasi saat operasi ini sedang berlangsung ...

Jenis perilaku "tidak terdefinisi" yang kurang formal dapat ditemukan misalnya di ConcurrentModificationException , di mana dokumen API menggunakan istilah "upaya terbaik":

Perhatikan bahwa perilaku gagal-cepat tidak dapat dijamin karena, secara umum, tidak mungkin untuk membuat jaminan keras di hadapan modifikasi bersamaan yang tidak disinkronkan. Operasi Fail-cepat membuang ConcurrentModificationExceptionpada upaya terbaik dasar. Oleh karena itu, akan salah untuk menulis sebuah program yang bergantung pada pengecualian ini untuk kebenarannya ...


Lampiran

Salah satu komentar pertanyaan merujuk pada sebuah artikel oleh Eric Lippert yang memberikan pengantar yang bermanfaat ke dalam hal-hal topik: Perilaku yang ditentukan implementasi .

Saya merekomendasikan artikel ini untuk alasan bahasa-agnostik, meskipun perlu diingat bahwa penulis menargetkan C #, bukan Java.

Secara tradisional kami mengatakan bahwa idiom bahasa pemrograman memiliki perilaku yang tidak terdefinisi jika penggunaan idiom itu dapat memiliki efek apa pun; itu bisa bekerja seperti yang Anda harapkan atau bisa menghapus hard disk Anda atau crash mesin Anda. Selain itu, pembuat kompiler tidak berkewajiban untuk memperingatkan Anda tentang perilaku yang tidak terdefinisi. (Dan pada kenyataannya, ada beberapa bahasa di mana program yang menggunakan idiom "perilaku tidak terdefinisi" diizinkan oleh spesifikasi bahasa untuk membuat crash kompilator!) ...

Sebaliknya, idiom yang memiliki perilaku implementasi-didefinisikan adalah perilaku di mana penulis kompiler memiliki beberapa pilihan tentang bagaimana menerapkan fitur, dan harus memilih satu. Seperti namanya, perilaku implementasi-didefinisikan setidaknya didefinisikan. Misalnya, C # mengizinkan implementasi untuk melempar pengecualian atau menghasilkan nilai ketika divisi integer meluap, tetapi implementasi harus memilih satu. Itu tidak dapat menghapus hard disk Anda ...

Apa saja faktor-faktor yang menyebabkan komite desain bahasa untuk meninggalkan idiom bahasa tertentu sebagai perilaku yang tidak terdefinisi atau implementasi?

Faktor utama pertama adalah: apakah ada dua implementasi bahasa yang ada di pasar yang tidak setuju dengan perilaku program tertentu? ...

Faktor utama berikutnya adalah: apakah fitur secara alami menghadirkan banyak kemungkinan berbeda untuk implementasi, beberapa di antaranya jelas lebih baik daripada yang lain? ...

Faktor ketiga adalah: apakah fitur itu begitu rumit sehingga rincian rincian perilaku tepatnya akan sulit atau mahal untuk ditentukan? ...

Faktor keempat adalah: apakah fitur memaksakan beban tinggi pada kompiler untuk dianalisis? ...

Faktor kelima adalah: apakah fitur memberlakukan beban tinggi pada lingkungan runtime? ...

Faktor keenam adalah: apakah membuat perilaku yang didefinisikan menghalangi beberapa optimasi utama? ...

Itu hanya beberapa faktor yang muncul di pikiran; tentu saja ada banyak, banyak faktor lain yang diperdebatkan komite desain bahasa sebelum membuat fitur "implementasi didefinisikan" atau "tidak terdefinisi".

Di atas hanya liputan yang sangat singkat; artikel lengkap berisi penjelasan dan contoh untuk poin-poin yang disebutkan dalam kutipan ini; itu sangat berharga untuk dibaca. Misalnya, perincian yang diberikan untuk "faktor keenam" dapat memberikan satu wawasan tentang motivasi untuk banyak pernyataan dalam Java Memory Model ( JSR 133 ), membantu untuk memahami mengapa beberapa optimasi diizinkan, mengarah pada perilaku yang tidak ditentukan sementara yang lain dilarang, yang mengarah ke keterbatasan seperti yang terjadi sebelum dan persyaratan kausalitas .

Tidak ada satu pun dari materi artikel yang baru bagi saya tetapi saya akan terkutuk jika saya pernah melihatnya disajikan dengan cara yang begitu elegan, singkat dan mudah dimengerti. Luar biasa.

agas
sumber
Saya akan menambahkan bahwa JMM! = Perangkat keras yang mendasari dan hasil akhir dari program eksekusi berkaitan dengan konkurensi dapat bervariasi dari mengatakan WinIntel vs Solaris
Martijn Verburg
2
@ MartijnVerburg itu poin yang cukup bagus. Satu-satunya alasan mengapa saya ragu untuk menandainya sebagai "tidak terdefinisi" adalah bahwa model memori memiliki kendala seperti yang terjadi sebelumnya dan hubungan sebab - akibat dalam pelaksanaan program yang disinkronkan dengan benar
nyamuk
Benar, spek menentukan bagaimana seharusnya berperilaku di bawah JMM, bagaimanapun, Intel et al tidak selalu setuju ;-)
Martijn Verburg
@ MartijnVerburg Saya pikir poin utama JMM adalah untuk mencegah kebocoran yang berlebihan dari pembuat prosesor yang "tidak setuju". Sejauh yang saya mengerti Java sebelum 5.0 memiliki sakit kepala semacam ini dengan DEC Alpha, ketika penulisan spekulatif yang dilakukan di bawah tenda dapat bocor ke dalam program seperti "kehabisan udara" - karenanya, persyaratan kausalitas masuk ke JSR 133 (JMM)
gnat
9
@MartinVerburg - ini adalah pekerjaan pelaksana JVM untuk memastikan bahwa JVM berperilaku sesuai dengan spesifikasi JLS / JMM pada platform perangkat keras yang didukung. Jika perangkat keras yang berbeda berperilaku berbeda, itu adalah tugas pelaksana JVM untuk menghadapinya ... dan membuatnya berfungsi.
Stephen C
10

Dari atas kepala saya, saya tidak berpikir ada perilaku yang tidak terdefinisi di Jawa, setidaknya tidak dalam arti yang sama seperti di C ++.

Alasan untuk ini adalah bahwa ada filosofi yang berbeda di belakang Jawa daripada di belakang C ++. Tujuan utama desain Java adalah untuk memungkinkan program berjalan tidak berubah di seluruh platform, demikian juga spesifikasi mendefinisikan semuanya dengan sangat eksplisit.

Sebaliknya, tujuan desain inti dari C dan C ++ adalah efisiensi: seharusnya tidak ada fitur (termasuk kemandirian platform) yang memerlukan biaya kinerja bahkan jika Anda tidak membutuhkannya. Untuk tujuan ini, spesifikasi sengaja tidak mendefinisikan beberapa perilaku karena mendefinisikan mereka akan menyebabkan kerja ekstra pada beberapa platform dan dengan demikian mengurangi kinerja bahkan untuk orang-orang yang menulis program khusus untuk satu platform dan menyadari semua keanehannya.

Bahkan ada contoh di mana Java dipaksa untuk secara surut memperkenalkan bentuk terbatas dari perilaku tidak terdefinisi untuk alasan itu: kata kunci tightfp diperkenalkan di Java 1.2 untuk memungkinkan perhitungan floating point untuk menyimpang dari mengikuti persis standar IEEE 754 seperti yang sebelumnya diminta oleh spec , karena melakukan itu memerlukan kerja ekstra dan membuat semua perhitungan floating-point lebih lambat pada beberapa CPU umum, sementara sebenarnya menghasilkan hasil yang lebih buruk dalam beberapa kasus.

Michael Borgwardt
sumber
2
Saya pikir penting untuk mencatat tujuan utama Jawa lainnya: keamanan dan isolasi. Saya pikir ini juga merupakan alasan kurangnya perilaku 'tidak terdefinisi' (seperti dalam C ++).
K.Steff
3
@ K.Steff: Hyper-modern C / C ++ sama sekali tidak cocok untuk apa pun yang terkait keamanan jarak jauh. Mengingat int x=-1; foo(); x<<=1;filsafat hiper-modern akan mendukung menulis ulang foosehingga setiap jalan yang tidak keluar harus terjangkau. Ini, jika fooadalah if (should_launch_missiles) { launch_missiles(); exit(1); }kompilator bisa (dan menurut beberapa orang harus) menyederhanakan yang untuk hanya launch_missiles(); exit(1);. UB tradisional adalah eksekusi kode acak, tetapi dulu terikat oleh hukum waktu dan hubungan sebab akibat. UB yang lebih baik tidak terikat oleh keduanya.
supercat
3

Java berusaha cukup keras untuk memusnahkan perilaku yang tidak terdefinisi, justru karena pelajaran dari bahasa sebelumnya. Sebagai contoh, variabel tingkat kelas diinisialisasi secara otomatis; variabel lokal tidak diinisialisasi otomatis karena alasan kinerja, tetapi ada analisis aliran data yang canggih untuk mencegah siapa pun menulis program yang dapat mendeteksi hal ini. Referensi bukan petunjuk, jadi referensi yang tidak valid tidak ada, dan dereferencing nullmenyebabkan pengecualian khusus.

Tentu saja masih ada beberapa perilaku yang tidak sepenuhnya ditentukan, dan Anda dapat menulis program yang tidak dapat diandalkan jika Anda berasumsi demikian. Misalnya, jika Anda beralih ke yang normal (tidak diurutkan) Set, bahasa menjamin bahwa Anda akan melihat setiap elemen tepat satu kali, tetapi tidak dalam urutan yang Anda akan melihatnya. Urutan mungkin sama pada proses yang berurutan, atau mungkin berubah; atau mungkin tetap sama selama tidak ada alokasi lain yang terjadi, atau selama Anda tidak memperbarui JDK Anda, dll. Hampir tidak mungkin untuk menghilangkan semua efek seperti itu; misalnya, Anda harus secara eksplisit memesan atau mengacak semua operasi Koleksi, dan itu sama sekali tidak sebanding dengan tambahan kecil un-undefined-ness.

Kilian Foth
sumber
Referensi adalah petunjuk dengan nama lain
curiousguy
@curiousguy - "referensi" umumnya dianggap tidak mengizinkan penggunaan manipulasi aritmatika dari nilai numerik mereka, yang sering diperbolehkan untuk "pointer". Karena itu yang pertama adalah konstruksi yang lebih aman daripada yang terakhir; dikombinasikan dengan sistem manajemen memori yang tidak memungkinkan penyimpanan objek untuk digunakan kembali sementara referensi yang valid untuk itu ada, referensi mencegah kesalahan penggunaan memori. Pointer tidak dapat melakukannya, bahkan ketika manajemen memori yang tepat digunakan.
Jules
@ Jules Maka itu masalah terminologi: Anda dapat memanggil satu hal sebagai pointer atau referensi, dan memutuskan untuk menggunakan "referensi" dalam bahasa "aman" dan "pointer" dalam bahasa yang memungkinkan penggunaan aritmatika pointer dan manajemen memori manual. (AFAIK "pointer aritmatika" hanya dilakukan di C / C ++.)
curiousguy
2

Anda harus memahami "Perilaku Tidak Terdefinisi" dan asalnya.

Perilaku Tidak Terdefinisi berarti perilaku yang tidak didefinisikan oleh standar. C / C ++ memiliki terlalu banyak implementasi kompiler yang berbeda dan fitur tambahan. Fitur tambahan ini mengikat kode ke kompiler. Ini karena tidak ada pengembangan bahasa yang terpusat. Jadi beberapa fitur canggih dari beberapa kompiler menjadi "perilaku tidak terdefinisi".

Sedangkan di Jawa spesifikasi bahasa dikendalikan oleh Sun-Oracle dan tidak ada orang lain yang mencoba membuat spesifikasi dan dengan demikian tidak ada perilaku yang tidak terdefinisi.

Diedit Khusus menjawab Pertanyaan

  1. Java bebas dari perilaku yang tidak terdefinisi karena standar dibuat sebelum kompiler
  2. Kompiler C / C ++ modern memiliki standar implementasi yang lebih / kurang, tetapi fitur yang diimplementasikan sebelum standarisasi masih ditandai sebagai "perilaku tidak terdefinisi" karena ISO menjaga ibu pada aspek-aspek ini.
Sarvex
sumber
2
Anda mungkin benar bahwa tidak ada UB di Jawa, tetapi bahkan ketika satu entitas mengendalikan semuanya, mungkin ada alasan untuk memiliki UB, jadi alasan yang Anda berikan tidak mengarah pada kesimpulan.
Pemrogram
2
Selain itu, baik C dan C ++ distandarisasi oleh ISO. Meskipun mungkin ada beberapa kompiler, hanya ada satu standar pada suatu waktu.
MSalters
1
@SarvexJatasra, saya tidak setuju bahwa itu adalah satu-satunya sumber UB. Sebagai contoh, satu UB adalah dereferencing dangling pointer dan ada alasan bagus untuk menjadikannya UB dalam bahasa apa pun yang tidak memiliki GC, bahkan jika Anda memulai spec Anda sekarang. Dan alasan itu tidak ada hubungannya dengan praktik yang ada atau kompiler yang ada.
Pemrogram
2
@SarvexJatasra, menandatangani overflow adalah UB karena standar mengatakan secara eksplisit begitu (itu bahkan contoh yang diberikan dengan definisi UB). Dereferencing pointer yang tidak valid juga merupakan UB untuk alasan yang sama, menurut standar.
Pemrogram
2
@ bames53: Tidak ada keunggulan yang dikutip akan membutuhkan tingkat kompiler hypermodern lintang yang diambil dengan UB. Dengan pengecualian akses memori di luar batas dan stack overflows, yang dapat "secara alami" memicu eksekusi kode acak, saya tidak dapat memikirkan optimasi yang berguna yang akan memerlukan garis lintang yang lebih luas daripada mengatakan bahwa sebagian besar operasi UB-ish menghasilkan hasil tak tentu nilai-nilai (yang mungkin berperilaku seolah-olah mereka memiliki "bit tambahan") dan mungkin hanya memiliki konsekuensi di luar itu jika dokumen implementasi secara tegas berhak untuk memaksakan seperti itu; docs dapat memberikan "Perilaku tidak terbatas" ...
supercat
1

Java pada dasarnya menghilangkan semua perilaku tidak terdefinisi yang ditemukan di C / C ++. (Sebagai contoh: Signed overflow integer, pembagian dengan nol, variabel tidak diinisialisasi, null pointer dereference, menggeser lebih dari lebar bit, bebas ganda, bahkan "tidak ada baris baru di akhir kode sumber".) Tetapi Java memiliki beberapa perilaku tidak jelas yang tidak jelas yang jarang ditemui oleh programmer.

  • Java Native Interface (JNI), suatu cara bagi Java untuk memanggil kode C atau C ++. Ada banyak cara untuk mengacaukan JNI, seperti membuat tanda tangan fungsi salah, membuat panggilan tidak sah ke layanan JVM, merusak memori, mengalokasikan / membebaskan barang secara tidak benar, dan banyak lagi. Saya telah membuat kesalahan ini sebelumnya, dan umumnya seluruh JVM crash ketika ada satu thread yang mengeksekusi kode JNI melakukan kesalahan.

  • Thread.stop(), yang sudah usang. Mengutip:

    Mengapa Thread.stopditinggalkan?

    Karena secara inheren tidak aman. Menghentikan utas menyebabkannya membuka kunci semua monitor yang telah dikunci. (Monitor tidak terkunci karena ThreadDeathperkecualian menyebar ke tumpukan.) Jika salah satu objek yang sebelumnya dilindungi oleh monitor ini berada dalam kondisi tidak konsisten, utas lain sekarang dapat melihat objek ini dalam keadaan tidak konsisten. Benda-benda seperti itu dikatakan rusak. Ketika utas beroperasi pada objek yang rusak, perilaku sewenang-wenang dapat terjadi. Perilaku ini mungkin halus dan sulit dideteksi, atau mungkin diucapkan. Tidak seperti pengecualian yang tidak dicentang, ThreadDeathmembunuh utas secara diam-diam; dengan demikian, pengguna tidak memiliki peringatan bahwa programnya mungkin rusak. Korupsi dapat memanifestasikan dirinya kapan saja setelah kerusakan yang sebenarnya terjadi, bahkan berjam-jam atau berhari-hari di masa depan.

    https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html

Nayuki
sumber