Selasa, 22 Oktober 2013

Pengecualian dan Pernyataan "try ... catch"

Versi ramah cetakVersi ramah cetak
Membuat program untuk bekerja dalam kondisi ideal jauh lebih mudah daripada membuat program yang tangguh. Program tangguh dapat tahan dari kondisi yang tidak biasa atau dalam kondisi "pengecualian". Salah satu pendekatanya adalah dengan melihat kemungkinan masalah yang mungkin terjadi dan menambahkan tes/pengujian pada program tersebut untuk setiap kemungkinan masalah yang mungkin terjadi.
Misalnya, program akan berhenti jika mencoba mengakses elemen array A[i], di mana i tidak berada di dalam rentang yang dibolehkan. Program tangguh harus dapat mengantisipasi kemungkinan adanya indeks yang tak masuk akal dan menjaganya dari kemungkinan itu. Misalnya bisa dilakukan dengan :
if (i < 0 || i >= A.length) {
    ...  // Lakukan sesuatu untuk menangani indeks i diluar rentang
} else {
    ...  // Proses elemen A[i]
}
Ada beberapa masalah yang mungkin terjadi dengan pendekatan seperti ini. Adalah hal yang sangat sulit dan kadang kala tidak mungkin untuk mengantisipasi segala kemungkinan yang dapat terjadi. Kadang tidak selalu jelas apa yang harus dilakukan apabila suatu kesalahan ditemui. Untuk mengantisipasi semua masalah yang mungkin terjadi bisa jadi membuat program sederhana menjadi lautan pernyataan if.
Java (seperti C++) membutuhkan metode alternatif yang lebih rapi dan terstruktur untuk menghadapi masalah yang mungkin terjadi ketika program dijalankan. Metode ini disebut sebagai penanganan pengecualian (exception-handling). Kata "pengecualian" diartikan sesuatu yang lebih umum daripada "kesalahan". Termasuk di antaranya adalah kondisi yang mungkin terjadi yang berada di luar aliran suatu program. Pengecualian, bisa berupa kesalahan, atau bisa juga kasus tertentu yang kita inginkan terpisah dari algoritma kita.
Ketika pengecualian terjadi dalam eksekusi suatu program, kita sebut bahwa pengecualian tersebut di-lempar-kan (thrown). Ketika ini terhadi, aliran program artinya terlempar dari jalurnya, dan program berada dalam bahaya akan crash. Akan tetapi crash bisa dihindari jika pengecualian tersebut ditangkap (catch) dan ditangani dengan cara tertentu. Suatu pengecualian dapat dilempar oleh satu bagian program dan ditangkap oleh bagian program lain. Pengecualian yang tidak ditangkap secara umum akan menyebabkan program berhenti. (Lebih tepat apabila disebut thread yang melemparkan pengecualian tersebut akan berhenti. Dalam program multithreading, mungkin saja thread lain akan terus berjalan apabila thread yang satu berhenti.)
Karena program Java dijalankan di dalam interpreter, program yang crash berarti bahwa program tersebut berhenti berjalan secara prematur. Tidak berarti bahwa interpreter juga akan crash. Atau dengan kata lain, interpreter akan menangkap pengecualian yang tidak ditangkap oleh program. Interpreter akan merespon dengan menghentikan jalannya program. Dalam banyak bahasa pemrograman lainnya, program yang crash sering menghentikan seluruh sistem hingga kita menekan tombol reset. Dalam Java, kejadian seperti itu tidak mungkin -- yang artinya ketika hal itu terjadi, maka yang salah adalah komputer kita, bukan program kita.

