Tôi nghĩ rằng bạn có thể đã gặp phải trường hợp khi bạn chạy mã và kết thúc bằng một thứ gì đó như NullPulumException , ClassCastException hoặc tệ hơn... Tiếp theo là một quá trình dài gỡ lỗi, phân tích, tìm kiếm trên Google, v.v. Các trường hợp ngoại lệ là tuyệt vời như vậy: chúng chỉ ra bản chất của vấn đề và nơi nó xảy ra. Nếu bạn muốn làm mới bộ nhớ của mình và tìm hiểu thêm một chút, hãy xem bài viết này: Ngoại lệ: đã chọn, bỏ chọn và tùy chỉnh .

Điều đó nói rằng, có thể có những tình huống khi bạn cần tạo ngoại lệ của riêng mình. Ví dụ: giả sử mã của bạn cần yêu cầu thông tin từ một dịch vụ từ xa không khả dụng vì lý do nào đó. Hoặc giả sử ai đó điền vào đơn đăng ký thẻ ngân hàng và cung cấp số điện thoại, dù tình cờ hay không, đã được liên kết với một người dùng khác trong hệ thống.

Tất nhiên, hành vi đúng ở đây vẫn phụ thuộc vào yêu cầu của khách hàng và kiến ​​trúc của hệ thống, nhưng hãy giả sử rằng bạn được giao nhiệm vụ kiểm tra xem số điện thoại đã được sử dụng chưa và đưa ra một ngoại lệ nếu có.

Hãy tạo một ngoại lệ:


public class PhoneNumberAlreadyExistsException extends Exception {

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

Tiếp theo, chúng tôi sẽ sử dụng nó khi chúng tôi thực hiện kiểm tra của mình:


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

Để đơn giản hóa ví dụ của chúng tôi, chúng tôi sẽ sử dụng một số số điện thoại được mã hóa cứng để đại diện cho cơ sở dữ liệu. Và cuối cùng, hãy thử sử dụng ngoại lệ của chúng tôi:


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

Và bây giờ là lúc nhấn Shift+F10 (nếu bạn đang sử dụng IDEA), tức là chạy dự án. Đây là những gì bạn sẽ thấy trong bảng điều khiển:

ngoại lệ.CreditCardIssue
ngoại lệ.PhoneNumberAlreadyExistsException: Số điện thoại được chỉ định đã được sử dụng bởi một khách hàng khác!
tại ngoại lệ.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Nhìn bạn kìa! Bạn đã tạo ngoại lệ của riêng mình và thậm chí đã thử nghiệm nó một chút. Chúc mừng thành tích này! Tôi khuyên bạn nên thử nghiệm mã một chút để hiểu rõ hơn về cách thức hoạt động của nó.

Thêm một bước kiểm tra khác — ví dụ: kiểm tra xem số điện thoại có bao gồm các chữ cái hay không. Như bạn có thể biết, ở Hoa Kỳ, các chữ cái thường được sử dụng để làm cho số điện thoại dễ nhớ hơn, ví dụ: 1-800-MY-APPLE. Séc của bạn có thể đảm bảo rằng số điện thoại chỉ chứa các số.

Được rồi, vậy là chúng ta đã tạo một ngoại lệ được kiểm tra. Tất cả sẽ ổn và tốt, nhưng...

Cộng đồng lập trình được chia thành hai phe — những người ủng hộ các ngoại lệ được kiểm tra và những người phản đối chúng. Cả hai bên đều đưa ra những lập luận chặt chẽ. Cả hai đều bao gồm các nhà phát triển hàng đầu: Bruce Eckel chỉ trích các ngoại lệ được kiểm tra, trong khi James Gosling bảo vệ chúng. Có vẻ như vấn đề này sẽ không bao giờ được giải quyết vĩnh viễn. Điều đó nói rằng, chúng ta hãy xem xét những nhược điểm chính của việc sử dụng các ngoại lệ được kiểm tra.

Nhược điểm chính của các ngoại lệ được kiểm tra là chúng phải được xử lý. Và ở đây, chúng tôi có hai tùy chọn: hoặc xử lý tại chỗ bằng cách sử dụng try-catch hoặc, nếu chúng tôi sử dụng cùng một ngoại lệ ở nhiều nơi, hãy sử dụng lệnh ném để loại bỏ các ngoại lệ và xử lý chúng trong các lớp cấp cao nhất.

Ngoài ra, chúng tôi có thể kết thúc với mã "bản soạn sẵn", tức là mã chiếm nhiều dung lượng nhưng không thực hiện được nhiều công việc nặng nhọc.

Các vấn đề xuất hiện trong các ứng dụng khá lớn với rất nhiều ngoại lệ đang được xử lý: danh sách ném trên một phương pháp cấp cao nhất có thể dễ dàng phát triển để bao gồm hàng tá ngoại lệ.

công khai OurCoolClass() ném FirstException, SecondException, ThirdException, ApplicationNameException...

Các nhà phát triển thường không thích điều này và thay vào đó chọn một thủ thuật: họ làm cho tất cả các ngoại lệ được kiểm tra của họ kế thừa một tổ tiên chung — ApplicationNameException . Bây giờ họ cũng phải nắm bắt ngoại lệ ( đã kiểm tra !) đó trong trình xử lý:


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

Ở đây chúng ta phải đối mặt với một vấn đề khác - chúng ta nên làm gì trong khối bắt cuối cùng ? Ở trên, chúng tôi đã xử lý tất cả các tình huống dự kiến, vì vậy tại thời điểm này ApplicationNameException đối với chúng tôi không có ý nghĩa gì khác ngoài " Ngoại lệ : đã xảy ra một số lỗi khó hiểu". Đây là cách chúng tôi xử lý nó:


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

Và cuối cùng, chúng ta không biết chuyện gì đã xảy ra.

Nhưng chúng ta không thể loại bỏ tất cả các ngoại lệ cùng một lúc như thế này sao?


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

Vâng chúng ta có thể. Nhưng "ném Ngoại lệ" cho chúng ta biết điều gì? Rằng một cái gì đó bị hỏng. Bạn sẽ phải điều tra mọi thứ từ trên xuống dưới và làm quen với trình gỡ lỗi trong một thời gian dài để hiểu lý do.

Bạn cũng có thể gặp một cấu trúc đôi khi được gọi là "nuốt ngoại lệ":


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

Không có gì nhiều để thêm vào đây bằng cách giải thích - mã làm cho mọi thứ trở nên rõ ràng, hay đúng hơn, nó làm cho mọi thứ trở nên không rõ ràng.

Tất nhiên, bạn có thể nói rằng bạn sẽ không thấy điều này trong mã thực. Chà, chúng ta hãy xem phần ruột (mã) của lớp URL từ gói java.net . Hãy theo dõi tôi nếu bạn muốn biết!

Đây là một trong những cấu trúc trong lớp URL :


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

Như bạn có thể thấy, chúng tôi có một ngoại lệ được kiểm tra thú vị — MalformingURLException . Đây là khi nó có thể bị ném (và tôi trích dẫn):
"nếu không có giao thức nào được chỉ định hoặc tìm thấy một giao thức không xác định hoặc thông số kỹ thuật là null hoặc URL được phân tích cú pháp không tuân thủ cú pháp cụ thể của giao thức được liên kết."

Đó là:

  1. Nếu không có giao thức được chỉ định.
  2. Một giao thức không xác định được tìm thấy.
  3. Thông số kỹ thuật là null .
  4. URL không tuân thủ cú pháp cụ thể của giao thức được liên kết.

Hãy tạo một phương thức tạo một đối tượng URL :


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

Ngay khi bạn viết những dòng này trong IDE (Tôi đang viết mã trong IDEA, nhưng điều này hoạt động ngay cả trong Eclipse và NetBeans), bạn sẽ thấy điều này:

Điều này có nghĩa là chúng ta cần đưa ra một ngoại lệ hoặc bọc mã trong một khối thử bắt . Hiện tại, tôi khuyên bạn nên chọn tùy chọn thứ hai để hình dung những gì đang xảy ra:


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

Như bạn có thể thấy, mã đã khá dài dòng. Và chúng tôi đã đề cập đến điều đó ở trên. Đây là một trong những lý do rõ ràng nhất để sử dụng các ngoại lệ không được kiểm tra.

Chúng ta có thể tạo một ngoại lệ không được kiểm tra bằng cách mở rộng RuntimeException trong Java.

Các ngoại lệ không được kiểm tra được kế thừa từ lớp Lỗi hoặc lớp RuntimeException . Nhiều lập trình viên cảm thấy rằng những ngoại lệ này có thể được xử lý trong các chương trình của chúng tôi vì chúng đại diện cho các lỗi mà chúng tôi không thể khôi phục trong khi chương trình đang chạy.

Khi một ngoại lệ không được kiểm soát xảy ra, thường là do sử dụng mã không chính xác, chuyển vào một đối số không có giá trị hoặc không hợp lệ.

Chà, hãy viết mã:


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

Lưu ý rằng chúng tôi đã tạo nhiều hàm tạo cho các mục đích khác nhau. Điều này cho phép chúng tôi cung cấp cho ngoại lệ của mình nhiều khả năng hơn. Ví dụ: chúng ta có thể tạo ngoại lệ cho chúng ta mã lỗi. Để bắt đầu, hãy tạo một enum để biểu thị các mã lỗi của chúng ta:


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

Bây giờ, hãy thêm một hàm tạo khác vào lớp ngoại lệ của chúng ta:


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

Và đừng quên thêm một trường (suýt quên):


private Integer errorCode;
    

Và tất nhiên, một phương pháp để lấy mã này:


public Integer getErrorCode() {
   return errorCode;
}
    

Hãy nhìn vào cả lớp để chúng ta có thể kiểm tra và so sánh:

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! Ngoại lệ của chúng ta đã xong! Như bạn có thể thấy, không có gì đặc biệt phức tạp ở đây. Hãy kiểm tra nó trong hành động:


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

Khi chúng tôi chạy ứng dụng nhỏ của mình, chúng tôi sẽ thấy một cái gì đó giống như sau trong bảng điều khiển:

Bây giờ, hãy tận dụng chức năng bổ sung mà chúng tôi đã thêm. Chúng tôi sẽ thêm một chút vào mã trước đó:


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

}
    

Bạn có thể làm việc với các ngoại lệ giống như cách bạn làm việc với các đối tượng. Tất nhiên, tôi chắc rằng bạn đã biết rằng mọi thứ trong Java đều là đối tượng.

Và nhìn vào những gì chúng tôi đã làm. Đầu tiên, chúng tôi đã thay đổi phương thức, phương thức này hiện không ném mà thay vào đó, chỉ đơn giản là tạo một ngoại lệ, tùy thuộc vào tham số đầu vào. Tiếp theo, bằng cách sử dụng câu lệnh trường hợp chuyển đổi , chúng tôi tạo một ngoại lệ với thông báo và mã lỗi mong muốn. Và trong phương thức chính, chúng ta lấy ngoại lệ đã tạo, lấy mã lỗi và ném nó.

Hãy chạy cái này và xem những gì chúng ta nhận được trên bảng điều khiển:

Hãy nhìn xem - chúng tôi đã in mã lỗi mà chúng tôi nhận được từ ngoại lệ và sau đó ném chính ngoại lệ đó. Hơn nữa, chúng ta thậm chí có thể theo dõi chính xác vị trí mà ngoại lệ được ném ra. Khi cần, bạn có thể thêm tất cả thông tin liên quan vào thông báo, tạo mã lỗi bổ sung và thêm các tính năng mới vào các ngoại lệ của mình.

Vâng, bạn nghĩ gì về điều đó? Tôi hy vọng mọi thứ làm việc ra cho bạn!

Nói chung, ngoại lệ là một chủ đề khá rộng và không rõ ràng. Sẽ có nhiều tranh chấp hơn về nó. Ví dụ, chỉ có Java đã kiểm tra các ngoại lệ. Trong số các ngôn ngữ phổ biến nhất, tôi chưa thấy ngôn ngữ nào sử dụng chúng.

Bruce Eckel đã viết rất hay về các ngoại lệ trong chương 12 của cuốn sách "Tư duy bằng Java" của ông ấy — Tôi khuyên bạn nên đọc nó! Ngoài ra, hãy xem tập đầu tiên của "Core Java" của Horstmann - Nó cũng có rất nhiều nội dung thú vị trong chương 7.

Một bản tóm tắt nhỏ

  1. Viết mọi thứ vào nhật ký! Ghi thông điệp trong trường hợp ngoại lệ ném. Điều này thường sẽ giúp ích rất nhiều trong việc gỡ lỗi và sẽ cho phép bạn hiểu điều gì đã xảy ra. Đừng để trống khối catch , nếu không nó sẽ chỉ "nuốt chửng" ngoại lệ và bạn sẽ không có bất kỳ thông tin nào để giúp bạn tìm ra sự cố.

  2. Khi nói đến các trường hợp ngoại lệ, thật không tốt nếu bắt tất cả chúng cùng một lúc (như một đồng nghiệp của tôi đã nói, "không phải Pokemon, đó là Java"), vì vậy hãy tránh bắt (Ngoại lệ e) hoặc tệ hơn là bắt ( Throwable t ) .

  3. Ném ngoại lệ càng sớm càng tốt. Đây là cách thực hành lập trình Java tốt. Khi bạn nghiên cứu các framework như Spring, bạn sẽ thấy rằng chúng tuân theo nguyên tắc "fail fast". Tức là họ "lỗi" càng sớm càng tốt để có thể nhanh chóng tìm ra lỗi. Tất nhiên, điều này mang lại những bất tiện nhất định. Nhưng cách tiếp cận này giúp tạo mã mạnh mẽ hơn.

  4. Khi gọi các phần khác của mã, tốt nhất bạn nên nắm bắt một số ngoại lệ nhất định. Nếu mã được gọi đưa ra nhiều ngoại lệ, thì thực hành lập trình kém là chỉ bắt lớp cha của những ngoại lệ đó. Ví dụ: giả sử bạn gọi mã ném FileNotFoundExceptionIOException . Trong mã gọi mô-đun này của bạn, tốt hơn là viết hai khối bắt để bắt từng ngoại lệ, thay vì một lần bắt duy nhất để bắt Ngoại lệ .

  5. Chỉ nắm bắt các ngoại lệ khi bạn có thể xử lý chúng một cách hiệu quả cho người dùng và để gỡ lỗi.

  6. Đừng ngần ngại viết các ngoại lệ của riêng bạn. Tất nhiên, Java có rất nhiều thứ làm sẵn, thứ dành cho mọi trường hợp, nhưng đôi khi bạn vẫn cần phát minh ra "bánh xe" của riêng mình. Nhưng bạn nên hiểu rõ ràng lý do tại sao bạn làm điều này và đảm bảo rằng bộ ngoại lệ tiêu chuẩn chưa có thứ bạn cần.

  7. Khi bạn tạo các lớp ngoại lệ của riêng mình, hãy cẩn thận về cách đặt tên! Có thể bạn đã biết rằng việc đặt tên chính xác cho các lớp, biến, phương thức và gói là cực kỳ quan trọng. Ngoại lệ cũng không ngoại lệ! :) Luôn kết thúc bằng từ Ngoại lệ và tên của ngoại lệ phải truyền đạt rõ ràng loại lỗi mà nó đại diện. Ví dụ: FileNotFoundException .

  8. Tài liệu ngoại lệ của bạn. Chúng tôi khuyên bạn nên viết thẻ @throws Javadoc cho các trường hợp ngoại lệ. Điều này sẽ đặc biệt hữu ích khi mã của bạn cung cấp bất kỳ loại giao diện nào. Và bạn cũng sẽ thấy việc hiểu mã của mình sau này dễ dàng hơn. Bạn nghĩ sao, làm thế nào bạn có thể xác định được MalformingURLException là gì? Từ Javadoc! Vâng, ý tưởng viết tài liệu không hấp dẫn lắm, nhưng tin tôi đi, bạn sẽ cảm ơn chính mình khi quay lại mã của chính mình sáu tháng sau.

  9. Giải phóng tài nguyên và đừng bỏ qua cấu trúc dùng thử tài nguyên .

  10. Đây là tóm tắt tổng thể: sử dụng ngoại lệ một cách khôn ngoan. Ném một ngoại lệ là một hoạt động khá "tốn kém" về tài nguyên. Trong nhiều trường hợp, có thể dễ dàng hơn để tránh ném ngoại lệ và thay vào đó, trả về một biến boolean cho biết liệu thao tác có thành công hay không, bằng cách sử dụng if- else đơn giản và "ít tốn kém hơn" .

    Nó cũng có thể hấp dẫn để ràng buộc logic ứng dụng với các ngoại lệ, điều mà rõ ràng bạn không nên làm. Như chúng tôi đã nói ở đầu bài viết, các trường hợp ngoại lệ dành cho các tình huống đặc biệt, không mong đợi và có nhiều công cụ khác nhau để ngăn chặn chúng. Cụ thể, có Tùy chọn để ngăn NullPulumException hoặc Scanner.hasNext và tương tự để ngăn IOException mà phương thức read() có thể ném ra.