Exceptions (ngoại lệ) và Assertions (xác nhận)
- 01-11-2023
- Toanngo92
- 0 Comments
Ngôn ngữ lập trình Java cung cấp khả năng xử lý các sự kiện không mong muốn trong chương trình. Điều này bởi vì, ngay cả khi mã code được viết tốt, nó có thể hoạt động không chính xác. Với sự giúp đỡ của xử lý ngoại lệ, người phát triển có thể đảm bảo rằng người dùng sẽ nhận được một thông báo chính xác trong trường hợp có lỗi xảy ra trong chương trình. Nó cũng giúp đảm bảo rằng trong trường hợp có lỗi, dữ liệu và quy trình mà chương trình đang sử dụng sẽ không bị ảnh hưởng xấu.
1.2 Tổng quan về Ngoại lệ
Một ngoại lệ là một sự kiện xảy ra trong quá trình thực thi chương trình dẫn đến sự gián đoạn của luồng thực thi bình thường của các hướng dẫn trong chương trình. Xử lý ngoại lệ trong Java là một cách để đảm bảo việc thực thi suôn sẻ của chương trình. Để xử lý ngoại lệ, Java cung cấp các khối try-catch-finally. Sử dụng các khối này, người phát triển có thể kiểm tra các câu lệnh trong chương trình để xử lý lỗi trong trường hợp chúng xảy ra.
1.3 Xử lý Ngoại lệ
Các loại ngoại lệ và lỗi là các lớp con của lớp Throwable và được sử dụng để xử lý các ngoại lệ xảy ra trong quá trình thực thi chương trình. Một ví dụ về ngoại lệ phổ biến là NullPointerException (Ngoại lệ Null).
Oracle phân loại các ngoại lệ thành ba danh mục như sau:
- Ngoại lệ kiểm tra (Checked Exception)
- Ngoại lệ không kiểm tra (Unchecked Exception)
- Lỗi (Error)
Hệ thống phân cấp exceptions và errors
Một đối tượng được tạo bởi một phương thức nếu một ngoại lệ xảy ra trong phương thức đó. Đối tượng này được gọi là đối tượng Ngoại lệ và nó được gửi đến hệ thống thời gian chạy (runtime system). Đối tượng ngoại lệ này chứa tên và mô tả của ngoại lệ và thông tin về trạng thái hiện tại của chương trình tại nơi xảy ra ngoại lệ. Việc tạo một đối tượng Ngoại lệ và gửi nó đến hệ thống thời gian chạy được gọi là “ném ngoại lệ” (throwing an exception). Có thể là hệ thống tự ném ngoại lệ hoặc người phát triển có thể tự ném. Một danh sách các phương thức sẽ được gọi để tìm phương thức mà ngoại lệ đã xảy ra. Quá trình này gọi là “Call Stack” (Ngăn xếp cuộc gọi).
Có một số từ khóa được sử dụng để xử lý ngoại lệ trong Java. Chúng gồm các từ khóa: try, catch, throw, throws và finally. Một cách ngắn gọn, dưới đây là cách các từ khóa này hoạt động. Các câu lệnh chương trình có thể gây ra ngoại lệ được bao quanh trong một khối try. Nếu một ngoại lệ xảy ra trong khối try, nó có thể được xử lý một cách thích hợp để tránh kết thúc đột ngột của chương trình. Một khối catch thường chứa mã code để xử lý ngoại lệ. Hệ thống thời gian chạy Java tự động ném các ngoại lệ do hệ thống tạo ra khi tình huống đòi hỏi. Từ khóa throw được sử dụng bởi người phát triển để tự tay ném một ngoại lệ. Từ khóa throws được sử dụng để khai báo các ngoại lệ có thể bị ném bởi một phương thức. Bất kỳ mã code nào phải được thực thi sau khi hoàn thành một khối try (và khối catch nếu có) được đặt trong một khối finally.
Trong bài viết này, một số khái niệm cũ được nhắc lại, có thể xem tại link:
Mục lục
Khối try-catch-finally
Để xử lý ngoại lệ, người phát triển phải xác định các câu lệnh có thể gây ra ngoại lệ và đặt chúng trong một khối try. Tiếp theo, các xử lý ngoại lệ phải được kết nối với khối try bằng cách cung cấp một hoặc nhiều khối catch ngay sau khối try.
Cú pháp của một khối try-catch có dạng như sau:
try {
// Các câu lệnh có thể gây ra ngoại lệ
} catch (ExceptionType1 e1) {
// Xử lý ngoại lệ kiểu 1
} catch (ExceptionType2 e2) {
// Xử lý ngoại lệ kiểu 2
}
Mỗi khối catch hoạt động như một trình xử lý ngoại lệ xử lý một loại cụ thể của ngoại lệ. Tham số ExceptionType chỉ ra loại ngoại lệ mà trình xử lý này có thể xử lý. Tham số này phải là tên của một lớp kế thừa từ lớp Throwable.
Mã code bên trong khối catch sẽ được thực thi khi trình xử lý ngoại lệ được gọi. Khối catch đầu tiên trong ngăn xếp cuộc gọi mà loại ngoại lệ của nó khớp với loại ngoại lệ bị ném sẽ được gọi bởi hệ thống thời gian chạy để xử lý ngoại lệ.
Ví dụ:
public class MultipleCatchExample {
public static void main(String[] args) {
try {
int[] numbers = {1, 2, 3};
int index = 4;
int result = numbers[index];
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Caught an ArrayIndexOutOfBoundsException: " + e.getMessage());
} catch (ArithmeticException e) {
System.out.println("Caught an ArithmeticException: " + e.getMessage());
} catch (Exception e) {
System.out.println("Caught a generic Exception: " + e.getMessage());
}
System.out.println("Program continues after handling exceptions.");
}
}
Khối finally
Khối finally được thực thi ngay cả khi một ngoại lệ xảy ra trong khối try. Nó giúp người lập trình đảm bảo rằng mã dọn dẹp không bị vô tình bỏ qua bởi các câu lệnh break, continue hoặc return trong khối try. Điều khuyến nghị là viết mã dọn dẹp trong một khối finally, ngay cả khi không mong đợi có ngoại lệ nào.
Có một số trường hợp mà khối finally có thể không được thực thi, bao gồm:
Khi Máy ảo Java (JVM) thoát ra khi mã trong khối try hoặc catch đang được thực thi.
Khi luồng thực thi mã trong khối try hoặc catch bị gián đoạn hoặc bị kết thúc.
Trong những trường hợp này, khối finally có thể không được thực thi, ngay cả khi ứng dụng trong toàn bộ vẫn tiếp tục thực thi.
Ví dụ:
import java.io.FileNotFoundException;
import java.io.PrintWriter;
public class FileDemo {
PrintWriter objPwOut = null; // PrintWriter object
public void writeToFile() {
try {
// Initializing the PrintWriter with a filename
objPwOut = new PrintWriter("C:\\MyFile.txt");
} catch (FileNotFoundException ex) {
// Printing the error message
System.out.println("File Does not Exist " + ex.getMessage());
} finally {
// Verifying if the PrintWriter object is still open
if (objPwOut != null) {
// Closing the PrintWriter object
objPwOut.close();
System.out.println("Printwriter closed");
}
}
}
public static void main(String[] args) {
FileDemo demo = new FileDemo();
demo.writeToFile();
}
}
Trong đoạn mã, một đối tượng của lớp PrintWriter được tạo với tên tệp “MyFile.txt” làm đối số. Trong quá trình thực thi, khi PrintWriter cố gắng truy cập tệp “MyFile.txt” và gặp vấn đề, nó có thể ném một ngoại lệ nếu tệp không tồn tại, không thể ghi được hoặc nếu có lỗi khác xảy ra khi cố gắng mở hoặc tạo tệp.
Để xử lý tình huống này và thông báo cho người dùng về lỗi, một khối catch tương ứng được cung cấp để xử lý FileNotFoundException. Bên trong khối catch, một thông báo lỗi được in ra.
Khối finally được sử dụng để đảm bảo rằng, bất kể liệu có ngoại lệ được ném hay không, đối tượng PrintWriter sẽ được đóng đúng cách. Điều này quan trọng để làm sạch các tài nguyên và đảm bảo rằng tệp đã được đóng.
Tóm lại, mã xử lý các ngoại lệ có thể liên quan đến tệp, thông báo cho người dùng về lỗi và đảm bảo rằng đối tượng PrintWriter được đóng đúng cách, ngay cả khi có ngoại lệ.
Từ khóa throw và throws
Đôi khi, có thể cần cho phép một phương thức chuyển quyền kiểm soát lên trên trong ngăn xếp cuộc gọi để xử lý ngoại lệ. Ví dụ, khi kết nối đến máy chủ, người dùng có thể không thể dự đoán được định dạng mà máy chủ sẽ cung cấp cho mã ID. Trong trường hợp này, nên tránh việc bắt ngoại lệ và để cho một phương thức ở trên trong ngăn xếp cuộc gọi xử lý nó.
Nếu một phương thức không bắt các ngoại lệ kiểm tra mà có thể xảy ra trong phần thân của phương thức, nó phải chỉ định rằng nó có thể ném ra những ngoại lệ này.
Ví dụ, phương thức writeToFile() trong Đoạn mã trên có thể được chỉnh sửa để chỉ định những ngoại lệ mà nó có thể ném ra thay vì bắt chúng. Điều này có thể thực hiện bằng cách thêm mệnh đề throws vào khai báo phương thức, theo sau là một danh sách các ngoại lệ được phân cách bằng dấu phẩy mà phương thức có thể ném ra.
Mệnh đề throws được viết sau tên phương thức và danh sách đối số và trước dấu ngoặc mở của phương thức.
Ví dụ:
import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
public class WriteToFile {
public static void writeToFile() throws FileNotFoundException {
PrintWriter objPwout = null;
try {
objPwout = new PrintWriter("C:\\MyFile.txt");
objPwout.println("This is a test line.");
} finally {
if (objPwout != null) {
objPwout.close();
System.out.println("PrintWriter closed");
}
}
}
public static void main(String[] args) {
try {
writeToFile();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
Để bắt ngoại lệ ở điểm cao hơn trong cấu trúc hệ thống, cuối cùng mã phải ném ngoại lệ đó.
Ngoại lệ có thể được ném từ bất kỳ đâu, chẳng hạn từ tệp hiện tại, hoặc từ một tệp trong một gói khác, hoặc
các gói đi kèm với nền tảng Java, hoặc môi trường chạy Java. Bất kể mã nào
ném ngoại lệ, nó luôn được ném bằng cách sử dụng câu lệnh throw.
Nền tảng Java cung cấp một số lớp ngoại lệ là các lớp con của lớp Throwable.
Các lớp này cho phép chương trình phân biệt giữa các loại ngoại lệ khác nhau có thể xảy ra trong
quá trình thực thi của một chương trình.
Tất cả các phương thức sử dụng câu lệnh throw để ném một ngoại lệ. Câu lệnh throw nhận một đối số duy nhất
là một đối tượng Throwable. Các đối tượng Throwable là các thể hiện của bất kỳ lớp con nào của lớp Throwable.
Cú pháp:
throw <ThrowableObject>
Ví dụ 2:
import java.io.FileNotFoundException;
import java.io.PrintWriter;
public class Filewriting {
PrintWriter objPwOut;
// Declares the exception in the throws clause
public void writeToFile() throws FileNotFoundException {
try {
objPwOut = new PrintWriter("C:\\MyFile.txt");
} catch (FileNotFoundException ex) {
// Re-throwing the exception
throw ex;
} finally {
if (objPwOut != null) {
objPwOut.close();
System.out.println("PrintWriter closed");
}
}
}
public static void main(String[] args) {
try {
Filewriting fw = new Filewriting();
fw.writeToFile();
} catch (FileNotFoundException ex) {
System.out.println("File does not exist"); // Catching the exception
}
}
}
Ném ngoại lệ từ các phương thức
Một người có thể ném ngoại lệ trực tiếp từ một phương thức và chuyển nó đến một phương thức ở phía trên trong cấu trúc phân cấp. Ví dụ:
Tạo file Calculator.java
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com;
/**
*
* @author toan1
*/
public class Calculator {
public void divide(int a, int b) throws ArithmeticException {
if (b == 0) {
throw new ArithmeticException("Denominator cannot be set to zero");
}
int result = a / b;
System.out.println("Result is " + result);
}
}
Tạo file TestCalculator.java
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com;
/**
*
* @author toan1
*/
public class TestCalculator {
public static void main(String[] args) {
try {
Calculator objCalc = new Calculator();
// Invoking the divide() method
objCalc.divide(Integer.parseInt(args[0]), Integer.parseInt(args[1]));
} catch (ArithmeticException ex) {
System.out.println("Arithmetic Exception: " + ex.getMessage());
}
}
}
Trong đoạn mã, phương thức divide() khai báo rằng nó có thể ném một ArithmeticException bằng cách sử dụng mệnh đề throws. Khi phương thức divide() phát hiện rằng mẫu số b được thiết lập thành 0 trong thời gian chạy (điều này sẽ dẫn đến lỗi toán học), nó ném ArithmeticException. Sau đó, ngoại lệ này được truyền lên theo ngăn xếp gọi, và trách nhiệm xử lý nó thuộc về một phương thức khác, trong trường hợp này là phương thức main().
Phương thức main() chứa một khối catch để bắt ngoại lệ ArithmeticException và cung cấp một thông báo cho người dùng. Đây là một cách để xử lý ngoại lệ theo cách lớp, với các phương thức khác nhau ở các cấp độ của ngăn xếp cuộc gọi có trách nhiệm bắt và xử lý ngoại lệ phù hợp với ngữ cảnh của họ.
Xử lý nhiều Exception trong một khối Catch
Từ phiên bản Java SE 7 trở đi, bạn có thể xử lý nhiều ngoại lệ trong một khối catch duy nhất. Tính năng này giúp giảm sự trùng lặp mã và ngăn việc sử dụng ngoại lệ tổng quát quá nhiều.
Để tạo một khối catch xử lý nhiều ngoại lệ, bạn chỉ cần liệt kê các loại ngoại lệ mà khối catch đó có thể xử lý, ngăn cách nhau bằng dấu thẳng đứng (|) như sau:
Cú pháp:
try{
// todo
}catch (ExceptionType1|ExceptionType2 ex){
// todo
}
Bởi vì catch block xử lý nhiều loại exception, nên tham số catch được khai báo là final. Vì vậy, bạn không thể gán bất kỳ giá trị nào cho nó bên trong khối catch.
Ví dụ:
public class Calculator {
public static void main(String[] args) {
int result, sum;
int[] marks = {20, 30, 50};
try {
result = Integer.parseInt(args[0]) / Integer.parseInt(args[1]);
System.out.println("Result is " + result);
sum = 0;
for (int i = 0; i < marks.length; i++) {
sum += marks[i];
}
System.out.println("Sum is " + sum);
} catch (ArrayIndexOutOfBoundsException | ArithmeticException ex) {
// Catching multiple exceptions
throw ex;
}
}
}
Trong đoạn mã, dòng 1 có thể gây ra ngoại lệ ArithmeticException nếu args[1] được thiết lập thành 0 khi chạy. Tương tự, dòng 2 sẽ gây ra ngoại lệ ArrayIndexOutOfBoundsException vì điều kiện kết thúc của vòng lặp là i < 4 trong khi mảng marks[] chỉ có ba phần tử. Để xử lý cả hai loại ngoại lệ này, đã được định nghĩa một khối catch duy nhất. Nó sẽ hiển thị thông báo lỗi tùy thuộc vào loại ngoại lệ được ném, tức là ArrayIndexOutOfBoundsException hoặc ArithmeticException. Từ khóa throw được sử dụng để ném ngoại lệ xảy ra.
Output:
Sử dụng try-with-resources và interface AutoCloseable
Lệnh try-with-resources là một lệnh try mà khai báo một hoặc nhiều tài nguyên. Một tài nguyên là một đối tượng phải được đóng sau khi chương trình hoàn thành việc sử dụng nó. Lệnh try-with-resources được viết để đảm bảo rằng mỗi tài nguyên sẽ được đóng khi kết thúc lệnh. Bất kỳ đối tượng nào thực hiện java.lang.AutoCloseable, bao gồm tất cả các đối tượng thực hiện java.io.Closeable, có thể được sử dụng như một tài nguyên. Giao diện AutoCloseable được sử dụng để đóng tài nguyên khi nó không còn cần thiết nữa.
Người dùng có thể tìm danh sách các lớp mà thực hiện một trong hai giao diện AutoCloseable và Closeable trong Javadoc của các giao diện này. Giao diện Closeable mở rộng giao diện AutoCloseable. Closeable là nguồn hoặc đích của dữ liệu có thể được đóng. Phương thức close() được gọi để giải phóng tài nguyên mà đối tượng đang giữ, chẳng hạn như các tệp đã mở.
Phương thức close() của giao diện Closeable ném các ngoại lệ thuộc loại IOException trong khi phương thức close() của giao diện AutoCloseable ném các ngoại lệ thuộc loại Exception. Do đó, các lớp con của giao diện AutoCloseable có thể ghi đè hành vi này của phương thức close() để ném các ngoại lệ chuyên biệt, chẳng hạn như IOException, hoặc không ném ngoại lệ nào cả.
Ví dụ:
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
public class FileWriting {
public void writeToFile(String path) {
// Creating a try-with-resources statement
try (Writer output = new BufferedWriter(new FileWriter(path))) {
output.write("This is a sample statement.");
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
}
public static void main(String[] args) {
FileWriting f = new FileWriting();
f.writeToFile("C:\\Misc\\test.txt");
}
}
Trong đoạn mã, một đối tượng của lớp BufferedWriter đã được tạo và khởi tạo với tham chiếu của BufferedWriter ghi một dòng vào tệp được chỉ định bởi biến path. Ở đây, BufferedWriter là một tài nguyên mà phải được đóng sau khi chương trình đã sử dụng nó xong. Lệnh khai báo xuất hiện trong dấu ngoặc đơn ngay sau từ khóa try. Trong Java SE 7 và các phiên bản sau đó, lớp BufferedWriter triển khai giao diện AutoCloseable. Vì thế, khi một thể hiện của BufferedWriter được khai báo trong một lệnh try-with-resources, nó sẽ bị đóng bất kể liệu lệnh write() hoàn thành bình thường hay bất thường do một ngoại lệ được ném bởi phương thức write().
Lưu ý: Một lệnh try-with-resources có thể có các khối catch và finally giống như một lệnh try thông thường. Trong một lệnh try-with-resources, bất kỳ khối catch hoặc finally nào cũng được thực thi sau khi các tài nguyên đã được đóng.
Trước khi có Java SE 7, khối finally được sử dụng để đảm bảo rằng tài nguyên sẽ được đóng bất kể liệu lệnh try hoàn thành bình thường hay bất thường.
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
public class FileWriting {
public void writeToFile(String path) throws IOException {
Writer output = null;
try {
output = new BufferedWriter(new FileWriter(path));
output.write("This is a sample statement.");
} finally {
if (output != null) {
output.close();
}
}
}
public static void main(String[] args) {
FileWriting f = new FileWriting();
try {
f.writeToFile("C:\\Misc\\test.txt");
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
}
}
Đoạn mã trong Mẫu mã & mô tả một ví dụ tương tự với ví dụ trong ví dụ mẫu phía trên, sử dụng khối finally thay vì một lệnh try-with-resources để đóng các tài nguyên. Tuy nhiên, trong ví dụ này, nếu cả hai phương thức write() và close() đều ném ra ngoại lệ, thì phương thức writeToFile() sẽ ném ra ngoại lệ được ném từ khối finally; ngoại lệ ném ra từ khối try độ ưu tiên sẽ thấp hơn.
Ngược lại, nếu có ngoại lệ được ném từ cả khối try và khối try-with-resources, thì phương thức writeToFile() sẽ ném ra ngoại lệ được ném từ khối try; ngoại lệ ném ra từ khối try-with-resources độ ưu tiên sẽ thấp hơn. Tuy nhiên, trong Java SE 7 và các phiên bản sau này, người ta có thể lấy cả các ngoại lệ bị đè bẹp.
Java SE 7 và các phiên bản sau này cho phép khai báo một hoặc nhiều tài nguyên trong một lệnh try-with-resources.
Đoạn mã dưới mô tả một ví dụ cho thấy việc khai báo nhiều tài nguyên trong một câu lệnh try-with-resources duy nhất:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class Filecontents {
public void writeToFilecontents(String sourceFile, String targetFile) throws IOException {
// Declaring more than one resource in the try-with-resources statement
try (
BufferedReader objIn = new BufferedReader(new FileReader(sourceFile));
BufferedWriter objOut = new BufferedWriter(new FileWriter(targetFile))
) {
// Code to read from the source and write to the target file.
String line;
while ((line = objIn.readLine()) != null) {
objOut.write(line);
objOut.newLine(); // Add a newline after each line
}
}
}
public static void main(String[] args) {
Filecontents f = new Filecontents();
try {
f.writeToFilecontents("C:\\Misc\\test.txt", "C:\\Misc\\test2.txt");
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
}
}
Trong ví dụ này, câu lệnh try-with-resources chứa hai khai báo được phân tách bằng dấu chấm phẩy: BufferedReader và BufferedWriter. Khi khối mã nguồn ngay sau khai báo kết thúc, bất kể là theo cách thông thường hoặc do một ngoại lệ, các phương thức close() của các đối tượng BufferedWriter và BufferedReader được tự động gọi theo thứ tự này. Điều này có nghĩa là các phương thức close() của các tài nguyên được gọi theo thứ tự ngược với thứ tự tạo chúng.
Các tính năng của xử lý ngoại lệ
Các tính năng khác của việc xử lý ngoại lệ bao gồm các điểm sau:
- Câu lệnh multi-catch có thể giúp các lập trình viên viết mã nguồn hiệu quả và ngắn gọn hơn.
- Các câu lệnh multi-catch cũng cho phép các lập trình viên xử lý một phần của ngoại lệ và sau đó cho nó lan truyền lên bằng cách sử dụng re-throw.
- Câu lệnh try-with-resources giúp làm cho việc dọn dẹp ngoại lệ ít gặp lỗi hơn.
java.lang.IllegalCallerException Class
Trong Java, một số hoạt động cụ thể được coi là tốn nhiều tài nguyên hoặc tốn nhiều nguồn lực vì chúng tiêu thụ một lượng đáng kể tài nguyên, như sử dụng CPU, lưu trữ hoặc bộ nhớ. Để xây dựng ứng dụng có khả năng mở rộng và nhanh chóng, các nhà phát triển nên cố gắng tối ưu hóa tần suất thực hiện những hoạt động đắt đỏ này. Nhận thông tin về dãy ngăn xếp sử dụng phương thức getStackTrace() của Throwable là một hoạt động tốn nhiều tài nguyên và ảnh hưởng đến quá trình thực hiện. Trong phương pháp này, JVM chụp một bản chụp của toàn bộ ngăn xếp, ngay cả khi chỉ có một vài khung cần thiết. Mã sẽ xử lý tất cả các khung, bao gồm cả những khung không cần thiết, điều này làm cho nhiệm vụ này tốn thời gian và có một số hạn chế.
Có thể trở nên khó khăn khi truy cập thực thể thực sự của lớp java.lang.Class đã khai báo phương thức trong một khung ngăn xếp. Để truy cập đối tượng Class này, một nhà phát triển phải mở rộng java.lang.SecurityManager để truy cập phương thức getClassContext() được bảo vệ, phương thức này trả về ngăn xếp thực thi hiện tại dưới dạng một mảng các đối tượng Class.
Để khắc phục những vấn đề này, Stack-Walking API đã được giới thiệu trong Java 9. API này bao gồm lớp java.lang.StackWalker, cùng với lớp nhúng Option và giao diện StackFrame. Stack-Walking API cũng bao gồm lớp Java.lang.IllegalCallerException. Lớp này đại diện cho một ngoại lệ có thể được ném để cho biết rằng một phương thức đã được gọi bởi một người gọi không thích hợp.
Exception người dùng tự định nghĩa
Khi quyết định loại ngoại lệ cần ném, bạn có thể sử dụng các lớp ngoại lệ tích hợp sẵn của nền tảng Java hoặc tạo một lớp ngoại lệ tùy chỉnh. Tạo một lớp ngoại lệ tùy chỉnh là một lựa chọn hợp lý trong các tình huống sau:
- Các loại ngoại lệ tích hợp không đáp ứng được yêu cầu cụ thể của bạn.
- Bạn cần phải phân biệt ngoại lệ của bạn với những ngoại lệ được ném bởi các lớp do các nhà cung cấp khác tạo ra.
- Mã của bạn cần xử lý và ném nhiều ngoại lệ liên quan.
Tạo Exception người dùng tự định nghĩa
Để tạo một lớp ngoại lệ do người dùng xác định, lớp phải kế thừa từ lớp Exception.
Cú pháp:
public class <ExceptionName> extends Exception
Trong đó,
<ExceptionName>: là tên của lớp ngoại lệ do người dùng xác định.
Ví dụ:
Tạo file ServerException
public class ServerException extends Exception {
public ServerException() {
// Call the constructor of the superclass (Exception)
super("Connection Failed");
}
}
Tạo file Example
public class Example {
public static void main(String[] args) {
try {
// Attempt to establish a connection or perform some server-related operation
// If the operation fails, throw the custom ServerException
throw new ServerException();
} catch (ServerException ex) {
System.out.println("Caught ServerException: " + ex.getMessage());
}
}
}
Trong ví dụ, ServerException là một lớp ngoại lệ do người dùng xác định kế thừa từ lớp Exception tích hợp sẵn. Phương thức getMessage() của lớp Exception đã được ghi đè trong lớp ServerException để in ra một thông báo do người dùng định nghĩa, ‘Kết nối thất bại.’ Mã này cơ bản không tạo ra bất kỳ kết quả nào; nó cần được khởi tạo, ví dụ, trong một câu lệnh ném ngoại lệ (throw statement).
Throw Exception người dùng tự định nghĩa
Để gây ra một ngoại lệ do người dùng xác định, một phương thức phải ném ngoại lệ trong quá trình chạy. Ngoại lệ sau đó được truyền lên trong ngăn xếp gọi và có thể được xử lý bởi người gọi của phương thức.
Mã dưới mô tả cách ném một ngoại lệ do người dùng xác định:
Tạo file ServerException:
class ServerException extends Exception { // Define your user-defined exception
public ServerException() {
super("Server Connection Error");
}
}
Tạo file MyConnection:
class MyConnection {
String ip;
String port;
public MyConnection(String ip, String port) {
this.ip = ip;
this.port = port;
}
// Creating a method that throws the user-defined exception
public void connectToServer() throws ServerException {
if (ip.equals("127.10.10.1") && port.equals("1234")) {
System.out.println("Connecting to Server..");
} else {
throw new ServerException(); // Throwing the exception
}
}
}
Tạo file TestConnection:
public class TestConnection {
public static void main(String[] args) {
try {
MyConnection con = new MyConnection("127.10.10.1", "1234");
con.connectToServer();
} catch (ServerException ex) {
System.out.println("Error: " + ex.getMessage());
}
}
}
Trong mã nguồn, lớp MyConnection bao gồm một hàm khởi tạo chấp nhận địa chỉ IP và số cổng của máy chủ và khởi tạo các biến thể tương ứng. Phương thức connectToServer() khai báo rằng nó có thể ném một ServerException bằng cách sử dụng mệnh đề throws ở dòng 1. Bên trong phương thức, các giá trị của biến ip và port được kiểm tra. Nếu các giá trị này không khớp với các giá trị được xác định trước, phương thức sẽ ném một ServerException ở dòng 2.
Bây giờ, trong lớp TestConnection, một đối tượng của MyConnection được tạo với các giá trị không đúng cho port trong khối try. Tiếp theo, phương thức connectToServer() được gọi. Ngoài ra, một khối catch được bao gồm để xử lý ngoại lệ do người dùng định nghĩa, ServerException. Câu lệnh ex.getMessage() được sử dụng để gọi phương thức getMessage() đã ghi đè của lớp ServerException để in ra thông báo lỗi do người dùng định nghĩa.
Output:
Kết quả hiển thị thông báo lỗi do người dùng lấy từ phương thức getMessage() của lớp ServerException. Điều này xảy ra vì khi phương thức connectToServer() được thực thi trong phương thức main(), giá trị IP và port không khớp với các giá trị đã xác định trước. Do đó, phương thức ném ra ngoại lệ ServerException. Ngoại lệ này được bắt trong khối catch của phương thức main(), trong đó gọi phương thức getMessage() của đối tượng ServerException. Vì vậy, có thể tạo một lớp ngoại lệ tùy chỉnh và xử lý ngoại lệ này khi các lớp ngoại lệ tích hợp không đáp ứng được mục đích cần thiết.
Wrapper Exceptions
Để ẩn loại ngoại lệ đang được tạo ra mà không loại bỏ ngoại lệ hoàn toàn, người ta có thể sử dụng ngoại lệ bọc (wrapper exceptions). Cách tiếp cận này dẫn đến thay thế triển khai (implementation substitution). Nói cách khác, một ngoại lệ bọc được sử dụng để duy trì trừu tượng hóa và ngăn các ngoại lệ bị loại bỏ mà không thông báo rõ ràng. Quá trình bọc ngoại lệ bao gồm việc bắt ngoại lệ, bọc nó bằng một ngoại lệ khác, và sau đó ném ngoại lệ bọc.
Việc bọc ngoại lệ là một tính năng tiêu chuẩn trong Java kể từ JDK 1.4. Hầu hết các ngoại lệ tích hợp trong Java đều có các hàm tạo chấp nhận tham số ‘nguyên nhân’ (’cause’). Chúng cũng cung cấp một phương thức getCause() để trả về ngoại lệ bọc.
Lý do chính để sử dụng việc bọc ngoại lệ là để bảo vệ mã nguồn phía trên trong ngăn xếp gọi khỏi việc phải xử lý tất cả các loại ngoại lệ có thể xảy ra trong hệ thống. Điều này xảy ra vì ngoại lệ được khai báo có xu hướng tích tụ ở phía trên của ngăn xếp gọi. Nếu ngoại lệ không được bọc mà được truyền bằng cách khai báo chúng trong các phương thức, có thể dẫn đến việc các phương thức cấp cao khai báo nhiều loại ngoại lệ khác nhau. Việc khai báo tất cả những ngoại lệ này trong mỗi phương thức ở trên ngăn xếp gọi trở nên phiền toái.
Hơn nữa, bạn có thể không muốn các thành phần cấp cao biết về các thành phần cấp thấp hoặc về các loại ngoại lệ mà chúng gây ra. Ví dụ, giao diện và triển khai Data Access Object (DAO) nhằm ẩn đi chi tiết của việc truy cập dữ liệu khỏi phần còn lại của ứng dụng. Nếu các phương thức DAO gây ra SQLException, thì mã sử dụng DAO phải bắt ngoại lệ này. Nếu triển khai thay đổi sao cho nó lấy dữ liệu từ một dịch vụ web thay vì từ cơ sở dữ liệu, các phương thức DAO sẽ phải ném cả RemoteException và SQLException. Hơn nữa, nếu DAO được chỉnh sửa để đọc dữ liệu từ một tệp, nó sẽ phải ném IOException nữa. Trong trường hợp này, ba loại ngoại lệ khác nhau sẽ được sử dụng, mỗi loại liên quan đến triển khai DAO cụ thể.
Lưu ý: Mô hình DAO sử dụng trừu tượng hóa (qua giao diện) để cho phép thay thế triển khai.
Để giải quyết vấn đề này, các phương thức giao diện DAO có thể ném một ngoại lệ duy nhất là DAOException. Trong mỗi triển khai của giao diện DAO, bất kỳ đó là cho cơ sở dữ liệu, tệp, hoặc dịch vụ web, người dùng có thể bắt các ngoại lệ cụ thể (SQLException, IOException, hoặc RemoteException), bọc chúng trong DAOException, và ném DAOException. Kết quả là, mã sử dụng giao diện DAO chỉ cần xử lý DAOException. Nó không cần biết về công nghệ truy cập dữ liệu cụ thể được sử dụng trong các triển khai khác nhau
Ví dụ:
Tạo lớp CalculatorException:
class CalculatorException extends Exception {
public CalculatorException() {
super();
}
public CalculatorException(Throwable cause) {
super(cause);
}
public CalculatorException(String message, Throwable cause) {
super(message, cause);
}
}
Tạo lớp Calculator:
class Calculator {
public void divide(int a, int b) throws CalculatorException {
try {
if (b == 0) {
throw new ArithmeticException("Denominator cannot be zero");
}
int result = a / b; // Performing division
System.out.println("Result is " + result);
} catch (ArithmeticException ex) {
throw new CalculatorException("An error occurred during division", ex);
}
}
}
Tạo lớp Calculator:
public class TestCalculator {
public static void main(String[] args) {
Calculator objCalc = new Calculator();
try {
objCalc.divide(10, 0);
} catch (CalculatorException ex) {
Throwable t = ex.getCause();
System.out.println("Error: " + ex.getMessage());
System.out.println("Cause: " + t);
}
}
}
Trong mã nguồn, một lớp ngoại lệ do người dùng xác định có tên là CalculatorException được tạo ra. Lớp này bao gồm hai hàm tạo nạp chồng, một nhận đối tượng Throwable và một nhận một chuỗi thông báo làm tham số. Hơn nữa, ở dòng 2, một lớp khác có tên là Calculator được tạo ra. Lớp này chứa một phương thức divide() có thể ném ngoại lệ CalculatorException.
Bên trong phương thức divide(), có một khối try-catch dùng để bắt ngoại lệ thực sự, cụ thể là ArithmeticException khi ‘b’ được thiết lập thành 0. Đối tượng ArithmeticException sau đó được bọc với một thông báo tùy chỉnh và ném lại dưới dạng đối tượng CalculatorException.
Tuy nhiên, trong khối catch, đối tượng ArithmeticException được bọc với một thông báo tùy chỉnh và sau đó ném lại dưới dạng đối tượng CalculatorException.
Tiếp theo, lớp TestCalculator được định nghĩa với phương thức main(). Bên trong phương thức main(), phương thức divide() được gọi trong một khối try. Trong khối catch, CalculatorException được xử lý. Tuy nhiên, nguyên nhân thực sự của CalculatorException là một ArithmeticException. Để lấy nguyên nhân, phương thức getCause() được sử dụng ở dòng 5 để lấy ra nguyên nhân trong một đối tượng Throwable.
Cuối cùng, cả thông báo và nguyên nhân được in ra ở các dòng 6 và 7.
Output:
Kết quả hiển thị cả chuỗi thông báo tùy chỉnh cũng như nguyên nhân thực sự của ngoại lệ, tức là ArithmeticException. Tương tự, nếu phương thức divide() có khả năng ném bất kỳ ngoại lệ nào khác, bạn có thể bắt nó trong khối catch thích hợp bên trong phương thức và sau đó ném lại sau khi bọc nó trong đối tượng CalculatorException. Do đó, có thể ném nhiều ngoại lệ từ một phương thức bằng cách đặt chúng trong một ngoại lệ bọc duy nhất và xử lý chúng trong một khối xử lý ngoại lệ thống nhất ở phía trên của callstack.
Assertsions
Khẳng định (assertion) là một câu lệnh trong Java cho phép người lập trình kiểm tra giả định của họ về chương trình. Ví dụ, nếu có một phương thức được viết để tính tốc độ của một hạt, người lập trình có thể khẳng định rằng tốc độ tính toán phải nhỏ hơn tốc độ ánh sáng.
Mỗi câu lệnh khẳng định bao gồm một biểu thức logic kiểu boolean được kỳ vọng sẽ đúng khi câu lệnh khẳng định được thực thi. Nếu biểu thức không đúng, hệ thống sẽ ném ra một lỗi. Bằng cách xác nhận rằng biểu thức logic thực sự là đúng, câu lệnh khẳng định xác nhận các giả định về hành vi của chương trình. Điều này giúp tăng sự tự tin của người lập trình rằng mã nguồn không có lỗi.
Việc viết các câu lệnh khẳng định trong quá trình lập trình là một trong những cách hiệu quả và nhanh chóng nhất để phát hiện và sửa lỗi trong mã nguồn. Ngoài ra, các câu lệnh khẳng định cũng giúp tài liệu cách hoạt động bên trong của chương trình, từ đó, tăng cường khả năng bảo trì. Cú pháp của câu lệnh khẳng định có hai dạng như sau:
Cú pháp
assert <boolean_expression>;
Trong đó,
<boolean_expression>: là biểu thức kiểu boolean. Khi hệ thống thực thi câu lệnh khẳng định, nó kiểm tra biểu thức kiểu boolean. Nếu biểu thức này sai, hệ thống sẽ ném ra một AssertionError mà không có thông điệp chi tiết.
assert <boolean_expression> : <detail_expression>;
Trong đó:
<boolean_expression>: là biểu thức kiểu boolean.
<detail_expression>: là một biểu thức có giá trị. Không thể là một lời gọi phương thức có kiểu void. Dạng này của câu lệnh khẳng định được sử dụng để cung cấp một thông điệp chi tiết cho AssertionError. Hệ thống truyền giá trị của <boolean_expression> tới hàm tạo AssertionError tương ứng. Hàm tạo này sử dụng biểu diễn chuỗi của giá trị làm thông điệp chi tiết của lỗi.
Mục đích của việc sử dụng thông điệp chi tiết là để lấy và truyền thông tin về việc câu lệnh khẳng định bị thất bại. Thông điệp này nên giúp người lập trình chẩn đoán và sửa lỗi dẫn đến sự thất bại của khẳng định. Lưu ý rằng thông điệp chi tiết không dành cho hiển thị lỗi cho người dùng cuối. Do đó, thường không cần phải làm cho thông điệp này có thể hiểu được độc lập hoặc có khả năng quốc tế hóa. Thông điệp chi tiết được thiết kế để được giải thích cùng với một đám ngăn xếp đầy đủ cùng với mã nguồn chứa câu lệnh khẳng định bị lỗi.
Tương tự như tất cả các ngoại lệ chưa được bắt, lỗi khẳng định thường được đánh dấu trong dãy ngăn xếp với tên tệp và số dòng từ nơi chúng được kích hoạt. Dạng thứ hai của câu lệnh khẳng định nên được sử dụng thay vì dạng thứ nhất chỉ khi chương trình chứa thông tin bổ sung có thể giúp chẩn đoán sự cố. Ví dụ, nếu boolean_expression liên quan đến mối quan hệ giữa hai biến, ‘a’ và ‘b,’ thì nên sử dụng dạng thứ hai. Trong trường hợp như vậy, detail_expression có thể được thiết lập thành “a:” + a + “, b=” + b.’
Trong một số trường hợp, việc đánh giá boolean_expression có thể tốn nhiều tài nguyên. Ví dụ, nếu có một phương thức được viết để tìm số nhỏ nhất trong một danh sách số chưa được sắp xếp, một lệnh khẳng định được thêm vào để xác minh rằng phần tử được chọn thực sự là số nhỏ nhất. Trong trường hợp này, công việc được thực hiện bởi lệnh khẳng định có thể tốn nhiều tài nguyên tương tự công việc của phương thức.
Để đảm bảo rằng lệnh khẳng định không ảnh hưởng đến hiệu suất trong các ứng dụng triển khai, chúng có thể được bật hoặc tắt khi chương trình được khởi chạy. Mặc định, lệnh khẳng định được tắt. Việc tắt lệnh khẳng định giải quyết hoàn toàn các vấn đề liên quan đến hiệu suất. Một khi đã tắt, lệnh khẳng định thực tế trở thành các câu lệnh rỗng trong ngữ nghĩa mã nguồn.
Do đó, lệnh khẳng định có thể được thêm vào mã nguồn để đảm bảo ứng dụng hoạt động như dự kiến. Bằng cách sử dụng lệnh khẳng định, người lập trình có thể kiểm tra việc thất bại của nhiều điều kiện. Nếu một điều kiện thất bại, ứng dụng có thể bị kết thúc và thông tin gỡ lỗi có thể được hiển thị. Tuy nhiên, lệnh khẳng định không nên được sử dụng khi kiểm tra phải luôn được thực hiện vào thời gian chạy, vì lệnh khẳng định có thể bị tắt. Trong trường hợp như vậy, nên dựa vào logic chương trình thay vì lệnh khẳng định.
Kiểm tra lệnh khẳng định được tắt mặc định. Lệnh khẳng định có thể được bật bằng dòng lệnh sau:
java -ea <class-name>
Hoặc
java -enableassertions <class-name>
Để bật tính năng này trên netbean, thao tác theo các bước sau:
- Nhấp chuột phải vào dự án trong tab Project. Sẽ hiển thị một menu bật lên.
- Chọn “Properties.” Hộp thoại Project Properties sẽ hiển thị.
- Chọn “Run” từ bảng Category. Bảng thiết run time sẽ hiển thị bên phải, như trong Hình:
Mô tả thao tác bằng hình ảnh:
Người ta có thể sử dụng các khẳng định để tài liệu và xác minh các giả định như sau:
Logic Nội bộ của Một Phương thức Đơn Lẻ
- Biến nội bộ bảo toàn
- Luồng điều khiển bảo toàn
- Tiền điều kiện và Sau điều kiện
Biến Bảo toàn Lớp
Bất biến nội bộ bảo toàn/bất biến nội bộ (Internal Invariants)
Trước đây, khi chưa có sẵn các lệnh khẳng định, nhiều người lập trình đã sử dụng các ghi chú để biểu đạt các giả định về hành vi của một chương trình. Ví dụ, một người có thể đã viết một ghi chú như được hiển thị trong đoạn mã dưới để làm rõ một giả định về một mệnh đề ‘else’ trong câu lệnh ‘if…else’.
public class IfElseTest {
public static void main(String[] args) {
int a = 0;
if (a > 0) {
// Do this if 'a' is greater than zero
System.out.println("a is greater than zero");
} else {
// Do that unless 'a' is negative
System.out.println("a is not greater than zero");
}
}
}
Đoạn mã cho biết rằng mệnh đề ‘else’ chỉ nên được thực thi nếu ‘a’ bằng không, nhưng không nếu ‘a’ lớn hơn 0. Tuy nhiên, trong quá trình chạy, điều kiện này có thể bị bỏ qua vì không có lỗi nào được thông báo ngay cả khi cung cấp một số âm trong quá trình chạy. Để giải quyết những biến bảo toàn như vậy, người ta có thể sử dụng các khẳng định, như được thể hiện trong đoạn mã dưới:
package com.mycompany.testassertion;
/**
*
* @author toan1
*/
public class TestAssertion {
public static void main(String[] args) {
int a = -1; // You need to specify a value for 'a' here
if (a > 0) {
System.out.println("Greater than zero");
} else {
assert a == 0: "Number should not be negative";
System.out.println("Number is zero");
}
}
}
Output:
Các Biến Bảo Toàn Luồng Điều Khiển (Control Flow Invariants)
Khẳng định cũng có thể được sử dụng cho các biến bảo toàn luồng điều khiển, ví dụ như một câu lệnh switch mà thiếu trường hợp mặc định. Sự thiếu trường hợp mặc định cho thấy sự tin tưởng rằng một trong các trường hợp sẽ luôn được thực hiện. Giả định rằng một biến cụ thể sẽ chắc chắn có một trong một tập hợp nhỏ các giá trị là một biến bảo toàn cần phải được kiểm tra bằng một lệnh khẳng định.
Ví dụ:
public class SwitchWithAssertions {
public static void main(String[] args) {
int dayOfWeek = 9;
switch (dayOfWeek) {
case 1:
System.out.println("Monday");
break;
case 2:
System.out.println("Tuesday");
break;
case 3:
System.out.println("Wednesday");
break;
case 4:
System.out.println("Thursday");
break;
case 5:
System.out.println("Friday");
break;
case 6:
System.out.println("Saturday");
break;
case 7:
System.out.println("Sunday");
break;
default:
System.out.println("Invalid day of the week");
assert false : "Invalid dayOfWeek value in the default case";
}
}
}
Output:
Tiền Điều Kiện, Sau Điều Kiện và Biến Bảo Toàn Lớp (PreCondition, PostCondition, Class Invariants)
Mặc dù cấu trúc ‘assert’ không phải là một giải pháp hoàn chỉnh mà chỉ có thể hỗ trợ trong việc triển khai một phong cách lập trình không chính thức dựa trên hợp đồng.
Người ta có thể sử dụng các khẳng định cho các mục đích sau:
Điều Kiện trước: Điều gì phải đúng khi một phương thức được gọi?
Điều Kiện sau: Điều gì phải đúng sau khi một phương thức thực hiện thành công?
Biến Bảo Toàn Lớp: Điều gì phải đúng với mỗi trường hợp của một lớp?
PreCondition
Theo quy ước, các tiền điều kiện cho các phương thức public được thực thi bằng cách kiểm tra điều kiện một cách rõ ràng và ném các ngoại lệ cụ thể khi cần. Ví dụ, hãy xem xét đoạn mã dưới:
public void setRate(int rate) {
final int MAX_RATE = 60; // Adjust the maximum rate as needed
if (rate <= 0 || rate > MAX_RATE) {
throw new IllegalArgumentException("Illegal rate: " + rate);
}
setInterval(1000 / rate);
}
Trong đoạn mã, phương thức ‘setRate()‘ chấp nhận biến rate làm tham số và kiểm tra xem giá trị có nhỏ hơn không hoặc lớn hơn ‘MAX_RATE‘ không. Nếu điều kiện không được đáp ứng, nó sẽ ném một ‘IllegalArgumentException,’ và phần mã tiếp theo sẽ không được thực thi.
PostCondition
Những điều kiện sau thực hiện có thể được kiểm tra bằng các lệnh khẳng định trong cả các phương thức public và non-public. Ví dụ, phương thức công khai ‘pop()‘ trong đoạn mã dưới sử dụng một lệnh ‘assert‘ để xác minh một điều kiện sau
import java.util.ArrayList;
public class Postconditiontest {
private ArrayList<String> values = new ArrayList<>();
public Postconditiontest() {
values.add("one");
values.add("two");
values.add("three");
values.add("four");
}
public Object pop() {
int size = values.size(); // Get the current size
if (size == 0) {
throw new RuntimeException("List is empty!");
}
Object result = values.remove(0); // Remove the first element
// Verify the postcondition
assert values.size() == size - 1 : "Postcondition failed";
return result;
}
public static void main(String[] args) {
Postconditiontest test = new Postconditiontest();
Object poppedValue = test.pop();
System.out.println("Popped value: " + poppedValue);
}
}
Trong đoạn mã, một phương thức ‘pop()‘ được sử dụng để loại bỏ các phần tử khỏi một danh sách. Trước tiên, kích thước của danh sách được xác định ở dòng 1 bằng cách sử dụng phương thức ‘size()‘. Sau đó, nếu kích thước bằng không, một ‘RuntimeException‘ sẽ được ném. Tiếp theo, mã để loại bỏ phần tử được cung cấp. Bây giờ, trước khi loại bỏ phần tử tiếp theo, một kiểm tra được thực hiện bằng cách sử dụng một lệnh khẳng định ở dòng 2. Ở đây, nếu phương thức ‘size()‘ không trả về một giá trị bằng ‘size – 1’, lệnh khẳng định sẽ thất bại và ném một ‘AssertionError‘.
Class InVariants
Biến bảo toàn lớp là một loại biến bảo toàn nội bộ được áp dụng cho mọi trường hợp của một lớp. Nó có thể áp dụng bất kỳ lúc nào, ngoại trừ khi trường hợp đang chuyển từ một trạng thái thống nhất sang trạng thái khác. Biến bảo toàn lớp có thể được sử dụng để xác định các mối quan hệ giữa nhiều thuộc tính. Hơn nữa, nó nên luôn đúng trước và sau khi một phương thức hoàn thành. Ví dụ, trong trường hợp của một chương trình thực hiện cấu trúc dữ liệu cây cân bằng, biến bảo toàn lớp có thể chỉ định rằng cây cân bằng và được sắp xếp đúng cách.
Cơ chế khẳng định không quy định một kiểu cụ thể cho việc kiểm tra biến bảo toàn. Tuy nhiên, đôi khi việc kết hợp các biểu thức kiểm tra ràng buộc cần thiết vào một phương thức nội bộ duy nhất mà có thể được gọi bởi các lệnh khẳng định có thể tiện lợi và khuyến cáo. Trong bối cảnh của ví dụ cây cân bằng, nó sẽ phù hợp hơn để tạo một phương thức riêng tư chịu trách nhiệm xác nhận rằng cây thực sự cân bằng theo quy tắc của cấu trúc dữ liệu. Phương thức riêng tư này có thể trông như sau:
private boolean balanced(){
//todo
}
Vì phương thức này được sử dụng để xác minh một ràng buộc mà nên đúng trước và sau khi một phương thức hoàn thành, mỗi phương thức công khai và hàm khởi tạo nên chứa dòng ‘assert balanced();‘ ngay trước câu lệnh return.
Thường thì không cần phải đặt các kiểm tra tương tự ở đầu của mỗi phương thức công khai trừ khi cấu trúc dữ liệu được triển khai bằng các phương thức nguyên thuỷ. Sự thất bại của lệnh kiểm tra ở đầu của một phương thức như vậy sẽ chỉ ra rằng có khả năng xảy ra lỗi làm mất dữ liệu. Tương tự, nên khuyến cáo bao gồm kiểm tra biến bảo toàn lớp ở đầu của các phương thức trong các lớp mà trạng thái của chúng có thể bị thay đổi bởi các lớp khác.
Sử Dụng Khẳng Định Một Cách Không Thích Hợp
Khẳng định có thể bị vô hiệu hóa khi chạy. Do đó,
Không sử dụng khẳng định để kiểm tra các tham số của một phương thức công khai.
Điều này bởi vì việc kiểm tra tham số thường là một phần của các quy định hoặc hợp đồng được công bố của một phương thức. Các quy định này phải được tuân theo, cho dù khẳng định được bật hoặc tắt. Vấn đề khác khi sử dụng khẳng định để kiểm tra tham số là theo tiêu chuẩn, tham số không đúng sẽ dẫn đến một ngoại lệ chạy thời gian thích hợp như ‘IllegalArgumentException,’ ‘NullPointerException,’ hoặc ‘IndexOutOfBoundsException.’ Một lỗi khẳng định sẽ không ném ra ngoại lệ thích hợp.
Không sử dụng các phương thức có thể gây ra tác động phụ trong kiểm tra khẳng định.
Không sử dụng khẳng định để thực hiện các nhiệm vụ mà ứng dụng phụ thuộc vào để hoạt động đúng.
Điều này bởi vì khẳng định có thể bị vô hiệu hóa. Do đó, chương trình không nên giả định rằng biểu thức Boolean chứa trong một khẳng định sẽ được đánh giá, đặc biệt nếu vi phạm quy tắc này có thể dẫn đến hậu quả. Ví dụ, giả sử một đoạn mã yêu cầu loại bỏ tất cả các phần tử null khỏi danh sách ‘cities,’ và bạn biết rằng danh sách này chứa một hoặc nhiều phần tử null. Trong trường hợp này, việc thực hiện sau sẽ là không đúng:
// mã bị lỗi - vì hành động được chứa trong một khẳng định
assert cities.remove(null);
Chương trình sẽ hoạt động tốt khi khẳng định được bật nhưng sẽ thất bại khi chúng bị tắt, vì nó sẽ không loại bỏ các phần tử null khỏi danh sách.
Cách tiếp cận đúng là thực hiện hành động trước khi khẳng định và sau đó khẳng định rằng hành động đã thành công, như sau:
// Sửa mã - hành động đứng trước khẳng định
Boolean removedNull = cities.remove(null);
assert removedNull;
Như một quy tắc, các biểu thức viết trong các khẳng định nên không gây tác động phụ. Điều này có nghĩa là việc đánh giá biểu thức không nên ảnh hưởng đến trạng thái của chương trình mà có thể nhìn thấy sau khi đánh giá hoàn thành. Một ngoại lệ duy nhất cho quy tắc này là khẳng định có thể sửa đổi trạng thái chỉ được sử dụng trong các khẳng định khác.