Ketika pengecualian terjadi, yang terjadi adalah program tersebut melemparkan suatu objek. Objek tersebut membawa informasi (dalam variabel instansinya) dari tempat di mana pengecualian terjadi ke titik di mana ia bisa ditangkap dan ditangani. Informasi ini selalu terdiri dari tumpukan panggilan subrutin (subrutin call stack), yaitu daftar di mana dan dari mana subrutin tersebut dipanggil dan kapan pengecualian tersebut dilemparkan. (Karena suatu subrutin bisa memanggil subrutin yang lain, beberapa subrutin bisa aktif dalam waktu yang sama.) Biasanya, objek pengecualian juga memiliki pesan kesalahan mengapa ia dilemparkan, atau bisa juga memiliki data lain. Objek yang dilemparkan harus diciptakan dari kelas standarjava.lang.Throwable atau kelas turunannya.
Secara umum, setiap jenis pengecualian dikelompokkan dalam kelas turunan ThrowableThrowable memiliki dua kelas turunan langsung, yaitu Error dan Exception. Kedua kelas turunan ini pada akhirnya memiliki banyak kelas turunan lain. Kita juga bisa membuat kelas pengecualian baru untuk melambangkan jenis pengecualian baru.
Kebanyakan turunan dari kelas Error merupakan kesalahan serius dalam mesin virtual Java yang memang seharusnya menyebabkan berhentinya program karena sudah tidak dapat diselamatkan lagi. Kita sebaiknya tidak mencoba untuk menangkap dan menangani kesalahan jenis ini. Misalnya ClassFormatError dilempar karena mesin virtual Java menemukan data ilegal dalam suatu file yang seharusnya berisi kelas Java yang sudah terkompilasi. Jika kelas tersebut dipanggil ketika program sedang berjalan, maka kita tidak bisa melanjutkan program tersebut sama sekali.
Di lain pihak, kelas turunan dari kelas Exception melambangkan pengecualian yang memang seharusnya ditangkap. Dalam banyak kasus, pengecualian seperti ini adalah sesuatu yang mungkin biasa disebut "kesalahan", akan tetapi kesalahan seperti ini adalah jenis yang bisa ditangani dengan cara yang baik. (Akan tetapi, jangan terlalu bernafsu untuk menangkap semua kesalahan hanya karena kita ingin program kita tidak crash. Jika kita tidak memiliki cara untuk menanganinya, mungkin menangkap pengecualian dan membiarkannya akan menyebabkan masalah lain di tempat lain).
Kelas Exception memiliki kelas turunan lainnnya, misalnya RuntimeException. Kelas ini mengelompokkkan pengecualian umum misalnya ArithmeticException yang terjadi misalnya ada pembagian dengan nol, ArrayIndexOutOfBoundsExceptionyang terjadi jika kita mencoba mengakses indeks array di luar rentang yang diijinkan, dan NullPointerException yang terjadi jika kita mencoba menggunakan referensi ke null di mana seharusnya referensi objek diperlukan.
RuntimeException biasanya menginidikasikan adanya bug dalam program yang harus diperbaiki oleh programmer.RuntimeException dan Error memiliki sifat yang sama yaitu program bisa mengabaikannya. ("Mengabaikan" artinya kita membiarkan program crash jika pengecualian tersebut terjadi). Misalnya, program yang melemparkanArrayIndexOutOfBoundsException dan tidak menanganinya akan menghentikan program saat itu juga. Untuk pengecualian lain selain Error dan RuntimeException beserta kelas turunannya, pengecualian wajib ditangani.
Diagram berikut menggambarkan hirarki suatu kelas turunan dari kelas Throwable. Kelas yang membutuhkan penanganan pengecualian wajib ditunjukkan dalam kotak merah.


Untuk menangkap pengecualian pada program Java, kita menggunakan pernyataan try. Maksudnya memberi tahu komputer untuk "mencoba" (try) menjalankan suatu perintah. Jika berhasil, semuanya akan berjalan seperti biasa. Tapi jika pengecualian dilempar pada saat mencoba melaksanakan perintah tersebut, kita bisa menangkapnya dan menanganinya. Misalnya,
try {
    double determinan = M[0][0]*M[1][1] - M[0][1]*M[1][0];
    System.out.println("Determinan matriks M adalah " + determinan);
}
catch ( ArrayIndexOutOfBoundsException e ) {
    System.out.println("Determinan M tidak bisa dihitung karena ukuran M salah.");
}
Komputer mencoba menjalankan perintah di dalam blok setelah kata "try". Jika tidak ada pengecualian, maka bagian "catch" akan diabaikan. Akan tetapi jika ada pengecualian ArrayIndexOutOfBoundsException, maka komputer akan langsung lompat ke dalam blok setelah pernyataan "catch (ArrayIndexOutOfBoundsException)". Blok ini disebut blok yang menangani pengecualian (exception handler) untuk pengecualian ArrayIndexOutOfBoundsException". Dengan cara ini, kita mencegah program berhenti tiba-tiba.
Mungkin kita akan sadar bahwa ada kemungkinan kesalahan lagi dalam blok di dalam pernyataan try. Jika variabel M berisinull, maka pengecualian NullPointerException akan dilemparkan. Dalam pernyataan try di atas, pengecualianNullPointerException tidak ditangkap, sehingga akan diproses seperti biasa (yaitu menghentikan program saat itu juga, kecuali pengecualian ini ditangkap di tempat lain). Kita bisa menangkap pengecualian NullPointerException dengan menambah klausa catch lain, seperti :
try {
    double determinan = M[0][0]*M[1][1] - M[0][1]*M[1][0];
    System.out.println("Determinan matriks M adalah " + determinan);
}
catch ( ArrayIndexOutOfBoundsException e ) {
    System.out.println("Determinan M tidak bisa dihitung karena ukuran M salah.");
}
catch ( NullPointerException e ) {
    System.out.print("Kesalahan program!  M tidak ada:  " + );
    System.out.println( e.getMessage() );
}
Contoh ini menunjukkan bagaimana caranya menggunakan beberapa klausa catche adalah nama variabel (bisa kita ganti dengan nama apapun terserah kita). Ingat kembali bahwa ketika pengecualian terjadi, sebenarnya yang dilemparkan adalah objek. Sebelum menjalankan klausa catch, komputer mengisi variabel ini dengan objek yang akan ditangkap. Objek ini mengandung informasi tentang pengecualian tersebut.
Misalnya, pesan kesalahan yang menjelaskan tentang pengecualian ini bisa diambil dengan metode getMessage() seperti contoh di atas. Metode ini akan mencetak daftar subrutin yang dipanggil sebelum pengecualian ini dilempar. Informasi ini bisa menolong kita untuk melacak dari mana kesalahan terjadi.
Ingat bahwa baik ArrayIndexOutOfBoundsException dan NullPointerException adalah kelas turunan dariRuntimeException. Kita bisa menangkap semua pengecualian dalam kelas RuntimeException dengan klausa catch tunggal, misalnya :
try {
    double determinan = M[0][0]*M[1][1] - M[0][1]*M[1][0];
    System.out.println("Determinan matriks M adalah " + determinan);
}
catch ( RuntimeException e ) {
    System.out.println("Maaf, ada kesalahan yang terjadi.");
    e.printStackTrace();
}
Karena objek yang bertipe ArrayIndexOutOfBoundsException maupun NullPointerException juga bertipeRuntimeException, maka perintah seperti di atas akan menangkap kesalahan indeks maupun kesalahan pointer kosong dan juga pengecualian runtime yang lain. Ini menunjukkan mengapa kelas pengecualian disusun dalam bentuk hirarki kelas. Kita bisa membuat klausa catch secara spesifik hingga tahu persis apa yang salah, atau menggunakan klausa penangkap umum.
Contoh di atas mungkin tidak begitu realistis. Sepertinya kita jarang harus menangani kesalahan indeks ataupun kesalahan pointer kosong seperti di atas. Masalahnya mungkin terlalu banyak, dan mungkin kita akan bosan jika harus menulis try ...catch setiap kali kita menggunakan array. Yang penting kita mengisi variabel tersebut dengan sesuatu yang bukan null dan menjaga agar program tidak keluar indeks sudah cukup. Oleh karena itu kita sebut penanganan ini tidak wajib. Akan ada banyak hal yang bisa jadi masalah. (Makanya penanganan pengecualian tidak menyebabkan program makin tangguh. Ia hanya memberikan alat yang mungkin kita gunakan dengan cara memberi tahu di mana kesalahan mungkin muncul).
Bentuk pernyataan try sebenarnya lebih kompleks dari contoh di atas. Sintaksnya secara umum dapat ditulis seperti:
try {
    perintah
}
klausa_catch_tambahan
klausa_finally_tambahan
Ingat bahwa di sini blok ( yaitu { dan } ) diperlukan, meskipun jika perintah hanya terdiri dari satu perintah. Pernyataan tryboleh tidak dimasukkan, dan juga klausa finally boleh ada boleh tidak. (Pernyataan try harus memiliki satu klausa finallyatau catch). Sintaks dari klausa catch adalah:
catch (nama_kelas_pengecualian nama_variabel) {
    pernyataan
}
dan sintaks klausa finally adalah
finally {
    pernyataan
}
Semantik dari klausa finally yaitu pernyataan di dalam klausa finally akan dijamin untuk dilaksanakan sebagai perintah akhir dari pernyataan try, tidak peduli apakah ada pengecualian yang dilempar atau tidak. Pada dasarnya klausa finallydimaksudkan untuk melakukan langkah pembersihan yang tidak boleh dihilangkan.

Ada beberapa kejadian di mana suatu program memang harus melempar pengecualian. Misalnya apabila program tersebut menemukan adanya kesalahan pengurutan atau kesalahan lain, tapi tidak ada cara yang tepat untuk menanganinya di tempat di mana kesalahan tersebut ditemukan. Program bisa melempar pengecualian dengan harapan di bagian lain pengecualian ini akan ditangkap dan diproses.
Untuk melempar pengecualian, kita bisa menggunakan pernyataan throw dengan sintaks :
throw objek_pengecualian;
objek_pengecualian harus merupakan objek yang bertipe kelas yang diturunkan dari Throwable. Biasanya merupakan kelas turunan dari kelas Exception. Dalam banyak kasus, biasanya objek tersebut dibuat dengan operator new, misalnya :
throw new ArithmeticException("Pembagian dengan nol");
Parameter dalam konstruktor adalah pesan kesalahan dalam objek pengecualian.
Pengecualian bisa dilempar baik oleh sistem (karena terjadinya kesalahan) atau oleh pernyataan throw. Pengecualian ini ditangani dengan cara yang sama. Misalnya suatu pengecualian dilempar dari pernyataan try. Jika pernyataan try tersebut memiliki klausa catch untuk menangani tipe pengecualian tersebut, maka klausa ini akan melompat ke klausa catch dan menjalankan perintah di dalamnya. Pengecualian tersebut telah ditangani.
Setelah menangani pengecualian tersebut, komputer akan menjalankan klausa finally, jika ada. Kemudian program akan melanjutkan program seperti biasa. Jika suatu pengecualian tidak ditangkap dan ditangani, maka pengolahan pengecualian akan berlanjut.
Jika pengecualian dilempar pada saat eksekusi suatu subrutin dan pengecualian tersebut tidak ditangani di dalam subrutin yang sama, maka subrutin tersebut akan dihentikan (setelah menjalankan klausa finally, jika tersedia). Kemudian rutin yang memanggil subrutin tersebut memiliki kesempatan untuk menangani pengecualian tersebut. Artinya, jika subrutin tersebut dipanggil di dalam pernyataan try dan memiliki klausa catch yang cocok, maka klausa catch tersebut akan dieksekusi dan program akan berlanjut seperti biasa.
Lagi-lagi jika rutin tersebut tidak menangani pengecualian tersebut, rutin tersebut akan dihentikan dan rutin yang memanggilnya akan mencoba menangani pengecualian tersebut. Pengecualian akan menghentikan program secara keseluruhan jika keseluruhan rantai panggil subrutin hingga main() tidak menangani pengecualian tersebut.
Suatu subrutin yang mungkin melempar pengecualian dapat memberi tahu dengan menambahkan klausa "throwsnama_kelas_pengecualian" pada definisi suatu subrutin. Misalnya :
static double akar(double A, double B, double C)
    throws IllegalArgumentException {
    // Menghasilkan akar yang lebih besar
    // dari persamaan kuadrat A*x*x + B*x + C = 0.
    // (Lempar pengecualian jika A == 0 atau B*B-4*A*C < 0.)
    if (A == 0) {
        throw new IllegalArgumentException("A tidak boleh nol.");
    }
    else {
        double diskriminan = B*B - 4*A*C;
        if (diskriminan < 0)
            throw new IllegalArgumentException("Diskriminan < nol.");
            return  (-B + Math.sqrt(diskriminan)) / (2*A);
    }
}
Seperti kita diskusikan pada bagian sebelumnya, perhitungan dalam subrutin ini memiliki kondisi awal di mana != 0 dan B*B-4*A*>= 0. Subrutin akan melempar pengecualian dengan tipe IllegalArgumentException jika salah satu dari kondisi awal ini tidak dipenuhi. Jika kondisi ilegal ditemukan dalam suatu subrutin, melempar pengecualian kadang kala menjadi pilihan yang lebih bijaksana. Jika program yang memanggil subrutin tersebut mengetahui bagaimana cara yang tepat untuk menangani pengecualian tersebut, program tersebut dapat menangkapnya. JIka tidak, maka program akan crash dan programmer akan tahu apa yang harus diperbaiki.


Penanganan Pengecualian Wajib

Dalam contoh-contoh sebelumnya, mendeklarasikan subrutin akar() yang dapat melempar pengecualianIllegalArgumentException adalah sesuatu yang "sopan" untuk pembaca rutin tersebut. MenanganiIllegalArgumentException bukan sesuatu yang "wajib" dilakukan. Subruin dapat melempar IllegalArgumentExceptiontanpa mengumumkan kemungkinan tersebut. Kemudian program yang memanggil rutin tersebut bebas untuk menangkap atau membiarkan pengecualian yang dilempar oleh rutin tersebut. Begitu juga halnya dengan tipe NullPointerException[/code, programmer bebas untuk menangkap atau mengabaikan pengecualian tersebut.</p><p>Untuk kelas pengecualian yang mewajibkan penanganan pengecualian, siatuasinya sedikit berbeda. Jika suatu subrutin bisa melempar pengecualian seperti ini, maka klausa [code]throws harus ditulis pada definisi subrutin. Jika tidak, maka compiler akan menampilkan kesalahan sintaks.
Di sisi lain, misalnya suatu pernyataan dalam program dapat melemparkan pengecualian yang mewajibkan penanganan pengecualian. Pernyataan tersebut bisa berasal dari pernyataan throw yang sengaja dilemparkan atau hasil dari pemanggilan suatu subrutin yang melempar pengecualian tersebut. Pengecualian ini harus ditangani dengan salah satu dari dua cara, yaitu : Menggunakan pernyataan try yang memiliki klausa catch untuk menangani pengecualian tersebut. Atau dengan menambahkan klausa throws di kepala definisi subrutin.
Jika klausa throws digunakan, maka subrutin lain yang memanggil subrutin kita akan bertanggung jawab menangani pengecualian tersebut. Jika kita tidak menangani pengecualian tersebut dengan cara-cara di atas, maka Java akan menganggap sebagai kesalahan sintaks.
Penanganan pengecualian menjadi wajib untuk kelas yang bukan kelas turunan dari Error atau RuntimeException. Pengecualian yang wajib biasanya merupakan kondisi yang berada di luar jangkauan programmer. Misalnya, input ilegal atau aksi ilegal oleh user. Program tangguh harus bisa menangani kondisi seperti ini. Desain Java tidak memungkinkan programmer untuk mengabaikan kondisi tersebut.
Di antara pengecualian yang mewajibkan penanganan pengecualian adalah yang berhubungan dengan rutin input/output. Artinya kita tidak boleh menggunakan rutin ini jika kita mengetahui bagaimana menangani pengecualian. Bagian berikutnya akan membahas tentang contoh-contoh input/output dan penanganan pengecualian dengan lebih gamblang.

Tidak ada komentar:

Posting Komentar