JavaRush /Blog Java /Random-VI /Các ngoại lệ trong Java: bắt và xử lý

Các ngoại lệ trong Java: bắt và xử lý

Xuất bản trong nhóm
Xin chào! Tôi ghét phải chia sẻ với bạn điều này, nhưng phần lớn công việc của một lập trình viên là xử lý lỗi. Và thường xuyên nhất - với chính họ. Chỉ là không có người nào không mắc sai lầm. Và cũng không có chương trình nào như vậy. Tất nhiên, điều quan trọng nhất khi xử lý lỗi là phải hiểu nguyên nhân của nó. Và có thể có rất nhiều lý do như vậy trong chương trình. Tại một thời điểm, những người tạo ra Java phải đối mặt với một câu hỏi: phải làm gì với những lỗi rất tiềm ẩn này trong chương trình? Tránh chúng hoàn toàn là không thực tế. Các lập trình viên có thể viết một cái gì đó mà thậm chí không thể tưởng tượng được :) Điều này có nghĩa là cần phải xây dựng một cơ chế xử lý lỗi vào ngôn ngữ. Nói cách khác, nếu xảy ra lỗi nào đó trong chương trình thì cần có một tập lệnh để thực hiện công việc tiếp theo. Chính xác thì chương trình nên làm gì khi xảy ra lỗi? Hôm nay chúng ta sẽ làm quen với cơ chế này. Và nó được gọi là “Ngoại lệ .

Ngoại lệ trong Java là gì

Ngoại lệ là một số tình huống đặc biệt, ngoài kế hoạch xảy ra trong quá trình vận hành chương trình. Có thể có nhiều ví dụ về trường hợp ngoại lệ trong Java. Ví dụ: bạn đã viết mã đọc văn bản từ một tệp và hiển thị dòng đầu tiên trên bảng điều khiển.
public class Main {

   public static void main(String[] args) throws IOException {
       BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
       String firstString = reader.readLine();
       System.out.println(firstString);
   }
}
Nhưng một tập tin như vậy không tồn tại! Kết quả của chương trình sẽ là một ngoại lệ - FileNotFoundException. Phần kết luận:

Exception in thread "main" java.io.FileNotFoundException: C:\Users\Username\Desktop\test.txt (Системе не удается найти указанный путь)
Mỗi ngoại lệ được biểu diễn bằng một lớp riêng biệt trong Java. Tất cả các lớp ngoại lệ đều đến từ một “tổ tiên” chung - lớp cha Throwable. Tên của lớp ngoại lệ thường phản ánh ngắn gọn lý do xuất hiện của nó:
  • FileNotFoundException(không tìm thấy tập tin)
  • ArithmeticException(ngoại trừ khi thực hiện một phép toán)
  • ArrayIndexOutOfBoundsException(số lượng ô mảng được chỉ định vượt quá độ dài của nó). Ví dụ: nếu bạn cố gắng xuất mảng ô [23] ra bàn điều khiển cho một mảng mảng có độ dài 10.
Có gần 400 lớp như vậy trong Java! Tại sao nhiều như vậy? Chính xác là để giúp các lập trình viên làm việc với họ thuận tiện hơn. Hãy tưởng tượng: bạn đã viết một chương trình và khi chạy, nó sẽ đưa ra một ngoại lệ trông như thế này:
Exception in thread "main"
Uh-uh :/ Không có gì rõ ràng cả. Đó là loại lỗi gì và nó đến từ đâu thì không rõ. Không có thông tin hữu ích. Nhưng nhờ có nhiều lớp khác nhau như vậy, lập trình viên có được điều chính cho mình - loại lỗi và nguyên nhân có thể xảy ra của nó, được chứa trong tên của lớp. Rốt cuộc, đó là một điều hoàn toàn khác để thấy trong bảng điều khiển:
Exception in thread "main" java.io.FileNotFoundException: C:\Users\Username\Desktop\test.txt (Системе не удается найти указанный путь)
Nó ngay lập tức trở nên rõ ràng vấn đề có thể là gì và “đào theo hướng nào” để giải quyết vấn đề! Các ngoại lệ, giống như bất kỳ trường hợp nào của lớp, là các đối tượng.

Bắt và xử lý ngoại lệ

Để làm việc với các ngoại lệ trong Java, có các khối mã đặc biệt: try, catchfinally. Ngoại lệ: chặn và xử lý - 2Đoạn mã mà người lập trình mong muốn xảy ra ngoại lệ được đặt trong một khối try. Điều này không có nghĩa là một ngoại lệ nhất thiết sẽ xảy ra ở vị trí này. Điều này có nghĩa là nó có thể xảy ra ở đó và người lập trình biết được điều đó. Loại lỗi bạn mong nhận được sẽ được đặt trong một khối catch(“bắt”). Đây cũng là nơi đặt tất cả các mã cần được thực thi nếu xảy ra ngoại lệ. Đây là một ví dụ:
public static void main(String[] args) throws IOException {
   try {
       BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));

       String firstString = reader.readLine();
       System.out.println(firstString);
   } catch (FileNotFoundException e) {

       System.out.println("Error! File not found!");
   }
}
Phần kết luận:

Ошибка! Файл не найден!
Chúng tôi đặt mã của mình thành hai khối. Trong khối đầu tiên, chúng tôi cho rằng có thể xảy ra lỗi “Không tìm thấy tệp”. Đây là một khối try. Trong phần thứ hai, chúng tôi cho chương trình biết phải làm gì nếu xảy ra lỗi. Hơn nữa, có một loại lỗi cụ thể - FileNotFoundException. Nếu chúng ta chuyển catchmột lớp ngoại lệ khác vào dấu ngoặc khối, nó sẽ không bị bắt.
public static void main(String[] args) throws IOException {
   try {
       BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
       String firstString = reader.readLine();
       System.out.println(firstString);
   } catch (ArithmeticException e) {

       System.out.println("Error! File not found!");
   }
}
Phần kết luận:

Exception in thread "main" java.io.FileNotFoundException: C:\Users\Username\Desktop\test.txt (Системе не удается найти указанный путь)
Mã trong khối catchkhông hoạt động vì chúng tôi đã “cấu hình” khối này để chặn ArithmeticExceptionvà mã trong khối tryđã loại bỏ một loại khác - FileNotFoundException. Chúng tôi không viết tập lệnh cho FileNotFoundException, vì vậy chương trình hiển thị trong bảng điều khiển thông tin được hiển thị theo mặc định cho FileNotFoundException. Ở đây bạn cần chú ý đến 3 điều. Đầu tiên. Ngay khi một ngoại lệ xảy ra ở bất kỳ dòng mã nào trong khối thử, mã sau nó sẽ không còn được thực thi nữa. Việc thực thi chương trình sẽ ngay lập tức “nhảy” vào khối catch. Ví dụ:
public static void main(String[] args) {
   try {
       System.out.println("Divide a number by zero");
       System.out.println(366/0);//this line of code will throw an exception

       System.out.println("This");
       System.out.println("code");
       System.out.println("Not");
       System.out.println("will");
       System.out.println("done!");

   } catch (ArithmeticException e) {

       System.out.println("The program jumped to the catch block!");
       System.out.println("Error! You can't divide by zero!");
   }
}
Phần kết luận:

