Adakah pengecualian yang diperiksa baik atau buruk?
Java menyokong pengecualian yang diperiksa. Ciri bahasa kontroversial ini disukai oleh beberapa orang dan dibenci oleh yang lain, sehingga kebanyakan bahasa pengaturcaraan mengelakkan pengecualian yang diperiksa dan hanya menyokong rakan sejawatnya yang tidak dicentang.
Dalam catatan ini, saya mengkaji kontroversi mengenai pengecualian yang diperiksa. Saya mula-mula memperkenalkan konsep pengecualian dan menerangkan secara ringkas sokongan bahasa Jawa untuk pengecualian untuk membantu pemula memahami kontroversi dengan lebih baik.
Apakah pengecualian?
Dalam dunia yang ideal, program komputer tidak akan pernah menghadapi masalah: fail akan wujud ketika seharusnya ada, sambungan rangkaian tidak akan pernah ditutup tanpa diduga, tidak akan pernah ada usaha untuk menggunakan kaedah melalui rujukan nol, pembahagian integer-oleh -cubaan sifar tidak akan berlaku, dan seterusnya. Walau bagaimanapun, dunia kita jauh dari ideal; ini dan pengecualian lain untuk pelaksanaan program yang ideal semakin meluas.
Percubaan awal untuk mengenali pengecualian termasuk mengembalikan nilai khas yang menunjukkan kegagalan. Sebagai contoh, fopen()
fungsi bahasa C kembali NULL
apabila tidak dapat membuka fail. mysql_query()
Fungsi PHP kembali FALSE
apabila kegagalan SQL berlaku. Anda mesti mencari kod kegagalan sebenar di tempat lain. Walaupun mudah dilaksanakan, terdapat dua masalah dengan pendekatan "nilai khas kembali" ini untuk mengenali pengecualian:
- Nilai khas tidak menggambarkan pengecualian. Apa maksud
NULL
atauFALSE
sebenarnya? Semuanya bergantung pada pengarang fungsi yang mengembalikan nilai khas. Tambahan pula, bagaimana anda mengaitkan nilai khas dengan konteks program ketika pengecualian berlaku sehingga anda dapat menyampaikan pesan yang bermakna kepada pengguna? - Terlalu mudah untuk mengabaikan nilai khas. Sebagai contoh,
int c; FILE *fp = fopen("data.txt", "r"); c = fgetc(fp);
bermasalah kerana fragmen kod C ini berfungsifgetc()
untuk membaca watak dari fail walaupunfopen()
kembaliNULL
. Dalam kes ini,fgetc()
tidak akan berjaya: kita mempunyai bug yang mungkin sukar dicari.
Masalah pertama diselesaikan dengan menggunakan kelas untuk menerangkan pengecualian. Nama kelas mengenal pasti jenis pengecualian dan bidangnya mengumpulkan konteks program yang sesuai untuk menentukan (melalui kaedah panggilan) apa yang salah. Masalah kedua diselesaikan dengan meminta pengkompil memaksa pengaturcara untuk menanggapi pengecualian secara langsung atau menunjukkan bahawa pengecualian itu harus ditangani di tempat lain.
Beberapa pengecualian sangat serius. Sebagai contoh, program mungkin berusaha untuk memperuntukkan sedikit memori apabila tidak ada memori percuma. Rekursi tanpa had yang menghabiskan timbunan adalah contoh lain. Pengecualian tersebut dikenali sebagai kesalahan .
Pengecualian dan Java
Java menggunakan kelas untuk menerangkan pengecualian dan kesalahan. Kelas-kelas ini disusun menjadi hierarki yang berakar pada java.lang.Throwable
kelas. (Sebab mengapa Throwable
dipilih untuk menamakan kelas khas ini akan menjadi tidak lama lagi.) Tepat di bawahnya Throwable
adalah java.lang.Exception
dan java.lang.Error
kelas, yang masing-masing menerangkan pengecualian dan kesalahan.
Sebagai contoh, perpustakaan Java termasuk java.net.URISyntaxException
, yang meluas Exception
dan menunjukkan bahawa rentetan tidak dapat diuraikan sebagai rujukan Pengenal Sumber Daya Seragam. Perhatikan bahawa URISyntaxException
mengikuti konvensyen penamaan di mana nama kelas pengecualian berakhir dengan perkataan Exception
. Konvensyen serupa berlaku untuk nama kelas kesalahan, seperti java.lang.OutOfMemoryError
.
Exception
diklasifikasikan oleh java.lang.RuntimeException
, yang merupakan superclass dari pengecualian yang dapat dilemparkan semasa operasi normal Java Virtual Machine (JVM). Sebagai contoh, java.lang.ArithmeticException
menerangkan kegagalan aritmetik seperti percubaan membahagi bilangan bulat dengan bilangan bulat 0. Juga, java.lang.NullPointerException
menerangkan percubaan untuk mengakses anggota objek melalui rujukan nol.
Kaedah lain untuk melihat RuntimeException
Bahagian 11.1.1 dari Spesifikasi Bahasa Java 8 menyatakan: RuntimeException
adalah superclass dari semua pengecualian yang mungkin dilemparkan dengan banyak alasan semasa penilaian ekspresi, tetapi dari mana pemulihan masih mungkin dilakukan.
Apabila pengecualian atau ralat berlaku, objek dari subkelas yang sesuai Exception
atau Error
dibuat dibuat dan diserahkan ke JVM. Tindakan melepasi objek dikenali sebagai membuang pengecualian . Java memberikan throw
pernyataan untuk tujuan ini. Sebagai contoh, throw new IOException("unable to read file");
membuat java.io.IOException
objek baru yang diinisialisasi dengan teks yang ditentukan. Objek ini kemudian dilemparkan ke JVM.
Java memberikan try
pernyataan untuk membatasi kod dari mana pengecualian dapat dilemparkan. Pernyataan ini terdiri daripada kata kunci yang try
diikuti oleh blok berpandangan kurungan. Fragmen kod berikut menunjukkan try
dan throw
:
try { method(); } // ... void method() { throw new NullPointerException("some text"); }
Dalam fragmen kod ini, pelaksanaan memasuki try
blok dan memanggil method()
, yang melemparkan contoh NullPointerException
.
JVM menerima pelemparan dan mencari tumpukan kaedah panggilan untuk pengendali untuk menangani pengecualian. Pengecualian yang tidak berasal dari RuntimeException
sering dikendalikan; pengecualian dan kesilapan runtime jarang ditangani.
Mengapa kesilapan jarang ditangani
Kesalahan jarang dapat diatasi kerana selalunya tidak ada yang dapat dilakukan oleh program Java untuk memulihkan kesalahan. Sebagai contoh, apabila memori percuma habis, program tidak dapat mengalokasikan memori tambahan. Namun, jika kegagalan peruntukan disebabkan oleh banyak memori yang harus dibebaskan, penyedia dapat berusaha untuk membebaskan memori dengan bantuan dari JVM. Walaupun pengendali nampaknya berguna dalam konteks ralat ini, percubaan itu mungkin tidak berjaya.
Penangan digambarkan oleh catch
blok yang mengikuti try
blok. The catch
blok menyediakan header yang menyenaraikan jenis pengecualian bahawa ia bersedia untuk mengendalikan. Sekiranya jenis lemparan dimasukkan dalam senarai, lemparan boleh dilepaskan ke catch
blok yang kodnya dilaksanakan. Kod tersebut bertindak balas terhadap penyebab kegagalan sedemikian rupa sehingga menyebabkan program tersebut dapat diteruskan, atau mungkin berakhir:
try { method(); } catch (NullPointerException npe) { System.out.println("attempt to access object member via null reference"); } // ... void method() { throw new NullPointerException("some text"); }
Dalam fragmen kod ini, saya telah menambahkan catch
blok ke try
blok tersebut. Apabila NullPointerException
objek dilemparkan dari method()
, JVM mencari dan meneruskan pelaksanaan ke catch
blok, yang mengeluarkan mesej.
Akhirnya blok
A try
blok atau akhir catch
blok boleh diikuti dengan finally
blok yang digunakan untuk melaksanakan tugas-tugas pembersihan, seperti melepaskan sumber diperolehi. Saya tidak ada lagi yang perlu diperkatakan finally
kerana ia tidak berkaitan dengan perbincangan.
Pengecualian yang dijelaskan oleh Exception
dan subkelasnya kecuali untuk RuntimeException
dan subkelasnya dikenali sebagai pengecualian yang diperiksa . Untuk setiap throw
pernyataan, penyusun meneliti jenis objek pengecualian. Sekiranya jenis menunjukkan diperiksa, penyusun akan memeriksa kod sumber untuk memastikan bahawa pengecualian dikendalikan dalam kaedah di mana ia dilemparkan atau dinyatakan akan ditangani lebih jauh ke atas tumpukan kaedah panggilan. Semua pengecualian lain dikenali sebagai pengecualian yang tidak dicentang .
Java memungkinkan anda menyatakan bahawa pengecualian yang diperiksa ditangani lebih jauh ke atas tumpukan kaedah-kaedah dengan menambahkan throws
klausa (kata kunci throws
diikuti dengan senarai nama kelas pengecualian yang dibatasi tanda koma) ke tajuk kaedah:
try { method(); } catch (IOException ioe) { System.out.println("I/O failure"); } // ... void method() throws IOException { throw new IOException("some text"); }
Kerana IOException
adalah jenis pengecualian yang dicentang, contoh pengecualian yang dilemparkan ini harus dikendalikan dalam metode di mana mereka dilemparkan atau dinyatakan akan ditangani lebih jauh ke atas tumpukan kaedah-panggilan dengan menambahkan throws
klausa ke setiap tajuk metode yang terkena. Dalam kes ini, throws IOException
klausa ditambahkan method()
pada tajuk. IOException
Objek yang dilemparkan diteruskan ke JVM, yang menempatkan dan memindahkan pelaksanaan ke catch
pengendali.
Berhujah untuk dan menentang pengecualian yang diperiksa
Checked exceptions have proven to be very controversial. Are they a good language feature or are they bad? In this section, I present the cases for and against checked exceptions.
Checked exceptions are good
James Gosling created the Java language. He included checked exceptions to encourage the creation of more robust software. In a 2003 conversation with Bill Venners, Gosling pointed out how easy it is to generate buggy code in the C language by ignoring the special values that are returned from C's file-oriented functions. For example, a program attempts to read from a file that wasn't successfully opened for reading.
The seriousness of not checking return values
Not checking return values might seem like no big deal, but this sloppiness can have life-or-death consequences. For example, think about such buggy software controlling missile guidance systems and driverless cars.
Gosling also pointed out that college programming courses don't adequately discuss error handling (although that may have changed since 2003). When you go through college and you're doing assignments, they just ask you to code up the one true path [of execution where failure isn't a consideration]. I certainly never experienced a college course where error handling was at all discussed. You come out of college and the only stuff you've had to deal with is the one true path.
Focusing only on the one true path, laziness, or another factor has resulted in a lot of buggy code being written. Checked exceptions require the programmer to consider the source code's design and hopefully achieve more robust software.
Checked exceptions are bad
Many programmers hate checked exceptions because they're forced to deal with APIs that overuse them or incorrectly specify checked exceptions instead of unchecked exceptions as part of their contracts. For example, a method that sets a sensor's value is passed an invalid number and throws a checked exception instead of an instance of the unchecked java.lang.IllegalArgumentException
class.
Here are a few other reasons for disliking checked exceptions; I've excerpted them from Slashdot's Interviews: Ask James Gosling About Java and Ocean Exploring Robots discussion:
- Checked exceptions are easy to ignore by rethrowing them as
RuntimeException
instances, so what's the point of having them? I've lost count of the number of times I've written this block of code:try { // do stuff } catch (AnnoyingcheckedException e) { throw new RuntimeException(e); }
99% of the time I can't do anything about it. Finally blocks do any necessary cleanup (or at least they should).
- Checked exceptions can be ignored by swallowing them, so what's the point of having them? I've also lost count of the number of times I've seen this:
try { // do stuff } catch (AnnoyingCheckedException e) { // do nothing }
Why? Because someone had to deal with it and was lazy. Was it wrong? Sure. Does it happen? Absolutely. What if this were an unchecked exception instead? The app would've just died (which is preferable to swallowing an exception).
- Checked exceptions result in multiple
throws
clause declarations. The problem with checked exceptions is they encourage people to swallow important details (namely, the exception class). If you choose not to swallow that detail, then you have to keep addingthrows
declarations across your whole app. This means 1) that a new exception type will affect lots of function signatures, and 2) you can miss a specific instance of the exception you actually -want- to catch (say you open a secondary file for a function that writes data to a file. The secondary file is optional, so you can ignore its errors, but because the signature throwsIOException
, it's easy to overlook this). - Checked exceptions are not really exceptions. The thing about checked exceptions is that they are not really exceptions by the usual understanding of the concept. Instead, they are API alternative return values.
The whole idea of exceptions is that an error thrown somewhere way down the call chain can bubble up and be handled by code somewhere further up, without the intervening code having to worry about it. Checked exceptions, on the other hand, require every level of code between the thrower and the catcher to declare they know about all forms of exception that can go through them. This is really little different in practice to if checked exceptions were simply special return values which the caller had to check for.
Selain itu, saya menghadapi pertikaian mengenai aplikasi yang harus menangani sejumlah besar pengecualian yang diperiksa yang dihasilkan dari pelbagai perpustakaan yang mereka akses. Namun, masalah ini dapat diatasi melalui fasad yang dirancang dengan bijak yang memanfaatkan kemudahan pengecualian berantai dan pengecualian semula Java untuk mengurangkan jumlah pengecualian yang mesti ditangani sambil mengekalkan pengecualian asli yang dilemparkan.
Kesimpulannya
Adakah pengecualian yang diperiksa baik atau buruk? Dengan kata lain, haruskah pengaturcara terpaksa menangani pengecualian yang diperiksa atau diberi kesempatan untuk mengabaikannya? Saya suka idea untuk menguatkuasakan perisian yang lebih mantap. Namun, saya juga berpendapat bahawa mekanisme pengendalian pengecualian Java perlu berkembang untuk menjadikannya lebih mesra pengaturcara. Berikut adalah beberapa cara untuk memperbaiki mekanisme ini: