Saya tahu ini adalah pertanyaan lama tapi saya sudah menghabiskan waktu bergulat dengan pengecualian yang sudah diperiksa dan saya harus menambahkan sesuatu. Maafkan saya untuk panjangnya!
Daging sapi utama saya dengan pengecualian adalah mereka merusak polimorfisme. Tidak mungkin membuat mereka bermain dengan baik dengan antarmuka polimorfik.
Ambil List
antarmuka Java yang bagus . Kami memiliki implementasi dalam memori yang umum seperti ArrayList
dan LinkedList
. Kami juga memiliki kelas kerangka AbstractList
yang membuatnya mudah untuk merancang daftar tipe baru. Untuk daftar read-only, kita hanya perlu mengimplementasikan dua metode: size()
dan get(int index)
.
WidgetList
Kelas contoh ini membaca beberapa objek ukuran tetap tipe Widget
(tidak ditampilkan) dari file:
class WidgetList extends AbstractList<Widget> {
private static final int SIZE_OF_WIDGET = 100;
private final RandomAccessFile file;
public WidgetList(RandomAccessFile file) {
this.file = file;
}
@Override
public int size() {
return (int)(file.length() / SIZE_OF_WIDGET);
}
@Override
public Widget get(int index) {
file.seek((long)index * SIZE_OF_WIDGET);
byte[] data = new byte[SIZE_OF_WIDGET];
file.read(data);
return new Widget(data);
}
}
Dengan mengekspos Widget menggunakan List
antarmuka yang sudah dikenal , Anda dapat mengambil item ( list.get(123)
) atau mengulangi daftar ( for (Widget w : list) ...
) tanpa perlu tahu tentang WidgetList
dirinya sendiri. Seseorang dapat meneruskan daftar ini ke metode standar apa saja yang menggunakan daftar generik, atau membungkusnya dalam Collections.synchronizedList
. Kode yang menggunakannya tidak perlu tahu atau tidak peduli apakah "Widget" dibuat di tempat, berasal dari array, atau dibaca dari file, atau database, atau dari seluruh jaringan, atau dari relay ruang bagian masa depan. Itu masih akan berfungsi dengan benar karena List
antarmuka diimplementasikan dengan benar.
Kecuali itu tidak. Kelas di atas tidak dikompilasi karena metode akses file dapat melempar IOException
, pengecualian yang diperiksa yang harus Anda "tangkap atau tentukan". Anda tidak dapat menetapkannya sebagai dilemparkan - kompiler tidak akan membiarkan Anda karena itu akan melanggar kontrak List
antarmuka. Dan tidak ada cara yang berguna yang WidgetList
dapat menangani pengecualian (seperti yang akan saya jelaskan nanti).
Tampaknya satu-satunya hal yang harus dilakukan adalah menangkap dan menguji kembali pengecualian yang diperiksa sebagai pengecualian yang tidak dicentang:
@Override
public int size() {
try {
return (int)(file.length() / SIZE_OF_WIDGET);
} catch (IOException e) {
throw new WidgetListException(e);
}
}
public static class WidgetListException extends RuntimeException {
public WidgetListException(Throwable cause) {
super(cause);
}
}
((Sunting: Java 8 telah menambahkan UncheckedIOException
kelas untuk kasus ini: untuk menangkap dan mem-rethrowing IOException
melintasi batas-batas metode polimorfik. Jenis membuktikan poin saya!))
Jadi pengecualian yang diperiksa tidak berfungsi dalam kasus seperti ini. Anda tidak bisa melemparnya. Ditto untuk pandai yang Map
didukung oleh database, atau implementasi java.util.Random
terhubung ke sumber entropi kuantum melalui port COM. Segera setelah Anda mencoba melakukan novel apa pun dengan penerapan antarmuka polimorfik, konsep pengecualian yang diperiksa gagal. Tetapi pengecualian yang dicentang sangat berbahaya sehingga mereka tidak akan meninggalkan Anda dengan tenang, karena Anda masih harus menangkap dan mengolah kembali dari metode tingkat bawah, mengacaukan kode dan mengacaukan jejak tumpukan.
Saya menemukan bahwa Runnable
antarmuka di mana-mana sering dicadangkan ke sudut ini, jika itu memanggil sesuatu yang melempar pengecualian yang diperiksa. Itu tidak bisa melempar pengecualian seperti apa adanya, jadi yang bisa dilakukan hanyalah mengacaukan kode dengan menangkap dan mem-rethrowing sebagai RuntimeException
.
Sebenarnya, Anda dapat membuang pengecualian yang tidak dideklarasikan jika Anda menggunakan peretasan. JVM, pada saat dijalankan, tidak peduli dengan aturan pengecualian yang diperiksa, jadi kita hanya perlu menipu kompilator. Cara termudah untuk melakukan ini adalah dengan menyalahgunakan obat generik. Ini adalah metode saya untuk itu (nama kelas ditampilkan karena (sebelum Java 8) itu diperlukan dalam sintaks panggilan untuk metode generik):
class Util {
/**
* Throws any {@link Throwable} without needing to declare it in the
* method's {@code throws} clause.
*
* <p>When calling, it is suggested to prepend this method by the
* {@code throw} keyword. This tells the compiler about the control flow,
* about reachable and unreachable code. (For example, you don't need to
* specify a method return value when throwing an exception.) To support
* this, this method has a return type of {@link RuntimeException},
* although it never returns anything.
*
* @param t the {@code Throwable} to throw
* @return nothing; this method never returns normally
* @throws Throwable that was provided to the method
* @throws NullPointerException if {@code t} is {@code null}
*/
public static RuntimeException sneakyThrow(Throwable t) {
return Util.<RuntimeException>sneakyThrow1(t);
}
@SuppressWarnings("unchecked")
private static <T extends Throwable> RuntimeException sneakyThrow1(
Throwable t) throws T {
throw (T)t;
}
}
Hore! Dengan menggunakan ini kita bisa melempar pengecualian yang diperiksa setiap kedalaman tumpukan tanpa mendeklarasikannya, tanpa membungkusnya dalam RuntimeException
, dan tanpa mengacaukan jejak tumpukan! Menggunakan contoh "WidgetList" lagi:
@Override
public int size() {
try {
return (int)(file.length() / SIZE_OF_WIDGET);
} catch (IOException e) {
throw sneakyThrow(e);
}
}
Sayangnya, penghinaan terakhir dari pengecualian yang diperiksa adalah bahwa kompiler menolak untuk mengizinkan Anda menangkap pengecualian yang diperiksa jika, menurut pendapatnya yang cacat, itu tidak mungkin terlempar. (Pengecualian yang tidak dicentang tidak memiliki aturan ini.) Untuk mengetahui pengecualian yang terlempar secara licik, kita harus melakukan ini:
try {
...
} catch (Throwable t) { // catch everything
if (t instanceof IOException) {
// handle it
...
} else {
// didn't want to catch this one; let it go
throw t;
}
}
Itu agak canggung, tetapi di sisi positifnya, itu masih sedikit lebih sederhana daripada kode untuk mengekstraksi pengecualian diperiksa yang dibungkus dengan a RuntimeException
.
Untungnya, throw t;
pernyataan itu sah di sini, meskipun jenisnya t
dicek, berkat aturan yang ditambahkan di Jawa 7 tentang rethrowing pengecualian yang ditangkap.
Ketika pengecualian yang diperiksa memenuhi polimorfisme, kasus sebaliknya juga menjadi masalah: ketika suatu metode dispesifikasi berpotensi melemparkan pengecualian yang diperiksa, tetapi implementasi yang ditimpa tidak. Sebagai contoh, kelas abstrak OutputStream
's write
metode semua tentukan throws IOException
. ByteArrayOutputStream
adalah subclass yang menulis ke array di dalam memori alih-alih sumber I / O yang sebenarnya. write
Metode yang diganti tidak dapat menyebabkan IOException
s, sehingga mereka tidak memiliki throws
klausa, dan Anda dapat memanggil mereka tanpa khawatir tentang persyaratan tangkap-atau-tentukan.
Kecuali tidak selalu. Misalkan Widget
memiliki metode untuk menyimpannya ke aliran:
public void writeTo(OutputStream out) throws IOException;
Mendeklarasikan metode ini untuk menerima sebuah plain OutputStream
adalah hal yang benar untuk dilakukan, sehingga dapat digunakan secara polimorfis dengan semua jenis output: file, database, jaringan, dan sebagainya. Dan array dalam memori. Namun, dengan array dalam memori, ada persyaratan palsu untuk menangani pengecualian yang tidak dapat benar-benar terjadi:
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
someWidget.writeTo(out);
} catch (IOException e) {
// can't happen (although we shouldn't ignore it if it does)
throw new RuntimeException(e);
}
Seperti biasa, pengecualian yang diperiksa menghalangi. Jika variabel Anda dideklarasikan sebagai tipe dasar yang memiliki lebih banyak persyaratan pengecualian terbuka, Anda harus menambahkan penangan untuk pengecualian itu bahkan jika Anda tahu mereka tidak akan terjadi dalam aplikasi Anda.
Tetapi tunggu, pengecualian yang diperiksa sebenarnya sangat menjengkelkan, sehingga mereka bahkan tidak akan membiarkan Anda melakukan yang sebaliknya! Bayangkan saat ini Anda menangkap setiap panggilan yang IOException
dilemparkan write
pada OutputStream
, tetapi Anda ingin mengubah tipe variabel yang dideklarasikan menjadi a ByteArrayOutputStream
, kompiler akan mencaci Anda karena mencoba untuk menangkap pengecualian yang dicentang yang dikatakan tidak dapat dibuang.
Aturan itu menyebabkan beberapa masalah yang tidak masuk akal. Misalnya, salah satu dari tiga write
metode OutputStream
yang tidak diganti oleh ByteArrayOutputStream
. Secara khusus, write(byte[] data)
adalah metode kenyamanan yang menulis array penuh dengan memanggil write(byte[] data, int offset, int length)
dengan offset 0 dan panjang array. ByteArrayOutputStream
mengganti metode tiga argumen tetapi mewarisi metode kenyamanan satu argumen apa adanya. Metode yang diwariskan melakukan hal yang tepat, tetapi termasuk throws
klausa yang tidak diinginkan . Itu mungkin sebuah kekeliruan dalam desain ByteArrayOutputStream
, tetapi mereka tidak pernah bisa memperbaikinya karena itu akan merusak kompatibilitas sumber dengan kode apa pun yang menangkap pengecualian - pengecualian yang tidak pernah, tidak pernah, dan tidak akan pernah dilempar!
Aturan itu mengganggu saat mengedit dan debugging juga. Misalnya, kadang-kadang saya akan mengomentari panggilan metode sementara, dan jika itu bisa melempar pengecualian diperiksa, kompiler sekarang akan mengeluh tentang keberadaan lokal try
dan catch
blok. Jadi saya harus berkomentar juga, dan sekarang ketika mengedit kode di dalam, IDE akan masuk ke level yang salah karena {
dan }
dikomentari. Gah! Ini keluhan kecil tapi sepertinya satu-satunya pengecualian yang pernah dilakukan adalah menyebabkan masalah.
Saya hampir selesai. Frustrasi terakhir saya dengan pengecualian yang diperiksa adalah bahwa di sebagian besar situs panggilan , tidak ada yang berguna yang dapat Anda lakukan dengan mereka. Idealnya ketika terjadi kesalahan, kami akan memiliki penangan khusus aplikasi yang kompeten yang dapat memberi tahu pengguna tentang masalah dan / atau mengakhiri atau mencoba ulang operasi sebagaimana mestinya. Hanya pawang tinggi di atas tumpukan yang dapat melakukan ini karena itu adalah satu-satunya yang tahu tujuan keseluruhan.
Sebagai gantinya, kami mendapatkan idiom berikut, yang merajalela sebagai cara untuk menutup kompilator:
try {
...
} catch (SomeStupidExceptionOmgWhoCares e) {
e.printStackTrace();
}
Dalam GUI atau program otomatis pesan yang dicetak tidak akan terlihat. Lebih buruk lagi, ia melanjutkan dengan sisa kode setelah pengecualian. Apakah pengecualian itu sebenarnya bukan kesalahan? Maka jangan cetak itu. Jika tidak, sesuatu yang lain akan meledak sebentar, pada saat objek pengecualian asli akan hilang. Ungkapan ini tidak lebih baik dari BASIC On Error Resume Next
atau PHP error_reporting(0);
.
Memanggil semacam kelas logger tidak jauh lebih baik:
try {
...
} catch (SomethingWeird e) {
logger.log(e);
}
Itu sama malas e.printStackTrace();
dan masih bajak dengan kode dalam keadaan tak tentu. Plus, pilihan sistem pencatatan tertentu atau penangan lain adalah spesifik-aplikasi, jadi ini menyakitkan untuk digunakan kembali.
Tapi tunggu! Ada cara mudah dan universal untuk menemukan penangan khusus aplikasi. Ini lebih tinggi dari tumpukan panggilan (atau diatur sebagai pengendali pengecualian Thread yang tidak tertangkap ). Jadi di sebagian besar tempat, yang perlu Anda lakukan adalah melemparkan pengecualian lebih tinggi ke tumpukan . Misalnya throw e;
,. Pengecualian diperiksa hanya menghalangi.
Saya yakin pengecualian yang diperiksa terdengar seperti ide yang bagus ketika bahasa dirancang, tetapi dalam praktiknya saya merasa semuanya mengganggu dan tidak bermanfaat.