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 NULLapabila tidak dapat membuka fail. mysql_query()Fungsi PHP kembali FALSEapabila 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 NULLatau FALSEsebenarnya? 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 berfungsi fgetc()untuk membaca watak dari fail walaupun fopen()kembali NULL. 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.Throwablekelas. (Sebab mengapa Throwabledipilih untuk menamakan kelas khas ini akan menjadi tidak lama lagi.) Tepat di bawahnya Throwableadalah java.lang.Exceptiondan java.lang.Errorkelas, yang masing-masing menerangkan pengecualian dan kesalahan.

Sebagai contoh, perpustakaan Java termasuk java.net.URISyntaxException, yang meluas Exceptiondan menunjukkan bahawa rentetan tidak dapat diuraikan sebagai rujukan Pengenal Sumber Daya Seragam. Perhatikan bahawa URISyntaxExceptionmengikuti konvensyen penamaan di mana nama kelas pengecualian berakhir dengan perkataan Exception. Konvensyen serupa berlaku untuk nama kelas kesalahan, seperti java.lang.OutOfMemoryError.

Exceptiondiklasifikasikan oleh java.lang.RuntimeException, yang merupakan superclass dari pengecualian yang dapat dilemparkan semasa operasi normal Java Virtual Machine (JVM). Sebagai contoh, java.lang.ArithmeticExceptionmenerangkan kegagalan aritmetik seperti percubaan membahagi bilangan bulat dengan bilangan bulat 0. Juga, java.lang.NullPointerExceptionmenerangkan percubaan untuk mengakses anggota objek melalui rujukan nol.

Kaedah lain untuk melihat RuntimeException

Bahagian 11.1.1 dari Spesifikasi Bahasa Java 8 menyatakan: RuntimeExceptionadalah 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 Exceptionatau Errordibuat dibuat dan diserahkan ke JVM. Tindakan melepasi objek dikenali sebagai membuang pengecualian . Java memberikan throwpernyataan untuk tujuan ini. Sebagai contoh, throw new IOException("unable to read file");membuat java.io.IOExceptionobjek baru yang diinisialisasi dengan teks yang ditentukan. Objek ini kemudian dilemparkan ke JVM.

Java memberikan trypernyataan untuk membatasi kod dari mana pengecualian dapat dilemparkan. Pernyataan ini terdiri daripada kata kunci yang trydiikuti oleh blok berpandangan kurungan. Fragmen kod berikut menunjukkan trydan throw:

try { method(); } // ... void method() { throw new NullPointerException("some text"); }

Dalam fragmen kod ini, pelaksanaan memasuki tryblok 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 RuntimeExceptionsering 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 catchblok yang mengikuti tryblok. The catchblok menyediakan header yang menyenaraikan jenis pengecualian bahawa ia bersedia untuk mengendalikan. Sekiranya jenis lemparan dimasukkan dalam senarai, lemparan boleh dilepaskan ke catchblok 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 catchblok ke tryblok tersebut. Apabila NullPointerExceptionobjek dilemparkan dari method(), JVM mencari dan meneruskan pelaksanaan ke catchblok, yang mengeluarkan mesej.

Akhirnya blok

A tryblok atau akhir catchblok boleh diikuti dengan finallyblok yang digunakan untuk melaksanakan tugas-tugas pembersihan, seperti melepaskan sumber diperolehi. Saya tidak ada lagi yang perlu diperkatakan finallykerana ia tidak berkaitan dengan perbincangan.

Pengecualian yang dijelaskan oleh Exceptiondan subkelasnya kecuali untuk RuntimeExceptiondan subkelasnya dikenali sebagai pengecualian yang diperiksa . Untuk setiap throwpernyataan, 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 throwsklausa (kata kunci throwsdiikuti 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 IOExceptionadalah 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 throwsklausa ke setiap tajuk metode yang terkena. Dalam kes ini, throws IOExceptionklausa ditambahkan method()pada tajuk. IOExceptionObjek yang dilemparkan diteruskan ke JVM, yang menempatkan dan memindahkan pelaksanaan ke catchpengendali.

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 adding throws 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 throws IOException, 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: