Exception (ngoại lệ) trong Java
- 08-10-2023
- Toanngo92
- 0 Comments
Java là một ngôn ngữ lập trình rất mạnh mẽ và hiệu quả. Các tính năng như lớp, đối tượng, kế thừa và những tính năng khác khiến Java trở thành một ngôn ngữ mạnh mẽ, linh hoạt và an toàn. Tuy nhiên, dù mã được viết tốt đến đâu, nó vẫn có thể gặp sự cố hoặc hoạt động sai lầm trong một số điều kiện cụ thể. Những tình huống này có thể là dự kiến hoặc không dự kiến. Trong cả hai trường hợp, người dùng sẽ bị bối rối bởi hành vi không mong muốn của mã.
Để tránh tình huống như vậy, Java cung cấp khái niệm xử lý ngoại lệ, thông qua đó, một người lập trình có thể hiển thị thông báo phù hợp cho người dùng trong trường hợp mã hoạt động không mong muốn xảy ra.
Mục lục
Exception (ngoại lệ)
Một ngoại lệ là một sự kiện hoặc điều kiện bất thường trong một chương trình xảy ra trong quá trình thực thi của chương trình dẫn đến sự gián đoạn của luồng bình thường của các chỉ thị chương trình. Một ngoại lệ có thể xảy ra vì các lý do khác nhau như khi người dùng nhập dữ liệu không hợp lệ, một tệp phải được mở không thể được tìm thấy, một kết nối mạng bị mất giữa quá trình truyền thông hoặc JVM đã hết bộ nhớ. Khi một lỗi xảy ra bên trong một phương thức, nó tạo ra một đối tượng ngoại lệ và truyền nó cho hệ thống thời gian chạy. Đối tượng này chứa thông tin về loại lỗi và trạng thái của chương trình khi lỗi xảy ra.
Quá trình tạo ra một đối tượng ngoại lệ và truyền nó cho hệ thống thời gian chạy (runtime system) được gọi là ném ngoại lệ (throw exception). Sau khi một ngoại lệ được ném bởi một phương thức, hệ thống thời gian chạy cố gắng tìm một khối mã để xử lý nó. Các khối mã có thể xử lý ngoại lệ là một loạt các phương thức đã được gọi để đạt được phương thức mà lỗi đã thực sự xảy ra. Danh sách hoặc loạt các phương thức này được gọi là ngăn xếp cuộc gọi (caller stack). Nói cách khác, ngăn xếp cuộc gọi hiển thị chuỗi các cuộc gọi phương thức dẫn đến ngoại lệ.
Hình trên cho thấy cuộc gọi phương thức từ hàm main -> Phương thức A -> Phương thức B -> Phương thức C. Khi một ngoại lệ xảy ra trong phương thức C, nó ném đối tượng ngoại lệ đến môi trường thời gian chạy. Môi trường thời gian chạy sau đó tìm kiếm toàn bộ ngăn xếp cuộc gọi để tìm một phương thức chứa một khối mã có thể xử lý ngoại lệ. Khối mã này được gọi là một trình xử lý ngoại lệ.
Ngoại lệ được ném vì các lý do sau đây:
- Một câu lệnh throw trong một phương thức đã được thực thi.
- Máy ảo Java (JVM) phát hiện một sự bất thường trong quá trình thực thi, chẳng hạn như:
- Vi phạm cú pháp bình thường của Java trong quá trình đánh giá một biểu thức như một số nguyên chia cho 0.
- Lỗi xảy ra trong quá trình liên kết, tải, hoặc khởi tạo một phần của chương trình sẽ ném một đối tượng của một lớp con của LinkageError.
- JVM bị ngăn cản khỏi việc thực thi mã do lỗi nội bộ hoặc giới hạn tài nguyên, sẽ ném một đối tượng của một lớp con của VirtualMachineError.Ngoại lệ này không được ném một cách tùy ý, mà được xác định tại một điểm nào đó là một kết quả có thể của việc đánh giá biểu thức hoặc thực hiện câu lệnh.
- Một ngoại lệ không đồng bộ xảy ra.
Việc sử dụng ngoại lệ để xử lý lỗi mang lại một số lợi ích như sau:
- Tách Mã Xử Lý Lỗi ra khỏi Mã Bình Thường: Ngoại lệ giúp tách biệt chi tiết về những gì phải làm khi có điều gì đó không đúng xảy ra khỏi logic chính của một chương trình. Trong lập trình truyền thống, mã phát hiện lỗi, mã báo cáo lỗi và mã xử lý lỗi thường được viết trong cùng một khu vực, thường dẫn đến mã hỗn loạn và khó hiểu. Ngược lại, ngoại lệ cho phép người dùng viết mã chính của chương trình trong khi xử lý các trường hợp ngoại lệ ở nơi khác.
- Lan Truyền Lỗi Lên Trong Ngăn Xếp Cuộc Gọi: Ngoại lệ cho phép lan truyền việc báo cáo lỗi lên trong ngăn xếp cuộc gọi của các phương thức. Một phương thức nằm trong ngăn xếp cuộc gọi có thể tránh bất kỳ ngoại lệ nào được ném trong nó để một phương thức nằm ở trên trong ngăn xếp cuộc gọi có thể bắt nó. Do đó, chỉ có các phương thức liên quan phải lo lắng về việc phát hiện các loại lỗi cụ thể.
- Nhóm Các Loại Lỗi Tương Tự: Tất cả các ngoại lệ được ném trong một chương trình là các đối tượng. Do đó, việc nhóm các ngoại lệ tương tự là một kết quả rõ ràng của cấu trúc lớp. Ví dụ, nhóm các lớp ngoại lệ liên quan được định nghĩa trong gói java.io như IOException và các lớp con của nó. Lớp IOException là ngoại lệ chung nhất đại diện cho bất kỳ loại lỗi nào xảy ra trong quá trình thực hiện các hoạt động Input/Output (I/O). Các lớp con của IOException đại diện cho các lỗi cụ thể hơn. Ví dụ, lớp con FileNotFoundException xử lý ngoại lệ khi người dùng cố gắng mở một tệp không tồn tại.
Các kiểu lỗi và ngoại lệ
Như đã đề cập trước đó, một ngoại lệ là một điều kiện bất thường xảy ra trong quá trình thực thi chương trình. Có nhiều nguyên nhân dẫn đến sự xuất hiện của một ngoại lệ như dữ liệu không hợp lệ do người dùng nhập, cố gắng mở một tệp không tồn tại, mất kết nối mạng giữa một giao dịch, hoặc JVM hết bộ nhớ trong quá trình thực hiện một nhiệm vụ. Rõ ràng rằng một số ngoại lệ được gây ra do lỗi của người dùng, một số khác do lỗi lập trình và những cái còn lại do tài nguyên hệ thống gặp sự cố một cách nào đó.
Checked Exceptions (Ngoại lệ được kiểm tra)
Đây là những ngoại lệ mà một ứng dụng viết tốt phải dự kiến và cung cấp các phương thức để phục hồi. Ví dụ, giả sử một ứng dụng yêu cầu người dùng chỉ định tên của một tệp cần mở và người dùng chỉ định tên của một tệp không tồn tại. Trong trường hợp này, ngoại lệ java.io.FileNotFoundException sẽ được ném. Tuy nhiên, một chương trình viết tốt sẽ có khối mã để bắt lỗi này và thông báo cho người dùng về lỗi bằng cách hiển thị một thông báo thích hợp. Trong Java, tất cả các ngoại lệ đều là ngoại lệ được kiểm tra, ngoại trừ những ngoại lệ được chỉ ra bởi Error, RuntimeException và các lớp con của chúng.
Unchecked Exceptions (Ngoại lệ không được kiểm tra)
Các ngoại lệ không được kiểm tra bao gồm:
- Error: Đây là những ngoại lệ nằm bên ngoài ứng dụng. Thường thì ứng dụng không thể dự kiến hoặc phục hồi từ các lỗi này. Ví dụ, giả sử người dùng chỉ định tên tệp đúng cho tệp cần mở và tệp tồn tại trên hệ thống. Tuy nhiên, runtime không thể đọc tệp do một số lỗi phần cứng hoặc sự cố hệ thống. Tình trạng đọc không thành công như vậy sẽ ném ngoại lệ java.io.IOError. Trong trường hợp này, ứng dụng có thể bắt lỗi này và hiển thị thông báo thích hợp cho người dùng hoặc để chương trình in một stack trace và thoát. Các lỗi là các ngoại lệ được tạo ra bởi lớp Error và các lớp con của nó.
- RuntimeException: Những ngoại lệ này nằm bên trong ứng dụng và thường thì ứng dụng không thể dự kiến hoặc phục hồi từ các ngoại lệ này. Những ngoại lệ này thường chỉ ra các lỗi lập trình, chẳng hạn như lỗi logic hoặc việc sử dụng không đúng cách của một API. Ví dụ, giả sử người dùng chỉ định tên tệp cần mở. Tuy nhiên, do một số lỗi logic, một null được truyền cho ứng dụng, sau đó ứng dụng sẽ ném một ngoại lệ NullPointerException. Ứng dụng có thể chọn bắt lỗi này và hiển thị thông báo thích hợp cho người dùng hoặc khắc phục lỗi gây ra ngoại lệ. Ngoại lệ RuntimeException được chỉ ra bởi lớp RuntimeException và các lớp con của nó.
Error và ngoại lệ RuntimeException được gọi chung là ngoại lệ không được kiểm tra.
Trong Java, lớp Object là lớp cơ sở của toàn bộ hệ thống lớp. Lớp Throwable là lớp cơ sở của tất cả các lớp ngoại lệ. Lớp Object là lớp cơ sở của Throwable. Lớp Throwable có hai lớp con trực tiếp là Exception và Error. Cấu trúc lớp Throwable được hiển thị trong hình:
Danh sách một số checked exception:
- InstantiationException: Xảy ra khi có sự cố trong việc tạo một thể hiện của một lớp trừu tượng.
- InterruptedException: Xảy ra khi một luồng bị gián đoạn bởi một luồng khác.
- NoSuchMethodException: Xảy ra khi JVM không thể xác định phương thức nào để gọi.
- ArithmeticException: Cho biết một điều kiện lỗi toán học.
- ArrayIndexOutOfBoundsException: Xảy ra nếu chỉ số mảng nhỏ hơn không hoặc lớn hơn kích thước thực sự của mảng.
- IllegalArgumentException: Xảy ra nếu một phương thức nhận đối số không hợp lệ.
- NegativeArraySizeException: Xảy ra nếu kích thước mảng nhỏ hơn không.
- NullPointerException: Xảy ra khi cố gắng truy cập thành viên của một đối tượng null.
- NumberFormatException: Xảy ra nếu không thể chuyển đổi một chuỗi thành một số.
- StringIndexOutOfBoundsException: Xảy ra nếu chỉ số âm hoặc lớn hơn kích thước của chuỗi.
Exception Class
Lớp Exception và các lớp con của nó cho biết các điều kiện mà một ứng dụng có thể cố gắng xử lý. Lớp Exception và tất cả các lớp con của nó, ngoại trừ RuntimeException và các lớp con của nó, đều là các ngoại lệ được kiểm tra (checked exceptions). Các ngoại lệ kiểm tra phải được khai báo trong mệnh đề throws của một phương thức hoặc hàm tạo nếu phương thức hoặc hàm tạo có khả năng ném ra ngoại lệ trong quá trình thực thi và truyền nó lên trên ngăn xếp cuộc gọi. Đoạn mã duới hiển thị cấu trúc của lớp Exception:
public class Exception extends Exception {
// todo
}
Danh sách hàm khởi tạo Exeption:
- Exception() Xây dựng một ngoại lệ mới với thông báo lỗi được đặt thành null.
- Exception(String message) Xây dựng một ngoại lệ mới với thông báo lỗi được đặt thành thông báo chuỗi cụ thể đã cho.
- Exception(String message, Throwable cause) Xây dựng một ngoại lệ mới với thông báo lỗi được đặt thành thông báo chuỗi đã cho và nguyên nhân.
- Exception(Throwable cause) Xây dựng một ngoại lệ mới với nguyên nhân đã cho. Thông báo lỗi được đặt dựa trên sự đánh giá của cause == null ? null : cause.toString(). Điều này có nghĩa là nếu cause là null, nó sẽ trả về null, ngược lại nó sẽ trả về biểu diễn chuỗi của thông báo. Thông báo thường là tên lớp và thông điệp chi tiết của nguyên nhân.
Danh sách phương thức của Lớp Exception:
public String getMessage()
Mô tả Trả về thông tin về ngoại lệ đã xảy ra.
Public Throwable getCause()
Trả về nguyên nhân của ngoại lệ, được biểu diễn bởi một đối tượng Throwable.
Public String toString()
Nếu đối tượng Throwable được tạo với một chuỗi thông báo không phải là null, nó trả về kết quả của getMessage() cùng với tên của lớp ngoại lệ được nối vào. Nếu đối tượng Throwable được tạo với một chuỗi thông báo null, nó trả về tên của lớp thực tế của đối tượng.
Public void printStackTrace()
In kết quả của phương thức toString () và dãy ngăn xếp vào system.ezr, tức là luồng đầu ra lỗi.
public StackTraceElement [] getStackTrace()
Trả về một mảng, trong đó mỗi phần tử chứa một khung của dãy ngăn xếp. Index 0 đại diện cho phương thức ở đầu của dãy gọi và phần tử cuối cùng đại diện cho phương thức ở dưới cùng của dãy gọi.
Public Throwable fillInStackTrace()
Điền dãy ngăn xếp của đối tượng Throwable này bằng dãy ngăn xếp hiện tại, bổ sung vào bất kỳ thông tin trước đó nào trong dãy ngăn xếp.
Một ngoại lệ chỉ ra rằng đã xảy ra một vấn đề, nhưng đó không phải là một vấn đề nghiêm trọng của hệ thống. Hầu hết các chương trình có khả năng gây ra và bắt ngoại lệ so với lỗi.
Nền tảng Java xác định nhiều lớp con của lớp Exception. Các lớp con này chỉ ra các loại ngoại lệ khác nhau có thể xảy ra. Ví dụ, một IllegalAccessException chỉ ra rằng một phương thức đã được gọi không thể tìm thấy. Tương tự, một NegativeArraySizeException chỉ ra rằng một chương trình đã cố gắng tạo một mảng với kích thước âm.
Lớp con RuntimeException của lớp Exception được dành riêng cho những ngoại lệ chỉ ra việc sử dụng sai API. Ví dụ, NullPointerException xảy ra khi một phương thức cố gắng truy cập thành viên của một đối tượng thông qua một tham chiếu null.
Xử lý Exception trong Java
Bất kỳ ngoại lệ nào mà một phương thức có khả năng gây ra được coi là một phần của giao diện lập trình của phương thức đó, giống như các tham số và giá trị trả về của nó. Mã gọi một phương thức phải biết về các ngoại lệ mà phương thức có thể gây ra. Điều này giúp người gọi quyết định cách xử lý chúng khi chúng xảy ra.
Nhiều ngoại lệ runtime có thể xảy ra ở bất kỳ đâu trong chương trình. Việc phải thêm mã để xử lý các ngoại lệ runtime trong mọi khai báo phương thức có thể làm giảm tính rõ ràng của chương trình. Do đó, trình biên dịch không yêu cầu người dùng phải bắt hoặc chỉ định ngoại lệ runtime, tuy nó cũng không phản đối điều này.
Tình huống phổ biến mà người dùng có thể gây ra một RuntimeException là khi người dùng gọi một phương thức không đúng cách. Ví dụ, một phương thức có thể kiểm tra trước xem một trong các đối số của nó có được chỉ định sai như là null hay không. Trong trường hợp đó, phương thức có thể gây ra một NullPointerException, đó là một ngoại lệ không kiểm tra.
Vì vậy, nếu một client có khả năng phục hồi từ một ngoại lệ, hãy làm cho nó trở thành một ngoại lệ kiểm tra. Nếu một client không thể làm gì để phục hồi từ ngoại lệ, hãy làm cho nó trở thành một ngoại lệ không kiểm tra.
Khối try-catch
Bước đầu tiên trong việc tạo một xử lý ngoại lệ là xác định mã có thể gây ra ngoại lệ và bao nó trong khối try. Cú pháp cho việc khai báo một khối try như sau:
try {
// câu lệnh 1
// câu lệnh 2
// ...
}
Các câu lệnh bên trong khối try có thể gây ra một ngoại lệ. Khi ngoại lệ xảy ra, nó được bắt bởi khối try và runtime tìm kiếm một xử lý thích hợp để xử lý ngoại lệ đó. Để xử lý ngoại lệ, người dùng phải xác định một khối catch trong phương thức đã gây ra ngoại lệ hoặc ở một nơi cao hơn trong ngăn xếp cuộc gọi phương thức.
Khối catch chứa mã để xử lý ngoại lệ cụ thể. Nó cung cấp một cơ hội để kiểm tra loại ngoại lệ và thực hiện các hành động tương ứng. Nếu không có khối catch phù hợp, ngoại lệ sẽ được chuyển tiếp lên ngăn xếp cuộc gọi để tìm một xử lý ngoại lệ khác phù hợp.
Cú pháp:
try {
// Statements that may raise an exception
int result = 10 / 0; // This will throw an ArithmeticException
} catch (ExceptionType e) {
// Handling the exception
System.out.println("An ArithmeticException occurred: " + e.getMessage());
}
Ví dụ:
package session;
class Mathematics {
/**
* Divides two integers
*
* @param num1 an integer variable storing the value of the first number
* @param num2 an integer variable storing the value of the second number
* @return void
*/
public void divide(int num1, int num2) {
// Create the try block
try {
// Statement that can cause an exception
System.out.println("Division is: " + (num1 / num2));
} catch (ArithmeticException e) { // Catch block for ArithmeticException
// Display an error message to the user
System.out.println("Error: " + e.getMessage());
}
// Rest of the method
System.out.println("Method execution completed");
}
}
public class TestMath {
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
// Check the number of command line arguments
if (args.length == 2) {
// Instantiate the Mathematics class
Mathematics objMath = new Mathematics();
// Invoke the divide(int, int) method
objMath.divide(Integer.parseInt(args[0]), Integer.parseInt(args[1]));
} else {
System.out.println("Usage: java TestMath <number1> <number2>");
}
}
}
Lớp Mathematics bao gồm một phương thức có tên là divide() nhận hai số nguyên làm tham số và cố gắng thực hiện phép chia chúng. Tuy nhiên, có khả năng xảy ra lỗi nếu người dùng đặt giá trị của số thứ hai (mẫu số) bằng không. Để xử lý lỗi tiềm ẩn này, phép chia được bao quanh trong một khối try.
Nếu một lỗi xảy ra trong quá trình chia (cụ thể, là một ArithmeticException do chia cho không), lỗi sẽ được bắt bởi khối catch xử lý ArithmeticException. Trong khối catch, phương thức getMessage() của đối tượng ArithmeticException (e) được sử dụng để hiển thị thông tin chi tiết về lỗi.
Phương thức main() trong lớp TestMath tạo một thể hiện của lớp Mathematics và gọi phương thức divide() với các tham số do người dùng cung cấp tại dòng lệnh.
Tóm lại, đoạn mã này minh họa cách sử dụng xử lý ngoại lệ để xử lý lỗi tiềm ẩn có thể xảy ra trong phép tính toán (chia cho không) và hiển thị thông báo lỗi có ý nghĩa cho người dùng.
Output:
Luồng thực thi ngoại lệ
Trong đoạn code trên, ngoại lệ chia cho không xảy ra khi thực thi câu lệnh num1/num2. Nếu không có khối try-catch, bất kỳ mã nào sau câu lệnh này cũng không được thực thi vì một đối tượng ngoại lệ được tạo tự động. Vì không có khối try-catch nào tồn tại, JVM xử lý ngoại lệ, in ra dấu vết ngăn xếp (stack trace), và chương trình bị chấm dứt.
Hình dưới hiển thị quá trình thực thi mã khi không được cung cấp khối try-catch.
Tuy nhiên, khi khối try-catch được cung cấp, ngoại lệ chia cho không xảy ra trong mã được xử lý bởi khối try-catch và một thông báo ngoại lệ được hiển thị. Hơn nữa, phần còn lại của mã sẽ được thực thi bình thường.
Thừ khóa Throw và Throws
Java cung cấp các từ khóa throw và throws để tường minh ném ra một ngoại lệ trong phương thức main(). Từ khóa throw ném ra ngoại lệ trong một phương thức. Từ khóa throws cho biết ngoại lệ mà một phương thức có thể ném ra.
Mệnh đề throw yêu cầu một đối số là thể hiện/phiên bản (instance) của Throwable và ném ra các ngoại lệ kiểm tra hoặc không kiểm tra trong một phương thức. Đoạn mã dưới mô tả lớp Mathematics đã được sửa đổi sử dụng từ khóa throw và throws để xử lý ngoại lệ.
Ví dụ:
package session;
class Mathematics {
/**
* Divides two integers, throws ArithmeticException
*
* @param num1 an integer variable storing the value of the first number
* @param num2 an integer variable storing the value of the second number
* @throws ArithmeticException if num2 is zero
*/
public void divide(int num1, int num2) throws ArithmeticException {
// Check the value of num2
if (num2 == 0) {
// Throw the exception
throw new ArithmeticException("/ by zero");
} else {
System.out.println("Division is: " + (num1 / num2));
}
// Rest of the method
System.out.println("Method execution completed");
}
}
public class TestmathNew {
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
// Check the number of command line arguments
if (args.length == 2) {
// Instantiate the Mathematics class
Mathematics objMath = new Mathematics();
try {
// Invoke the divide(int, int) method
objMath.divide(Integer.parseInt(args[0]), Integer.parseInt(args[1]));
} catch (ArithmeticException e) {
// Display an error message to the user
System.out.println("Error: " + e.getMessage());
}
} else {
System.out.println("Usage: java Mathematics <number1> <number2>");
}
System.out.println("Back to Main");
}
}
Phương thức divide(int, int) bây giờ bao gồm mệnh đề throws để thông báo cho phương thức gọi biết rằng divide(int, int) có thể ném ra một ArithmeticException. Trong divide(int, int), mã kiểm tra giá trị của num. Nếu nó bằng 0, nó tạo một phiên bản của ArithmeticException bằng cách sử dụng từ khóa new với thông báo lỗi như một đối số. Từ khóa throw ném phiên bản này cho người gọi.
Trong phương thức main(), một phiên bản, objMath được sử dụng để gọi phương thức divide(int, int). Tuy nhiên, lần này, mã được viết trong khối try vì divide(int, int) có thể ném ra một ArithmeticException mà phương thức main() sẽ phải xử lý trong khối catch của nó.
Hình dưới mô tả kết quả của chương trình khi người dùng chỉ định 12 làm tử số và 0 làm mẫu số.
Hình dtrên cho thấy rằng khi thực thi mã, điều kiện if trở thành đúng và nó ném ra ArithmeticException. Kiểm soát trả về cho hàm gọi, tức là phương thức main() nơi nó cuối cùng được xử lý. Khối catch được thực thi và kết quả của getMessage() được hiển thị cho người dùng. Lưu ý rằng câu lệnh còn lại của phương thức divide(int, int) không được thực thi trong trường hợp này.
Hình dưới mô tả việc thực thi mã khi sử dụng các mệnh đề throw và throws:
Hình trên mô tả phương thức divide(int, int) được gọi trong khối try trong phương thức main(). Bên trong phương thức, ngoại lệ ArithmeticException được ném. Kiểm soát sau đó được chuyển đến khối catch của phương thức main(), in ra đầu ra của phương thức getMessage() và thực thi phần còn lại của mã trong phương thức main(). Tuy nhiên, các câu lệnh còn lại trong phương thức divide(int int) không được thực thi.
Sử dụng nhiều khối catch (multi catch blocks)
Người dùng có thể liên kết nhiều trình xử lý ngoại lệ với một khối try bằng cách cung cấp nhiều khối catch sau khối try. Cú pháp để khai báo một khối try với nhiều khối catch như sau:
try{...}
catch (ExceptionType <object_name>)
{...}
catch (ExceptionType2 <object_name>)
{...}
Trong trường hợp này, mỗi khối catch là một trình xử lý ngoại lệ xử lý một loại cụ thể được chỉ định bởi đối số exception-type (kiểu ngoại lệ) của nó. Khối catch bao gồm mã phải được thực thi nếu và khi trình xử lý ngoại lệ được gọi.
Hệ thống runtime gọi trình xử lý trong ngăn xếp cuộc gọi có exception-type phù hợp với loại ngoại lệ được ném. Mã dưới thể hiện việc sử dụng nhiều khối catch.
Ví dụ:
package session;
public class Calculate {
/**
* Main method to perform division and handle exceptions
*
* @param args the command line arguments
*/
public static void main(String[] args) {
// Check the number of command line arguments
if (args.length == 2) {
try {
// Parse command line arguments and perform division
int num1 = Integer.parseInt(args[0]);
int num2 = Integer.parseInt(args[1]);
int result = num1 / num2;
System.out.println("Division is: " + result);
} catch (ArithmeticException e) {
// Handle division by zero exception
System.out.println("Error: Division by zero is not allowed.");
} catch (NumberFormatException e) {
// Handle invalid input (non-integer) exception
System.out.println("Error: Required integers, found non-integer input.");
} catch (Exception e) {
// Handle other exceptions
System.out.println("Error: " + e.getMessage());
}
} else {
System.out.println("Usage: java Calculate <number1> <number2>");
}
}
}
Lớp Calculate bao gồm phương thức main(). Trong phương thức main(), khối try thực hiện phép chia của hai giá trị được chỉ định bởi người dùng. Như đã thấy trước đó, phép chia có thể dẫn đến lỗi chia cho không. Do đó, đã được tạo một khối catch tương ứng để xử lý ArithmeticException. Tuy nhiên, người ta có thể xác định một loại ngoại lệ khác mà cũng có thể xảy ra, đó là NumberFormatException. Điều này có thể xảy ra nếu người dùng chỉ định một chuỗi thay vì một số không thể chuyển đổi thành số nguyên bằng phương thức Integer.parseInt(). Do đó, một khối catch khác để xử lý NumberFormatException đã được xác định.
Lưu ý rằng khối catch cuối cùng với lớp Exception xử lý bất kỳ ngoại lệ nào khác có thể xảy ra trong mã. Vì Exception là lớp cha của tất cả các ngoại lệ, nó phải được sử dụng trong khối catch cuối cùng. Nếu lớp Exception được sử dụng với khối catch đầu tiên, nó sẽ xử lý tất cả các ngoại lệ và các khối catch khác, tức là các khối catch cho ArithmeticException và NumberFormatException sẽ không bao giờ được gọi. Nói cách khác, khi sử dụng nhiều khối catch, các khối catch với các lớp con ngoại lệ phải được đặt trước các lớp cha, nếu không, lớp cha ngoại lệ sẽ bắt các ngoại lệ cùng loại cũng như các lớp con của nó. Do đó, các khối catch với các lớp con ngoại lệ sẽ không bao giờ được thực hiện.
Hình dưới hiển thị kết quả của mã khi người dùng chỉ định 12 và 0 làm đối số.
Hình dưới hiển thị kết quả của mã khi người dùng chỉ định 12 và ‘abc’ làm đối số.
Hình dưới mô tả việc thực thi mã khi sử dụng nhiều khối catch.
Hình trên cho thấy rằng khối catch đầu tiên không có khả năng xử lý ngoại lệ cho tình huống tham số này. Do đó, runtime tiếp tục tìm kiếm trong một khối catch khác. Điều này còn được gọi là ngoại lệ sủi bọt (bubbling of exception). Nghĩa là ngoại lệ được truyền tiếp như một bong bóng. Ở đây, nó tìm thấy một khối catch phù hợp để xử lý NumberFormatException. Khối catch được thực thi và thông báo phù hợp được hiển thị cho người dùng. Một khi tìm thấy sự phù hợp, các khối catch còn lại sẽ bị bỏ qua và quá trình thực thi tiếp tục sau khối catch cuối cùng.
Khối finally
Java cung cấp khối finally để đảm bảo thực hiện một số lệnh cụ thể ngay cả khi một ngoại lệ xảy ra. Khối finally luôn được thực thi mà không phụ thuộc vào việc có xảy ra ngoại lệ trong khối try hay không. Điều này đảm bảo rằng mã dọn dẹp không được bỏ qua một cách tình cờ bởi lệnh return, break, hoặc continue. Do đó, viết mã dọn dẹp trong khối finally được xem là một giải pháp tốt (best pratice) ngay cả khi không dự đoán có xảy ra ngoại lệ.
Khối finally thường được sử dụng để ngăn chặn rò rỉ tài nguyên. Các nhiệm vụ như đóng một tệp và kết nối mạng, đóng luồng nhập/xuất, hoặc khôi phục tài nguyên phải được thực hiện trong một khối finally để đảm bảo rằng một tài nguyên được khôi phục ngay cả khi có một ngoại lệ xảy ra.
Tuy nhiên, nếu vì một lý do nào đó, JVM thoát khỏi quá trình thực hiện khối try hoặc catch, thì khối finally có thể không được thực thi. Tương tự, nếu một luồng đang thực hiện khối try hoặc catch bị gián đoạn hoặc bị kết thúc, thì khối finally có thể không được thực thi, mặc dù ứng dụng vẫn tiếp tục chạy.
Cú pháp:
try{
// statements may raise exceptions
// statement 1
// statement 2
}catch (ExceptionType ex){
// todo catch
}finally{
// todo
}
Ví dụ:
public class Calculate {
/**
* Main method to perform division and handle exceptions
*
* @param args the command line arguments
*/
public static void main(String[] args) {
// Check the number of command line arguments
if (args.length == 2) {
try {
int num1 = Integer.parseInt(args[0]);
int num2 = Integer.parseInt(args[1]);
int result = num1 / num2;
System.out.println("Division is: " + result);
} catch (ArithmeticException e) {
System.out.println("Error: Division by zero is not allowed.");
} catch (NumberFormatException e) {
System.out.println("Error: Required integer found non-integer input.");
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
} finally {
System.out.println("Executing Cleanup Code. Please Wait...");
// Add cleanup code for closing files, streams, or network connections here
System.out.println("All resources closed.");
}
} else {
System.out.println("Usage: java Calculate <number1> <number2>");
}
}
}
Trong lớp Calculate, khối finally được bao gồm sau khối catch cuối cùng. Trong trường hợp này, ngay cả khi một ngoại lệ xảy ra trong mã, các câu lệnh trong khối finally sẽ được thực thi. Hình dưới hiển thị kết quả của Đoạn mã sau khi sử dụng khối finally khi người dùng truyền đối số dòng lệnh ’12’ và ‘zero’.
Hình dưới mô tả cách khối finally được xử lý:
Hình trên cho thấy sau khi xử lý ngoại lệ trong khối catch thứ hai, sau đó quyền điều khiển được chuyển đến khối finally. Các câu lệnh trong khối finally được thực thi và quá trình thực thi và hoàn thành chương trình.
Hướng dẫn xử lý ngoại lệ
Dưới đây là một số hướng dẫn khi xử lý các ngoại lệ trong Java:
- Sử dụng try-catch hoặc try-finally: Lệnh try phải được theo sau bởi ít nhất một khối catch hoặc finally. Điều này đảm bảo rằng các ngoại lệ được xử lý một cách đúng đắn hoặc mã dọn dẹp cần thiết được thực thi.
- Sử dụng throw và throws: Sử dụng lệnh throw để ném một ngoại lệ mà một phương thức không xử lý được. Nếu một phương thức có thể ném một ngoại lệ kiểm tra, hãy khai báo nó bằng cách sử dụng mệnh đề throws trong khai báo phương thức.
- Luôn sử dụng finally: Sử dụng khối finally để viết mã dọn dẹp, đặc biệt là cho các nhiệm vụ như đóng tệp, kết nối mạng hoặc khôi phục tài nguyên. Khối finally đảm bảo rằng mã dọn dẹp được thực thi ngay cả khi có ngoại lệ xảy ra.
- Chọn lớp ngoại lệ thích hợp: Sử dụng các lớp con của ngoại lệ khi người gọi của một phương thức được kỳ vọng xử lý ngoại lệ. Điều này giúp cung cấp các thông báo lỗi có ý nghĩa hơn và cho phép trình biên dịch kiểm tra việc xử lý ngoại lệ.
- Tránh bắt ngoại lệ rộng rãi: Tránh bắt ngoại lệ chung chung như java.lang.Exception hoặc java.lang.Throwable vì chúng có thể bắt tất cả các ngoại lệ, kể cả các ngoại lệ thời gian chạy. Điều này có thể làm cho việc hiểu nguyên nhân của ngoại lệ trở nên khó khăn.
- Bao gồm các thông báo có ý nghĩa: Khi có ngoại lệ xảy ra, cung cấp một thông báo thích hợp cùng với thông báo mặc định của ngoại lệ. Truyền tất cả dữ liệu cần thiết vào hàm tạo của lớp ngoại lệ để hỗ trợ trong việc hiểu và giải quyết vấn đề.
- Xử lý ngoại lệ gần nguồn: Hãy cố gắng xử lý ngoại lệ càng gần mã nguồn càng tốt. Nếu người gọi có thể thực hiện các biện pháp sửa chữa, hãy khắc phục điều kiện đó ngay tại đó để tránh truyền ngoại lệ đi xa hơn.
- Không lạm dụng ngoại lệ: Ngoại lệ không nên được sử dụng để chỉ định các điều kiện nhánh bình thường có thể thay đổi luồng mã. Sử dụng ngoại lệ cho các tình huống ngoại lệ đòi hỏi xử lý đặc biệt.
- Tránh ném lại nhiều lần: Hãy tránh việc ném lại ngoại lệ cùng một lỗi nhiều lần vì điều này có thể làm chậm chương trình có tiền lệ thường ném ngoại lệ.
- Tránh viết khối catch trống: Khối catch trống không cung cấp bất kỳ thông tin nào cho người dùng và tạo ra ấn tượng rằng chương trình thất bại vì lý do không rõ ràng.
Bài tập
Bài tập: Quản lý Tài liệu
Bài 1:
Tạo một interface tên là `ITaiLieu` trong gói `hvc.data`. Interface `ITaiLieu` có các phương thức sau:
- `void nhap();`
- `void hienThi();`
Bài 2:
Tạo một lớp tên là `Sach` trong gói `hvc.data.impl`. Lớp `Sach` phải triển khai interface `ITaiLieu`. Lớp `Sach` cũng bao gồm các thuộc tính sau:
- `int id;`
- `String tenSach;`
- `String tenTacGia;`
- `float gia;`
Các yêu cầu:
- Tất cả các thuộc tính phải được khai báo là `private`.
- Tạo hai constructor cho lớp `Sach`, một constructor không có tham số và một constructor với đầy đủ 4 tham số để gán giá trị cho các thuộc tính.
- Tạo các phương thức getter và setter cho từng thuộc tính.
#### Bài 3:
Triển khai hai phương thức của lớp `Sach` được kế thừa từ interface `ITaiLieu`:
- Phương thức `nhap`: yêu cầu người dùng nhập `id`, `tenSach`, `tenTacGia` và `gia` cho đối tượng `Sach`. Sau đó gán các giá trị nhập vào cho các thuộc tính tương ứng.
- Phương thức `hienThi`: hiển thị thông tin của đối tượng `Sach`.
Bài 4:
Tạo một lớp tên là `QuanLyTaiLieu` trong gói `hvc.data.manager`. Lớp `QuanLyTaiLieu` có các phương thức sau:
- `void themTaiLieu()`: yêu cầu người dùng nhập số lượng sách `n` cần lưu trữ. Sau đó cho phép người dùng nhập thông tin `n` cuốn sách vào một mảng `Sach`.
- `void hienThiTatCaTaiLieu()`: hiển thị tất cả thông tin của từng đối tượng `Sach` trong mảng.
- `void timKiemTheoTacGia(String tenTacGia)`: cho phép người dùng nhập tên tác giả rồi tìm kiếm trong mảng `Sach` và hiển thị thông tin của các sách phù hợp.
Bài 5:
Tạo một lớp tên là `KiemTra` trong gói `hvc.data.test`. Lớp `KiemTra` chứa phương thức `main`, thực hiện các công việc sau:
- Hiển thị menu, cho phép người dùng nhập lựa chọn:
1. Thêm sách mới
2. Hiển thị tất cả sách
3. Tìm kiếm sách theo tên tác giả
4. Thoát
- Nếu người dùng chọn 1, gọi phương thức `themTaiLieu` của lớp `QuanLyTaiLieu`. Quay lại menu sau khi hoàn thành.
- Nếu người dùng chọn 2, gọi phương thức `hienThiTatCaTaiLieu` của lớp `QuanLyTaiLieu`. Quay lại menu sau khi hoàn thành.
- Nếu người dùng chọn 3, cho phép người dùng nhập tên tác giả, gọi phương thức `timKiemTheoTacGia` với tên tác giả đã nhập. Quay lại menu sau khi hoàn thành.
- Nếu người dùng chọn 4, kết thúc chương trình.
Bài tập 2: Hệ Thống Quản Lý Chuyến Dã Ngoại
Trong bài kiểm tra này, bạn sẽ xây dựng một hệ thống quản lý chuyến dã ngoại đơn giản bằng cách áp dụng các nguyên tắc lập trình hướng đối tượng (OOP). Hệ thống sẽ quản lý các chuyến đi dã ngoại với các thuộc tính như mã chuyến đi, điểm đến, ngày khởi hành, và số người tham gia.
Câu 1: Lớp Trừu Tượng cho Trip
a) Tạo lớp trừu tượng: Trip
Nhiệm vụ:
- Tạo một lớp trừu tượng có tên là **Trip** trong package `com.outdoor.trips`. Lớp này cần có các thuộc tính sau:
- String id protected
- String destination protected
- String date protected
- String numberOfParticipants protected
Tạo hai constructor:
- Một constructor mặc định.
- Một constructor có tham số.
- Tạo các phương thức getter và setter cho các thuộc tính.
- Định nghĩa hai phương thức trừu tượng:
- `public abstract void enterInfo();`
- `public abstract void showInfo();`
b) Tạo lớp FamilyTrip
Nhiệm vụ:
- Tạo một lớp có tên là **FamilyTrip** trong package `com.outdoor.trips.family`, kế thừa từ lớp **Trip**. Thêm các thuộc tính sau:
- boolean hasChildren private
- boolean familyType private
Tạo hai constructor:
- Một constructor không tham số.
- Một constructor có 6 tham số để thiết lập giá trị cho các thuộc tính được thừa kế và các thuộc tính mới. Sử dụng từ khóa `this` để gọi constructor của lớp cha.
- Tạo các phương thức getter và setter cho các thuộc tính mới.
- Triển khai các phương thức `enterInfo()` và `showInfo()` từ lớp cha **Trip**:
- `enterInfo()`: Yêu cầu người dùng nhập giá trị cho tất cả các thuộc tính của đối tượng **FamilyTrip**.
- `showInfo()`: In ra tất cả các thuộc tính của đối tượng **FamilyTrip**.
Câu 2: Lớp AdventureTrip
a) Tạo lớp AdventureTrip
Nhiệm vụ:
- Tạo một lớp có tên là **AdventureTrip** trong package `com.outdoor.trips.adventure`, kế thừa từ lớp **Trip**. Thêm các thuộc tính sau:
- String activityType private
- String difficultyLevel private
Tạo hai constructor:
- Một constructor không tham số.
- Một constructor có 6 tham số để thiết lập giá trị cho các thuộc tính thừa kế và các thuộc tính mới. Sử dụng từ khóa `this` để gọi constructor của lớp cha.
- Tạo các phương thức getter và setter cho các thuộc tính mới.
- Triển khai các phương thức `enterInfo()` và `showInfo()` từ lớp cha **Trip**:
- `enterInfo()`: Yêu cầu người dùng nhập giá trị cho tất cả các thuộc tính của đối tượng **AdventureTrip**.
- `showInfo()`: In ra tất cả các thuộc tính của đối tượng **AdventureTrip**.
b) Tính toán thời gian ước tính cho chuyến đi
Nhiệm vụ:
- Tạo một phương thức mới có tên là `estimateDuration()` trong lớp **AdventureTrip** để tính toán thời gian ước tính của chuyến đi dựa trên mức độ khó và loại hoạt động. Ví dụ:
- Loại hoạt động:
- Đi bộ đường dài (Hiking): 1 ngày cho mỗi cấp độ khó.
- Chèo thuyền (Rafting): 1,5 ngày cho mỗi cấp độ khó.
- Leo núi (Mountain Climbing): 2 ngày cho mỗi cấp độ khó.
- Ví dụ tính toán:
- Nếu hoạt động là "Hiking" và mức độ khó là 4, thời gian ước tính sẽ là 4 ngày (1 ngày * mức độ khó 4). Nếu hoạt động là "Rafting" và mức độ khó là 3, thời gian ước tính sẽ là 4,5 ngày (1,5 ngày * mức độ khó 3).
Triển khai:
- Tạo phương thức `estimateDuration()` trả về số ngày ước tính dựa trên các tiêu chí trên.
- Sau khi gọi phương thức `showInfo()`, cũng in ra thời gian ước tính của chuyến đi bằng phương thức này.
Kết quả mong đợi:
```
Mã chuyến đi: TR001
Điểm đến: Grand Canyon
Loại hoạt động: Hiking
Mức độ khó: 4
Thời gian ước tính cho chuyến đi: 4 ngày
```
Câu 3: Kiểm tra và hệ thống menu
a) Menu chính
Nhiệm vụ:
- Tạo một lớp có tên là **TripTest** trong package `com.outdoor.test`. Lớp này sẽ chứa phương thức `main()` để triển khai chương trình điều khiển bằng menu. Menu sẽ cung cấp các tùy chọn sau:
```
Vui lòng chọn:
1. Nhập thông tin cho n chuyến dã ngoại gia đình.
2. Nhập thông tin cho n chuyến dã ngoại mạo hiểm.
3. Hiển thị thông tin của n chuyến dã ngoại gia đình (Sắp xếp theo số lượng người tham gia giảm dần).
4. Hiển thị thông tin của n chuyến dã ngoại mạo hiểm (Sắp xếp theo mức độ khó tăng dần).
5. Thoát.
Lựa chọn của bạn:
```
b) Nhập thông tin cho chuyến dã ngoại gia đình
Nhiệm vụ:
- Khi người dùng chọn tùy chọn 1, yêu cầu người dùng nhập số lượng chuyến dã ngoại gia đình và lưu thông tin của chúng trong một mảng các đối tượng **FamilyTrip**.
c) Nhập thông tin cho chuyến dã ngoại mạo hiểm
Nhiệm vụ:
- Khi người dùng chọn tùy chọn 2, yêu cầu người dùng nhập số lượng chuyến dã ngoại mạo hiểm và lưu thông tin của chúng trong một mảng các đối tượng **AdventureTrip**.
d) Hiển thị chuyến dã ngoại gia đình đã sắp xếp
Nhiệm vụ:
- Khi người dùng chọn tùy chọn 3, hiển thị danh sách các đối tượng **FamilyTrip** đã được sắp xếp theo số lượng người tham gia giảm dần.
e) Hiển thị chuyến dã ngoại mạo hiểm đã sắp xếp
Nhiệm vụ:
- Khi người dùng chọn tùy chọn 4, hiển thị danh sách các đối tượng **AdventureTrip** đã được sắp xếp theo mức độ khó tăng dần.
f) Thoát chương trình
Nhiệm vụ:
- Khi người dùng chọn tùy chọn 5, thoát chương trình.