Apakah pengecualian yang dicentang baik atau buruk?

Java mendukung pengecualian yang dicentang. Fitur bahasa kontroversial ini disukai oleh beberapa orang dan dibenci oleh orang lain, sampai pada titik di mana sebagian besar bahasa pemrograman menghindari pengecualian yang dicentang dan hanya mendukung bahasa yang tidak dicentang.

Dalam posting ini, saya memeriksa kontroversi seputar pengecualian yang diperiksa. Saya pertama kali memperkenalkan konsep pengecualian dan menjelaskan secara singkat dukungan bahasa Java untuk pengecualian untuk membantu pemula lebih memahami kontroversi.

Apakah pengecualian itu?

Dalam dunia yang ideal, program komputer tidak akan pernah menemui masalah: file akan ada ketika mereka seharusnya ada, koneksi jaringan tidak akan pernah menutup secara tidak terduga, tidak akan pernah ada upaya untuk memanggil metode melalui referensi null, integer-division-by -coba nol tidak akan terjadi, dan seterusnya. Namun, dunia kita jauh dari ideal; ini dan pengecualian lain untuk pelaksanaan program yang ideal tersebar luas.

Upaya awal untuk mengenali pengecualian termasuk mengembalikan nilai khusus yang menunjukkan kegagalan. Misalnya, fungsi bahasa C fopen()kembali NULLketika tidak bisa membuka file. Selain itu, mysql_query()fungsi PHP kembali FALSEketika terjadi kegagalan SQL. Anda harus mencari di tempat lain untuk kode kegagalan yang sebenarnya. Meskipun mudah diterapkan, ada dua masalah dengan pendekatan "kembalikan nilai khusus" ini untuk mengenali pengecualian:

  • Nilai khusus tidak mendeskripsikan pengecualian. Apa artinya NULLatau FALSEsebenarnya? Itu semua tergantung pada pembuat fungsionalitas yang mengembalikan nilai khusus. Selanjutnya, bagaimana Anda menghubungkan nilai khusus dengan konteks program ketika pengecualian terjadi sehingga Anda dapat menyajikan pesan yang berarti kepada pengguna?
  • Terlalu mudah untuk mengabaikan nilai khusus. Misalnya, int c; FILE *fp = fopen("data.txt", "r"); c = fgetc(fp);bermasalah karena fragmen kode C ini dijalankan fgetc()untuk membaca karakter dari file bahkan ketika fopen()kembali NULL. Dalam kasus ini, fgetc()tidak akan berhasil: kami memiliki bug yang mungkin sulit ditemukan.

Masalah pertama diselesaikan dengan menggunakan kelas untuk mendeskripsikan pengecualian. Nama kelas mengidentifikasi jenis pengecualian dan bidangnya menggabungkan konteks program yang sesuai untuk menentukan (melalui panggilan metode) apa yang salah. Masalah kedua diselesaikan dengan meminta kompilator memaksa pemrogram untuk menanggapi pengecualian secara langsung atau menunjukkan bahwa pengecualian akan ditangani di tempat lain.

Beberapa pengecualian sangat serius. Misalnya, program mungkin mencoba mengalokasikan beberapa memori saat tidak ada memori kosong yang tersedia. Rekursi tanpa batas yang menghabiskan tumpukan adalah contoh lain. Pengecualian seperti itu dikenal sebagai kesalahan .

Pengecualian dan Java

Java menggunakan kelas untuk mendeskripsikan pengecualian dan kesalahan. Kelas-kelas ini diatur menjadi hierarki yang berakar di java.lang.Throwablekelas. (Alasan mengapa Throwabledipilih untuk menamai kelas khusus ini akan segera terlihat.) Tepat di bawahnya Throwableadalah kelas java.lang.Exceptiondan java.lang.Error, yang masing-masing menjelaskan pengecualian dan kesalahan.

Misalnya, library Java termasuk java.net.URISyntaxException, yang memperluas Exceptiondan menunjukkan bahwa string tidak dapat diurai sebagai referensi Uniform Resource Identifier. Perhatikan bahwa URISyntaxExceptionmengikuti konvensi penamaan di mana nama kelas pengecualian diakhiri dengan kata Exception. Konvensi serupa berlaku untuk nama kelas kesalahan, seperti java.lang.OutOfMemoryError.

Exceptionadalah subclass java.lang.RuntimeException, yang merupakan superclass dari pengecualian yang dapat dilemparkan selama operasi normal Java Virtual Machine (JVM). Misalnya, java.lang.ArithmeticExceptionmenjelaskan kegagalan aritmatika seperti upaya untuk membagi bilangan bulat dengan bilangan bulat 0. Juga, java.lang.NullPointerExceptionmenjelaskan upaya untuk mengakses anggota objek melalui referensi nol.

Cara lain untuk melihat RuntimeException

Bagian 11.1.1 dari Spesifikasi Bahasa Java 8 menyatakan: RuntimeExceptionadalah kelas super dari semua pengecualian yang dapat dilemparkan karena berbagai alasan selama evaluasi ekspresi, tetapi pemulihan masih dapat dilakukan.

Ketika pengecualian atau kesalahan terjadi, objek dari subclass Exceptionatau yang sesuai Errordibuat dan diteruskan ke JVM. Tindakan mengoper objek dikenal sebagai melempar eksepsi . Java menyediakan throwpernyataan untuk tujuan ini. Misalnya, throw new IOException("unable to read file");membuat java.io.IOExceptionobjek baru yang diinisialisasi ke teks yang ditentukan. Objek ini kemudian dilemparkan ke JVM.

Java menyediakan trypernyataan untuk membatasi kode yang darinya pengecualian dapat dilemparkan. Pernyataan ini terdiri dari kata kunci yang trydiikuti dengan blok yang dipisahkan oleh tanda kurung kurawal. Fragmen kode berikut menunjukkan trydan throw:

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

Dalam fragmen kode ini, eksekusi memasuki tryblok dan memanggil method(), yang memunculkan instance NullPointerException.

JVM menerima throwable dan mencari tumpukan panggilan metode untuk penangan untuk menangani pengecualian. Pengecualian yang bukan berasal dari RuntimeExceptionsering ditangani; pengecualian dan kesalahan runtime jarang ditangani.

Mengapa kesalahan jarang ditangani

Kesalahan jarang ditangani karena seringkali tidak ada program Java yang dapat melakukan pemulihan dari kesalahan. Misalnya, saat memori bebas habis, program tidak dapat mengalokasikan memori tambahan. Namun, jika kegagalan alokasi disebabkan karena menyimpan banyak memori yang harus dibebaskan, penangan dapat mencoba untuk membebaskan memori dengan bantuan JVM. Meskipun penangan mungkin tampak berguna dalam konteks kesalahan ini, upaya tersebut mungkin tidak berhasil.

Seorang penangan dijelaskan oleh catchblok yang mengikuti tryblok tersebut. The catchblok menyediakan header yang daftar jenis pengecualian bahwa itu siap untuk menangani. Jika jenis throwable termasuk dalam daftar, throwable tersebut diteruskan ke catchblok yang kodenya dijalankan. Kode menanggapi penyebab kegagalan sedemikian rupa sehingga menyebabkan program untuk melanjutkan, atau mungkin dihentikan:

try { method(); } catch (NullPointerException npe) { System.out.println("attempt to access object member via null reference"); } // ... void method() { throw new NullPointerException("some text"); }

Dalam potongan kode ini, saya telah menambahkan satu catchblok ke tryblok tersebut. Ketika NullPointerExceptionobjek dilempar method(), JVM menempatkan dan meneruskan eksekusi ke catchblok, yang mengeluarkan pesan.

Akhirnya blok

Sebuah tryblok atau akhir catchblok dapat diikuti oleh finallyblok yang digunakan untuk melakukan tugas-tugas pembersihan, seperti melepaskan sumber daya yang diperoleh. Tidak ada lagi yang bisa saya katakan finallykarena itu tidak relevan dengan diskusi.

Pengecualian yang dijelaskan oleh Exceptiondan subkelasnya kecuali untuk RuntimeExceptiondan subkelasnya dikenal sebagai pengecualian yang diperiksa . Untuk setiap throwpernyataan, kompilator memeriksa tipe objek pengecualian. Jika type mengindikasikan dicentang, compiler akan memeriksa kode sumber untuk memastikan bahwa pengecualian ditangani dalam metode di mana ia dilemparkan atau dideklarasikan untuk ditangani lebih jauh ke atas tumpukan metode-panggilan. Semua pengecualian lainnya dikenal sebagai pengecualian yang tidak dicentang .

Java memungkinkan Anda mendeklarasikan bahwa pengecualian yang dicentang ditangani lebih jauh ke tumpukan panggilan metode dengan menambahkan throwsklausa (kata kunci throwsdiikuti dengan daftar nama kelas pengecualian yang dipisahkan koma) ke header metode:

try { method(); } catch (IOException ioe) { System.out.println("I/O failure"); } // ... void method() throws IOException { throw new IOException("some text"); }

Karena IOExceptionmerupakan tipe pengecualian yang dicentang, instance yang muncul dari pengecualian ini harus ditangani dalam metode di mana mereka dilemparkan atau dideklarasikan untuk ditangani lebih jauh ke tumpukan metode-panggilan dengan menambahkan throwsklausa ke setiap header metode yang terpengaruh. Dalam kasus ini, throws IOExceptionklausa ditambahkan ke method()header. IOExceptionObjek yang dilempar diteruskan ke JVM, yang menempatkan dan mentransfer eksekusi ke catchpenangan.

Mendebat untuk dan menentang pengecualian yang dicentang

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 menemukan argumen tentang aplikasi yang harus menangani sejumlah besar pengecualian yang diperiksa yang dihasilkan dari beberapa pustaka yang mereka akses. Namun, masalah ini dapat diatasi melalui fasad yang dirancang dengan cerdik yang memanfaatkan fasilitas pengecualian berantai Java dan penggalian ulang pengecualian untuk sangat mengurangi jumlah pengecualian yang harus ditangani sambil mempertahankan pengecualian asli yang dilemparkan.

Kesimpulan

Apakah pengecualian yang dicentang baik atau buruk? Dengan kata lain, haruskah programmer dipaksa untuk menangani pengecualian yang dicentang atau diberi kesempatan untuk mengabaikannya? Saya menyukai gagasan untuk menerapkan perangkat lunak yang lebih kuat. Namun, menurut saya mekanisme penanganan pengecualian Java perlu berevolusi agar lebih ramah programmer. Berikut ini beberapa cara untuk meningkatkan mekanisme ini: