Saya pikir Anda mungkin pernah mengalami situasi di mana Anda menjalankan kode dan berakhir dengan sesuatu seperti NullPointerException , ClassCastException , atau lebih buruk... Ini diikuti dengan proses panjang debugging, analisis, googling, dan sebagainya. Pengecualian memang bagus: mereka menunjukkan sifat masalah dan di mana itu terjadi. Jika Anda ingin menyegarkan ingatan dan belajar sedikit lagi, lihat artikel ini: Pengecualian: dicentang, tidak dicentang, dan dikustomisasi .

Yang mengatakan, mungkin ada situasi ketika Anda perlu membuat pengecualian Anda sendiri. Misalnya, kode Anda perlu meminta informasi dari layanan jarak jauh yang tidak tersedia karena alasan tertentu. Atau misalkan seseorang mengisi aplikasi untuk kartu bank dan memberikan nomor telepon yang, entah secara tidak sengaja atau tidak, sudah dikaitkan dengan pengguna lain di sistem.

Tentu saja, perilaku yang benar di sini masih bergantung pada persyaratan pelanggan dan arsitektur sistem, tetapi anggaplah Anda ditugaskan untuk memeriksa apakah nomor telepon sudah digunakan dan memberikan pengecualian jika sudah.

Mari buat pengecualian:


public class PhoneNumberAlreadyExistsException extends Exception {

   public PhoneNumberAlreadyExistsException(String message) {
       super(message);
   }
}
    

Selanjutnya kita akan menggunakannya saat kita melakukan pemeriksaan:


public class PhoneNumberRegisterService {
   List<String> registeredPhoneNumbers = Arrays.asList("+1-111-111-11-11", "+1-111-111-11-12", "+1-111-111-11-13", "+1-111-111-11-14");

   public void validatePhone(String phoneNumber) throws PhoneNumberAlreadyExistsException {
       if (registeredPhoneNumbers.contains(phoneNumber)) {
           throw new PhoneNumberAlreadyExistsException("The specified phone number is already in use by another customer!");
       }
   }
}
    

Untuk menyederhanakan contoh kami, kami akan menggunakan beberapa nomor telepon hardcode untuk mewakili database. Dan terakhir, mari coba gunakan pengecualian kami:


public class CreditCardIssue {
   public static void main(String[] args) {
       PhoneNumberRegisterService service = new PhoneNumberRegisterService();
       try {
           service.validatePhone("+1-111-111-11-14");
       } catch (PhoneNumberAlreadyExistsException e) {
           // Here we can write to logs or display the call stack
		e.printStackTrace();
       }
   }
}
    

Dan sekarang saatnya menekan Shift+F10 (jika Anda menggunakan IDEA), yaitu menjalankan proyek. Inilah yang akan Anda lihat di konsol:

exception.CreditCardIssue
exception.PhoneNumberAlreadyExistsException: Nomor telepon yang ditentukan sudah digunakan oleh pelanggan lain!
di pengecualian.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Lihat dirimu! Anda membuat pengecualian Anda sendiri dan bahkan mengujinya sedikit. Selamat atas pencapaian ini! Saya sarankan sedikit bereksperimen dengan kode untuk lebih memahami cara kerjanya.

Tambahkan centang lain — misalnya, periksa apakah nomor telepon menyertakan huruf. Seperti yang mungkin Anda ketahui, huruf sering digunakan di Amerika Serikat untuk membuat nomor telepon lebih mudah diingat, misalnya 1-800-MY-APPLE. Cek Anda dapat memastikan bahwa nomor telepon hanya berisi angka.

Oke, jadi kami telah membuat pengecualian yang dicentang. Semua akan baik-baik saja dan bagus, tapi...

Komunitas pemrograman dibagi menjadi dua kubu — mereka yang mendukung pengecualian yang diperiksa dan mereka yang menentangnya. Kedua belah pihak membuat argumen yang kuat. Keduanya termasuk pengembang terkemuka: Bruce Eckel mengkritik pengecualian yang diperiksa, sementara James Gosling membela mereka. Sepertinya masalah ini tidak akan pernah diselesaikan secara permanen. Yang mengatakan, mari kita lihat kerugian utama menggunakan pengecualian yang diperiksa.

Kerugian utama dari pengecualian yang diperiksa adalah bahwa mereka harus ditangani. Dan di sini kita memiliki dua opsi: tangani di tempat menggunakan try-catch , atau, jika kita menggunakan pengecualian yang sama di banyak tempat, gunakan lemparan untuk memunculkan pengecualian, dan memprosesnya di kelas tingkat atas.

Selain itu, kita mungkin berakhir dengan kode "boilerplate", yaitu kode yang menghabiskan banyak ruang, tetapi tidak terlalu berat.

Masalah muncul dalam aplikasi yang cukup besar dengan banyak pengecualian yang ditangani: daftar lemparan pada metode tingkat atas dapat dengan mudah berkembang hingga mencakup selusin pengecualian.

public OurCoolClass() melempar FirstException, SecondException, ThirdException, ApplicationNameException...

Pengembang biasanya tidak menyukai ini dan malah memilih trik: mereka membuat semua pengecualian yang diperiksa mewarisi nenek moyang yang sama — ApplicationNameException . Sekarang mereka juga harus menangkap pengecualian itu ( dicentang !) Dalam sebuah handler:


catch (FirstException e) {
    // TODO
}
catch (SecondException e) {
    // TODO
}
catch (ThirdException e) {
    // TODO
}
catch (ApplicationNameException e) {
    // TODO
}
    

Di sini kita menghadapi masalah lain — apa yang harus kita lakukan di blok tangkapan terakhir ? Di atas, kami telah memproses semua situasi yang diharapkan, jadi pada titik ini ApplicationNameException tidak lebih berarti bagi kami selain " Pengecualian : terjadi kesalahan yang tidak dapat dipahami". Inilah cara kami menanganinya:


catch (ApplicationNameException e) {
    LOGGER.error("Unknown error", e.getMessage());
}
    

Dan pada akhirnya, kita tidak tahu apa yang terjadi.

Tapi tidak bisakah kita membuang semua pengecualian sekaligus, seperti ini?


public void ourCoolMethod() throws Exception {
// Do some work
}
    

Ya, kami bisa. Tapi apa yang dikatakan "melempar Pengecualian" kepada kita? Bahwa ada sesuatu yang rusak. Anda harus menyelidiki semuanya dari atas ke bawah dan terbiasa dengan debugger untuk waktu yang lama untuk memahami alasannya.

Anda mungkin juga menemukan konstruksi yang terkadang disebut "pengecualian menelan":


try {
// Some code
} catch(Exception e) {
   throw new ApplicationNameException("Error");
}
    

Tidak banyak yang bisa ditambahkan di sini sebagai penjelasan — kode membuat semuanya jelas, atau lebih tepatnya, membuat semuanya tidak jelas.

Tentu saja, Anda mungkin mengatakan bahwa Anda tidak akan melihat ini dalam kode sebenarnya. Baiklah, mari kita intip ke dalam (kode) kelas URL dari paket java.net . Ikuti saya jika Anda ingin tahu!

Berikut adalah salah satu konstruksi di kelas URL :


public URL(String spec) throws MalformedURLException {
   this(null, spec);
}
    

Seperti yang Anda lihat, kami memiliki pengecualian yang menarik — MalformedURLException . Di sinilah mungkin dilemparkan (dan saya kutip):
"jika tidak ada protokol yang ditentukan, atau protokol yang tidak dikenal ditemukan, atau spek adalah nol, atau URL yang diuraikan gagal untuk mematuhi sintaks spesifik dari protokol terkait."

Itu adalah:

  1. Jika tidak ada protokol yang ditentukan.
  2. Protokol yang tidak dikenal ditemukan.
  3. Spesifikasinya nol .
  4. URL tidak sesuai dengan sintaks spesifik dari protokol terkait.

Mari buat metode yang membuat objek URL :


public URL createURL() {
   URL url = new URL("https://codegym.cc");
   return url;
}
    

Segera setelah Anda menulis baris-baris ini di IDE (saya membuat kode di IDEA, tetapi ini berfungsi bahkan di Eclipse dan NetBeans), Anda akan melihat ini:

Ini berarti kita perlu membuang pengecualian, atau membungkus kode dalam blok try-catch . Untuk saat ini, saya sarankan memilih opsi kedua untuk memvisualisasikan apa yang terjadi:


public static URL createURL() {
   URL url = null;
   try {
       url = new URL("https://codegym.cc");
   } catch(MalformedURLException e) {
  e.printStackTrace();
   }
   return url;
}
    

Seperti yang Anda lihat, kodenya sudah agak bertele-tele. Dan kami menyinggung itu di atas. Ini adalah salah satu alasan paling jelas untuk menggunakan pengecualian yang tidak dicentang.

Kami dapat membuat pengecualian yang tidak dicentang dengan memperluas RuntimeException di Java.

Pengecualian yang tidak dicentang diwariskan dari kelas Error atau kelas RuntimeException . Banyak pemrogram merasa bahwa pengecualian ini dapat ditangani dalam program kami karena ini menunjukkan kesalahan yang tidak dapat kami harapkan untuk dipulihkan saat program sedang berjalan.

Ketika pengecualian yang tidak dicentang terjadi, biasanya disebabkan oleh penggunaan kode yang salah, meneruskan argumen yang null atau tidak valid.

Baiklah, mari kita tulis kodenya:


public class OurCoolUncheckedException extends RuntimeException {
   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }
  
   public OurCoolUncheckedException(String message, Throwable throwable) {
       super(message, throwable);
   }
}
    

Perhatikan bahwa kami membuat beberapa konstruktor untuk tujuan yang berbeda. Ini memungkinkan kami memberikan pengecualian lebih banyak kemampuan. Misalnya, kita dapat membuatnya agar pengecualian memberi kita kode kesalahan. Untuk memulainya, mari buat enum untuk mewakili kode kesalahan kita:


public enum ErrorCodes {
   FIRST_ERROR(1),
   SECOND_ERROR(2),
   THIRD_ERROR(3);

   private int code;

   ErrorCodes(int code) {
       this.code = code;
   }

   public int getCode() {
       return code;
   }
}
    

Sekarang mari tambahkan konstruktor lain ke kelas pengecualian kita:


public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
   super(message, cause);
   this.errorCode = errorCode.getCode();
}
    

Dan jangan lupa menambahkan field (kami hampir lupa):


private Integer errorCode;
    

Dan tentu saja, metode untuk mendapatkan kode ini:


public Integer getErrorCode() {
   return errorCode;
}
    

Mari kita lihat keseluruhan kelas sehingga kita dapat memeriksa dan membandingkannya:

public class OurCoolUncheckedException extends RuntimeException {
   private Integer errorCode;

   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }

   public OurCoolUncheckedException(String message, Throwable throwable) {

       super(message, throwable);
   }

   public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
       super(message, cause);
       this.errorCode = errorCode.getCode();
   }
   public Integer getErrorCode() {
       return errorCode;
   }
}
    

Ta-da! Pengecualian kami selesai! Seperti yang Anda lihat, tidak ada yang rumit di sini. Mari kita periksa dalam tindakan:


   public static void main(String[] args) {
       getException();
   }
   public static void getException() {
       throw new OurCoolUncheckedException("Our cool exception!");
   }
    

Saat kami menjalankan aplikasi kecil kami, kami akan melihat sesuatu seperti berikut di konsol:

Sekarang mari kita manfaatkan fungsi ekstra yang telah kita tambahkan. Kami akan menambahkan sedikit ke kode sebelumnya:


public static void main(String[] args) throws Exception {

   OurCoolUncheckedException exception = getException(3);
   System.out.println("getException().getErrorCode() = " + exception.getErrorCode());
   throw exception;

}

public static OurCoolUncheckedException getException(int errorCode) {
   return switch (errorCode) {
   case 1:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.FIRST_ERROR.getCode(), new Throwable(), ErrorCodes.FIRST_ERROR);
   case 2:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.SECOND_ERROR.getCode(), new Throwable(), ErrorCodes.SECOND_ERROR);
   default: // Since this is the default action, here we catch the third and any other codes that we have not yet added. You can learn more by reading Java switch statement
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.THIRD_ERROR.getCode(), new Throwable(), ErrorCodes.THIRD_ERROR);
}

}
    

Anda dapat bekerja dengan pengecualian dengan cara yang sama seperti Anda bekerja dengan objek. Tentu saja, saya yakin Anda sudah tahu bahwa semua yang ada di Java adalah objek.

Dan lihat apa yang kami lakukan. Pertama, kami mengubah metode, yang sekarang tidak melempar, tetapi hanya membuat pengecualian, tergantung pada parameter input. Selanjutnya, dengan menggunakan pernyataan switch-case , kami membuat pengecualian dengan kode dan pesan kesalahan yang diinginkan. Dan dalam metode utama, kami mendapatkan pengecualian yang dibuat, mendapatkan kode kesalahan, dan membuangnya.

Mari jalankan ini dan lihat apa yang kita dapatkan di konsol:

Lihat — kami mencetak kode kesalahan yang kami dapatkan dari pengecualian dan kemudian melemparkan pengecualian itu sendiri. Terlebih lagi, kami bahkan dapat melacak dengan tepat di mana pengecualian dilemparkan. Jika diperlukan, Anda dapat menambahkan semua informasi yang relevan ke pesan, membuat kode kesalahan tambahan, dan menambahkan fitur baru ke pengecualian Anda.

Nah, apa pendapat Anda tentang itu? Saya harap semuanya berhasil untuk Anda!

Secara umum, pengecualian adalah topik yang agak luas dan tidak jelas. Akan ada lebih banyak perselisihan tentang itu. Misalnya, hanya Java yang memeriksa pengecualian. Di antara bahasa yang paling populer, saya belum pernah melihat bahasa yang menggunakannya.

Bruce Eckel menulis dengan sangat baik tentang pengecualian di bab 12 dari bukunya "Thinking in Java" — Saya sarankan Anda membacanya! Lihat juga volume pertama "Core Java" karya Horstmann — Ini juga memiliki banyak hal menarik di bab 7.

Ringkasan kecil

  1. Tulis semuanya ke log! Pesan log dalam pengecualian yang dilemparkan. Ini biasanya akan banyak membantu dalam proses debug dan memungkinkan Anda untuk memahami apa yang terjadi. Jangan biarkan blok tangkap kosong, jika tidak blok itu hanya akan "menelan" pengecualian dan Anda tidak akan memiliki informasi apa pun untuk membantu Anda mencari masalah.

  2. Dalam hal pengecualian, adalah praktik yang buruk untuk menangkap semuanya sekaligus (seperti yang dikatakan rekan saya, "ini bukan Pokemon, ini Java"), jadi hindari catch (Exception e) atau lebih buruk lagi, catch (Throwable t) .

  3. Lempar pengecualian sedini mungkin. Ini adalah praktik pemrograman Java yang baik. Saat Anda mempelajari kerangka kerja seperti Spring, Anda akan melihat bahwa mereka mengikuti prinsip "fail fast". Artinya, mereka "gagal" sedini mungkin untuk memungkinkan menemukan kesalahan dengan cepat. Tentu saja, ini membawa ketidaknyamanan tertentu. Tetapi pendekatan ini membantu membuat kode yang lebih kuat.

  4. Saat memanggil bagian lain dari kode, yang terbaik adalah menangkap pengecualian tertentu. Jika kode yang dipanggil melontarkan beberapa pengecualian, praktik pemrograman yang buruk hanya menangkap kelas induk dari pengecualian tersebut. Misalnya, Anda memanggil kode yang melontarkan FileNotFoundException dan IOException . Dalam kode Anda yang memanggil modul ini, lebih baik menulis dua blok catch untuk menangkap setiap pengecualian, daripada satu catch untuk menangkap Exception .

  5. Tangkap pengecualian hanya jika Anda dapat menanganinya secara efektif untuk pengguna dan untuk debugging.

  6. Jangan ragu untuk menulis pengecualian Anda sendiri. Tentu saja, Java memiliki banyak yang sudah jadi, sesuatu untuk setiap kesempatan, tetapi terkadang Anda masih perlu membuat "roda" Anda sendiri. Tetapi Anda harus memahami dengan jelas mengapa Anda melakukan ini dan memastikan bahwa kumpulan pengecualian standar belum memiliki apa yang Anda butuhkan.

  7. Saat Anda membuat kelas pengecualian Anda sendiri, berhati-hatilah dengan penamaannya! Anda mungkin sudah tahu bahwa sangat penting untuk memberi nama kelas, variabel, metode, dan paket dengan benar. Pengecualian tidak terkecuali! :) Selalu akhiri dengan kata Exception , dan nama exception harus dengan jelas menyampaikan jenis kesalahan yang diwakilinya. Misalnya, FileNotFoundException .

  8. Dokumentasikan pengecualian Anda. Kami merekomendasikan menulis tag @throws Javadoc untuk pengecualian. Ini akan sangat berguna ketika kode Anda menyediakan antarmuka apa pun. Dan Anda juga akan lebih mudah memahami kode Anda sendiri nantinya. Bagaimana menurut Anda, bagaimana Anda bisa menentukan apa itu MalformedURLException ? Dari Javadoc! Ya, pemikiran untuk menulis dokumentasi tidak terlalu menarik, tapi percayalah, Anda akan berterima kasih pada diri sendiri saat kembali ke kode Anda sendiri enam bulan kemudian.

  9. Lepaskan sumber daya dan jangan abaikan konstruk try-with-resources .

  10. Inilah ringkasan keseluruhannya: gunakan pengecualian dengan bijak. Melempar pengecualian adalah operasi yang cukup "mahal" dalam hal sumber daya. Dalam banyak kasus, mungkin lebih mudah untuk menghindari melempar pengecualian dan sebagai gantinya mengembalikan, katakanlah, variabel boolean bahwa jika operasi berhasil, menggunakan if-else yang sederhana dan "lebih murah" .

    Mungkin juga tergoda untuk mengikat logika aplikasi ke pengecualian, yang jelas tidak boleh Anda lakukan. Seperti yang kami katakan di awal artikel, pengecualian adalah untuk situasi luar biasa, bukan yang diharapkan, dan ada berbagai alat untuk mencegahnya. Secara khusus, ada Optional untuk mencegah NullPointerException , atau Scanner.hasNext dan sejenisnya untuk mencegah IOException , yang mungkin dilontarkan oleh metode read() .