Делим число на ноль 
Программа перепрыгнула в блок catch! 
Ошибка! Нельзя делить на ноль! 
Trong khối tryở dòng thứ hai, chúng tôi đã cố chia một số cho 0, dẫn đến một ngoại lệ ArithmeticException. Sau đó, các dòng 6-10 của khối trysẽ không còn được thực thi nữa. Như chúng tôi đã nói, chương trình ngay lập tức bắt đầu thực thi khối catch. Thứ hai. Có thể có một số khối catch. Nếu mã trong một khối trycó thể đưa ra không chỉ một mà nhiều loại ngoại lệ, bạn có thể viết khối của riêng mình cho từng loại ngoại lệ đó catch.
public static void main(String[] args) throws IOException {
   try {
       BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));

       System.out.println(366/0);
       String firstString = reader.readLine();
       System.out.println(firstString);
   } catch (FileNotFoundException e) {

       System.out.println("Error! File not found!");

   } catch (ArithmeticException e) {

       System.out.println("Error! Division by 0!");

   }
}
Trong ví dụ này chúng tôi đã viết hai khối catch. Nếu , tryxảy ra trong khối FileNotFoundException, khối đầu tiên sẽ được thực thi catch. Nếu xảy ra ArithmeticException, cái thứ hai sẽ được thực thi. Bạn có thể viết ít nhất 50 khối catch. Nhưng tất nhiên, tốt hơn hết là bạn không nên viết mã có thể gây ra 50 loại lỗi khác nhau :) Thứ ba. Làm thế nào để bạn biết mã của bạn có thể đưa ra những ngoại lệ nào? Tất nhiên, bạn có thể đoán về một số điều, nhưng không thể giữ mọi thứ trong đầu. Do đó, trình biên dịch Java biết về các ngoại lệ phổ biến nhất và biết chúng có thể xảy ra trong những tình huống nào. Ví dụ: nếu bạn viết mã và trình biên dịch biết rằng 2 loại ngoại lệ có thể xảy ra trong quá trình hoạt động, mã của bạn sẽ không biên dịch cho đến khi bạn xử lý chúng. Chúng ta sẽ xem các ví dụ về điều này dưới đây. Bây giờ liên quan đến việc xử lý ngoại lệ. Có 2 cách để xử lý chúng. Chúng ta đã gặp trường hợp đầu tiên - phương thức có thể xử lý ngoại lệ một cách độc lập trong khối catch(). Có một tùy chọn thứ hai - phương thức này có thể đưa ra một ngoại lệ lên ngăn xếp cuộc gọi. Nó có nghĩa là gì? Ví dụ: trong lớp của chúng ta, chúng ta có một phương thức - cũng giống như vậy printFirstString()- đọc một tệp và hiển thị dòng đầu tiên của nó trên bảng điều khiển:
public static void printFirstString(String filePath) {

   BufferedReader reader = new BufferedReader(new FileReader(filePath));
   String firstString = reader.readLine();
   System.out.println(firstString);
}
Hiện tại mã của chúng tôi không biên dịch được vì nó có các ngoại lệ chưa được xử lý. Ở dòng 1 bạn chỉ đường dẫn tới file. Trình biên dịch biết rằng mã như vậy có thể dễ dàng dẫn đến các tệp FileNotFoundException. Trên dòng 3 bạn đọc văn bản từ tập tin. IOExceptionTrong quá trình này , lỗi trong quá trình nhập-xuất (Input-Output) có thể dễ dàng xảy ra . Bây giờ trình biên dịch sẽ nói với bạn: “Anh bạn, tôi sẽ không phê duyệt mã này hoặc biên dịch nó cho đến khi bạn cho tôi biết tôi nên làm gì nếu một trong những trường hợp ngoại lệ này xảy ra. Và chúng chắc chắn có thể xảy ra dựa trên đoạn mã bạn đã viết!” . Không có nơi nào để đi, bạn cần phải xử lý cả hai! Tùy chọn xử lý đầu tiên đã quen thuộc với chúng ta: chúng ta cần đặt mã của mình vào một khối tryvà thêm hai khối catch:
public static void printFirstString(String filePath) {

   try {
       BufferedReader reader = new BufferedReader(new FileReader(filePath));
       String firstString = reader.readLine();
       System.out.println(firstString);
   } catch (FileNotFoundException e) {
       System.out.println("Error, file not found!");
       e.printStackTrace();
   } catch (IOException e) {
       System.out.println("Error while inputting/outputting data from file!");
       e.printStackTrace();
   }
}
Nhưng đây không phải là lựa chọn duy nhất. Chúng ta có thể tránh viết tập lệnh cho lỗi bên trong phương thức và chỉ cần đưa ngoại lệ lên trên cùng. Việc này được thực hiện bằng cách sử dụng từ khóa throws, được viết trong phần khai báo phương thức:
public static void printFirstString(String filePath) throws FileNotFoundException, IOException {
   BufferedReader reader = new BufferedReader(new FileReader(filePath));
   String firstString = reader.readLine();
   System.out.println(firstString);
}
Sau từ này, throwschúng tôi liệt kê, phân tách bằng dấu phẩy, tất cả các loại ngoại lệ mà phương pháp này có thể đưa ra trong quá trình hoạt động. Tại sao việc này lại được thực hiện? Bây giờ, nếu ai đó trong chương trình muốn gọi phương thức này printFirstString(), anh ta sẽ phải tự mình thực hiện việc xử lý ngoại lệ. Ví dụ: trong một phần khác của chương trình, một trong những đồng nghiệp của bạn đã viết một phương thức trong đó nó gọi phương thức của bạn printFirstString():
public static void yourColleagueMethod() {

   //...your colleague's method does something

   //...and at one moment calls your printFirstString() method with the file it needs
   printFirstString("C:\\Users\\Eugene\\Desktop\\testFile.txt");
}
Lỗi, mã không biên dịch được! printFirstString()Chúng tôi không viết tập lệnh xử lý lỗi trong phương thức . Vì vậy, nhiệm vụ đặt lên vai những người sẽ sử dụng phương pháp này. Nghĩa là, phương thức yourColleagueMethod()hiện phải đối mặt với 2 tùy chọn giống nhau: nó phải xử lý cả hai ngoại lệ đã “bay” đến nó bằng cách sử dụng try-catchhoặc chuyển tiếp chúng thêm.
public static void yourColleagueMethod() throws FileNotFoundException, IOException {
   //...the method does something

   //...and at one moment calls your printFirstString() method with the file it needs
   printFirstString("C:\\Users\\Eugene\\Desktop\\testFile.txt");
}
Trong trường hợp thứ hai, việc xử lý sẽ thuộc về phương thức tiếp theo trên ngăn xếp - phương thức sẽ gọi yourColleagueMethod(). Đó là lý do tại sao cơ chế như vậy được gọi là “ném một ngoại lệ lên trên” hoặc “chuyển lên trên cùng”. Khi bạn đưa ra các ngoại lệ bằng cách sử dụng throws, mã sẽ biên dịch. Tại thời điểm này, trình biên dịch dường như nói: “Được rồi, được rồi. Mã của bạn chứa rất nhiều ngoại lệ tiềm ẩn, nhưng tôi vẫn sẽ biên dịch nó. Chúng ta sẽ quay lại cuộc trò chuyện này! Và khi bạn gọi một phương thức ở đâu đó trong chương trình mà chưa xử lý các ngoại lệ của nó, trình biên dịch sẽ thực hiện lời hứa của nó và nhắc bạn về chúng một lần nữa. Cuối cùng, chúng ta sẽ nói về khối finally(xin lỗi vì chơi chữ). Đây là phần cuối cùng của quy trình xử lý ngoại lệ triumvirate try-catch-finally. Điểm đặc biệt của nó là nó được thực thi trong bất kỳ kịch bản vận hành chương trình nào.
public static void main(String[] args) throws IOException {
   try {
       BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));

       String firstString = reader.readLine();
       System.out.println(firstString);
   } catch (FileNotFoundException e) {
       System.out.println("Error! File not found!");
       e.printStackTrace();
   } finally {
       System.out.println("And here is the finally block!");
   }
}
Trong ví dụ này, mã bên trong khối finallyđược thực thi trong cả hai trường hợp. Nếu mã trong khối tryđược thực thi hoàn toàn và không đưa ra ngoại lệ, khối sẽ kích hoạt ở cuối finally. Nếu mã bên trong trybị gián đoạn và chương trình nhảy vào khối catch, sau khi mã bên trong được thực thi catch, khối đó vẫn sẽ được chọn finally. Tại sao nó lại cần thiết? Mục đích chính của nó là thực thi phần mã được yêu cầu; phần đó phải được hoàn thành bất kể hoàn cảnh nào. Ví dụ, nó thường giải phóng một số tài nguyên được chương trình sử dụng. Trong mã của chúng tôi, chúng tôi mở một luồng để đọc thông tin từ một tệp và chuyển nó tới tệp BufferedReader. Chúng ta readercần phải đóng cửa và giải phóng tài nguyên. Điều này phải được thực hiện trong mọi trường hợp: không quan trọng chương trình có hoạt động như mong đợi hay đưa ra một ngoại lệ hay không. Thật thuận tiện để làm điều này trong một khối finally:
public static void main(String[] args) throws IOException {

   BufferedReader reader = null;
   try {
       reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));

       String firstString = reader.readLine();
       System.out.println(firstString);
   } catch (FileNotFoundException e) {
       e.printStackTrace();
   } finally {
       System.out.println("And here is the finally block!");
       if (reader != null) {
           reader.close();
       }
   }
}
Bây giờ chúng tôi hoàn toàn chắc chắn rằng chúng tôi đã xử lý các tài nguyên bị chiếm dụng, bất kể điều gì xảy ra khi chương trình đang chạy :) Đó không phải là tất cả những gì bạn cần biết về các trường hợp ngoại lệ. Xử lý lỗi là một chủ đề rất quan trọng trong lập trình: có nhiều bài viết viết về nó. Trong bài học tiếp theo chúng ta sẽ tìm hiểu có những loại ngoại lệ nào và cách tạo ngoại lệ của riêng bạn :) Hẹn gặp bạn ở đó!
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION