Saya rasa anda mungkin pernah mengalami situasi di mana anda menjalankan kod dan berakhir dengan sesuatu seperti NullPointerException , ClassCastException atau lebih teruk lagi... Ini diikuti dengan proses penyahpepijatan, analisis, googling dan sebagainya yang panjang. Pengecualian adalah indah kerana ia menunjukkan sifat masalah dan di mana ia berlaku. Jika anda ingin menyegarkan ingatan anda dan belajar sedikit lagi, lihat artikel ini: Pengecualian: ditandai, dinyahtanda dan tersuai .

Walau bagaimanapun, mungkin terdapat situasi apabila anda perlu membuat pengecualian anda sendiri. Sebagai contoh, katakan kod anda perlu meminta maklumat daripada perkhidmatan jauh yang tidak tersedia atas sebab tertentu. Atau katakan seseorang mengisi permohonan untuk kad bank dan memberikan nombor telefon yang, sama ada secara tidak sengaja atau tidak, sudah dikaitkan dengan pengguna lain dalam sistem.

Sudah tentu, tingkah laku yang betul di sini masih bergantung pada keperluan pelanggan dan seni bina sistem, tetapi mari kita anggap bahawa anda telah ditugaskan untuk menyemak sama ada nombor telefon sudah digunakan dan membuang pengecualian jika ada.

Mari buat pengecualian:


public class PhoneNumberAlreadyExistsException extends Exception {

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

Seterusnya kami akan menggunakannya apabila kami melakukan semakan kami:


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 memudahkan contoh kami, kami akan menggunakan beberapa nombor telefon berkod keras untuk mewakili pangkalan data. Dan akhirnya, mari cuba 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 kini tiba masanya untuk menekan Shift+F10 (jika anda menggunakan IDEA), iaitu jalankan projek. Inilah yang anda akan lihat dalam konsol:

exception.CreditCardIssue
exception.PhoneNomberAlreadyExistsException: Nombor telefon yang dinyatakan sudah digunakan oleh pelanggan lain!
di exception.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Tengok awak! Anda membuat pengecualian anda sendiri dan juga mengujinya sedikit. Tahniah atas pencapaian ini! Saya mengesyorkan mencuba sedikit kod untuk memahami cara ia berfungsi dengan lebih baik.

Tambah cek lain — contohnya, semak sama ada nombor telefon termasuk huruf. Seperti yang anda ketahui, huruf sering digunakan di Amerika Syarikat untuk menjadikan nombor telefon lebih mudah diingati, contohnya 1-800-MY-APPLE. Semakan anda boleh memastikan bahawa nombor telefon mengandungi nombor sahaja.

Baiklah, jadi kami telah mencipta pengecualian yang diperiksa. Semuanya akan baik-baik saja, tetapi...

Komuniti pengaturcaraan dibahagikan kepada dua kem — mereka yang memihak kepada pengecualian yang diperiksa dan mereka yang menentangnya. Kedua-dua pihak membuat hujah yang kuat. Kedua-duanya termasuk pembangun terkemuka: Bruce Eckel mengkritik pengecualian yang diperiksa, manakala James Gosling mempertahankannya. Nampaknya perkara ini tidak akan dapat diselesaikan secara kekal. Walau bagaimanapun, mari kita lihat kelemahan utama menggunakan pengecualian yang diperiksa.

Kelemahan utama pengecualian yang diperiksa ialah ia mesti dikendalikan. Dan di sini kita mempunyai dua pilihan: sama ada mengendalikannya di tempat menggunakan try-catch , atau, jika kita menggunakan pengecualian yang sama di banyak tempat, gunakan lontaran untuk melontar pengecualian dan memprosesnya dalam kelas peringkat atasan.

Selain itu, kita mungkin akan mendapat kod "boilerplate", iaitu kod yang memakan banyak ruang, tetapi tidak melakukan banyak pengangkatan berat.

Masalah muncul dalam aplikasi yang agak besar dengan banyak pengecualian sedang dikendalikan: senarai lontaran pada kaedah peringkat atas dengan mudah boleh berkembang untuk memasukkan sedozen pengecualian.

awam OurCoolClass() membuang FirstException, SecondException, ThirdException, ApplicationNameException...

Pembangun biasanya tidak menyukai ini dan sebaliknya memilih helah: mereka menjadikan semua pengecualian mereka yang disemak mewarisi nenek moyang yang sama — ApplicationNameException . Kini mereka juga mesti menangkap pengecualian itu ( ditanda !) dalam pengendali:


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

Di sini kita menghadapi masalah lain — apakah yang perlu kita lakukan dalam blok tangkapan terakhir ? Di atas, kami telah memproses semua situasi yang dijangkakan, jadi pada ketika ini ApplicationNameException bermakna tidak lebih kepada kami daripada " Pengecualian : beberapa ralat yang tidak dapat difahami berlaku". Beginilah cara kami mengendalikannya:


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

Dan akhirnya, kita tidak tahu apa yang berlaku.

Tetapi tidakkah kita boleh membuang setiap pengecualian sekaligus, seperti ini?


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

Ya, kita boleh. Tetapi apakah yang "membuang Pengecualian" memberitahu kita? Bahawa ada yang rosak. Anda perlu menyiasat segala-galanya dari atas ke bawah dan selesa dengan penyahpepijat untuk masa yang lama untuk memahami sebabnya.

Anda juga mungkin menemui binaan yang kadangkala dipanggil "pengecualian menelan":


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

Tidak banyak yang perlu ditambah di sini melalui penjelasan — kod itu menjelaskan segala-galanya, atau sebaliknya, ia menjadikan segala-galanya tidak jelas.

Sudah tentu, anda mungkin mengatakan bahawa anda tidak akan melihat ini dalam kod sebenar. Baiklah, mari kita lihat isi perut (kod) kelas URL daripada pakej java.net . Ikuti saya jika anda ingin tahu!

Berikut ialah salah satu binaan dalam kelas URL :


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

Seperti yang anda lihat, kami mempunyai pengecualian diperiksa yang menarik — MalformedURLException . Inilah masanya ia mungkin dilemparkan (dan saya petik):
"jika tiada protokol dinyatakan, atau protokol yang tidak diketahui ditemui, atau spesifikasi adalah batal, atau URL yang dihuraikan gagal mematuhi sintaks khusus protokol yang berkaitan."

Itu dia:

  1. Jika tiada protokol dinyatakan.
  2. Protokol yang tidak diketahui ditemui.
  3. Spesifikasi adalah batal .
  4. URL tidak mematuhi sintaks khusus protokol yang berkaitan.

Mari buat kaedah yang mencipta objek URL :


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

Sebaik sahaja anda menulis baris ini dalam IDE (saya mengekod dalam IDEA, tetapi ini berfungsi walaupun dalam Eclipse dan NetBeans), anda akan melihat ini:

Ini bermakna kita perlu sama ada membuang pengecualian, atau membungkus kod dalam blok cuba-tangkap . Buat masa ini, saya cadangkan memilih pilihan kedua untuk menggambarkan apa yang berlaku:


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, kod itu sudah agak bertele-tele. Dan kami merujuk kepada perkara di atas. Ini adalah salah satu sebab yang paling jelas untuk menggunakan pengecualian yang tidak disemak.

Kita boleh mencipta pengecualian yang tidak ditanda dengan melanjutkan RuntimeException dalam Java.

Pengecualian yang tidak ditandai diwarisi daripada kelas Ralat atau kelas RuntimeException . Ramai pengaturcara merasakan bahawa pengecualian ini boleh dikendalikan dalam program kami kerana ia mewakili ralat yang tidak dapat kami jangkakan untuk pulih semasa program berjalan.

Apabila pengecualian yang tidak disemak berlaku, ia biasanya disebabkan oleh penggunaan kod secara tidak betul, menghantar argumen yang batal atau sebaliknya tidak sah.

Nah, mari tulis kod:


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);
   }
}
    

Ambil perhatian bahawa kami membuat berbilang pembina untuk tujuan yang berbeza. Ini membolehkan kami memberikan pengecualian kami lebih banyak keupayaan. Sebagai contoh, kita boleh membuatnya supaya pengecualian memberi kita kod ralat. Sebagai permulaan, mari buat enum untuk mewakili kod ralat kami:


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 tambah pembina lain ke kelas pengecualian kami:


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

Dan jangan lupa untuk menambah medan (kita hampir terlupa):


private Integer errorCode;
    

Dan sudah tentu, kaedah untuk mendapatkan kod ini:


public Integer getErrorCode() {
   return errorCode;
}
    

Mari lihat keseluruhan kelas supaya kita boleh menyemaknya dan membandingkan:

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 lihat dalam tindakan:


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

Apabila kami menjalankan aplikasi kecil kami, kami akan melihat sesuatu seperti berikut dalam konsol:

Sekarang mari kita manfaatkan fungsi tambahan yang telah kami tambahkan. Kami akan menambah sedikit pada kod 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 boleh bekerja dengan pengecualian dengan cara yang sama seperti anda bekerja dengan objek. Sudah tentu, saya pasti anda sudah tahu bahawa segala-galanya di Jawa adalah objek.

Dan lihat apa yang kami lakukan. Pertama, kami menukar kaedah, yang kini tidak membuang, tetapi sebaliknya hanya mencipta pengecualian, bergantung pada parameter input. Seterusnya, menggunakan pernyataan kes suis , kami menjana pengecualian dengan kod ralat dan mesej yang diingini. Dan dalam kaedah utama, kami mendapat pengecualian yang dibuat, dapatkan kod ralat, dan membuangnya.

Mari jalankan ini dan lihat apa yang kita dapat pada konsol:

Lihat - kami mencetak kod ralat yang kami dapat daripada pengecualian dan kemudian membuang pengecualian itu sendiri. Lebih-lebih lagi, kita juga boleh menjejaki dengan tepat di mana pengecualian itu dilemparkan. Seperti yang diperlukan, anda boleh menambah semua maklumat yang berkaitan pada mesej, membuat kod ralat tambahan dan menambah ciri baharu pada pengecualian anda.

Nah, apa pendapat anda tentang itu? Saya harap semuanya berjaya untuk anda!

Secara umum, pengecualian adalah topik yang agak luas dan tidak jelas. Akan ada banyak lagi pertikaian mengenainya. Sebagai contoh, hanya Java yang telah menyemak pengecualian. Antara bahasa yang paling popular, saya tidak pernah melihat satu pun yang menggunakannya.

Bruce Eckel menulis dengan baik tentang pengecualian dalam bab 12 bukunya "Thinking in Java" — saya syorkan anda membacanya! Juga lihat pada jilid pertama "Core Java" Horstmann — Ia juga mempunyai banyak perkara menarik dalam bab 7.

Ringkasan kecil

  1. Tulis segala-galanya pada log! Log mesej dalam pengecualian yang dilemparkan. Ini biasanya akan banyak membantu dalam penyahpepijatan dan akan membolehkan anda memahami perkara yang berlaku. Jangan biarkan blok tangkapan kosong, jika tidak, ia hanya akan "menelan" pengecualian dan anda tidak akan mempunyai sebarang maklumat untuk membantu anda memburu masalah.

  2. Apabila bercakap tentang pengecualian, adalah amalan yang tidak baik untuk menangkap mereka semua sekali gus (seperti yang dikatakan oleh rakan sekerja saya, "ia bukan Pokemon, ia Java"), jadi elakkan tangkapan (Pengecualian e) atau lebih teruk, tangkap ( Throwable t ) .

  3. Buang pengecualian seawal mungkin. Ini adalah amalan pengaturcaraan Java yang baik. Apabila anda mengkaji rangka kerja seperti Spring, anda akan melihat bahawa ia mengikut prinsip "cepat gagal". Iaitu, mereka "gagal" seawal mungkin untuk memungkinkan untuk mencari ralat dengan cepat. Sudah tentu, ini membawa kesulitan tertentu. Tetapi pendekatan ini membantu mencipta kod yang lebih mantap.

  4. Apabila memanggil bahagian lain kod, lebih baik untuk menangkap pengecualian tertentu. Jika kod yang dipanggil melemparkan berbilang pengecualian, amalan pengaturcaraan yang lemah untuk hanya menangkap kelas induk bagi pengecualian tersebut. Sebagai contoh, katakan anda memanggil kod yang membuang FileNotFoundException dan IOException . Dalam kod anda yang memanggil modul ini, lebih baik menulis dua blok tangkapan untuk menangkap setiap pengecualian, bukannya satu tangkapan untuk menangkap Exception .

  5. Tangkap pengecualian hanya apabila anda boleh mengendalikannya dengan berkesan untuk pengguna dan untuk penyahpepijatan.

  6. Jangan teragak-agak untuk menulis pengecualian anda sendiri. Sudah tentu, Java mempunyai banyak yang siap, sesuatu untuk setiap kesempatan, tetapi kadangkala anda masih perlu mencipta "roda" anda sendiri. Tetapi anda harus memahami dengan jelas mengapa anda melakukan ini dan pastikan set pengecualian standard belum mempunyai perkara yang anda perlukan.

  7. Apabila anda membuat kelas pengecualian anda sendiri, berhati-hati tentang penamaan! Anda mungkin sudah tahu bahawa adalah sangat penting untuk menamakan kelas, pembolehubah, kaedah dan pakej dengan betul. Pengecualian tidak terkecuali! :) Sentiasa diakhiri dengan perkataan Exception , dan nama pengecualian harus jelas menyatakan jenis ralat yang diwakilinya. Sebagai contoh, FileNotFoundException .

  8. Dokumenkan pengecualian anda. Kami mengesyorkan menulis tag Javadoc @throws untuk pengecualian. Ini amat berguna apabila kod anda menyediakan antara muka dalam apa jua bentuk. Dan anda juga akan mendapati lebih mudah untuk memahami kod anda sendiri kemudian. Apakah pendapat anda, bagaimana anda boleh menentukan tentang MalformedURLException ? Daripada Javadoc! Ya, pemikiran menulis dokumentasi tidak begitu menarik, tetapi percayalah, anda akan berterima kasih kepada diri sendiri apabila anda kembali ke kod anda sendiri enam bulan kemudian.

  9. Lepaskan sumber dan jangan abaikan binaan cuba-dengan-sumber .

  10. Berikut ialah ringkasan keseluruhan: gunakan pengecualian dengan bijak. Membuang pengecualian adalah operasi yang agak "mahal" dari segi sumber. Dalam kebanyakan kes, mungkin lebih mudah untuk mengelak daripada membuang pengecualian dan sebaliknya mengembalikan, katakan, pembolehubah boolean yang sama ada operasi itu berjaya, menggunakan if-else yang mudah dan "kurang mahal" .

    Ia juga mungkin menggoda untuk mengikat logik aplikasi kepada pengecualian, yang anda jelas tidak sepatutnya lakukan. Seperti yang kami katakan pada permulaan artikel, pengecualian adalah untuk situasi yang luar biasa, bukan yang dijangka, dan terdapat pelbagai alat untuk mencegahnya. Khususnya, terdapat Pilihan untuk menghalang NullPointerException , atau Scanner.hasNext dan seumpamanya untuk menghalang IOException , yang mungkin dilemparkan oleh kaedah read() .