+1

Xử lý ngoại lệ trong Java

Lập trình viên, đặc biệt là lập trình viên .NET và Java thì không ai không biết đến Exceptions (ngoại lệ), và "bắt" Exceptions thế nào. Nhưng không phải ai cũng biết JVM xử lý Exceptions ra sao, lợi ích của Exceptions và tác hại của việc xử dụng try-catch để bắt Exceptions.

Exceptions là một sự kiện xảy ra khi một chương trình đang chạy (thực thi), sự kiện đó làm cho luồng xử lý thông thường của chương trình không thể thực hiện một cách bình thường, thậm chí chết chương trình.

Trong Java có 3 loại exception là Error, checked exception, và unchecked exception (runtime exception).

  • Error: là những lỗi nghiêm trọng xảy ra đối khi chương trình hoạt động mà lập trình viên không thể kiểm soát. Ví dụ như lỗi phần cứng, tràn bộ nhớ, hay lỗi của JVM.
  • Checked exceptions: lã những exception ta phải xử lý ngai khi viết code, vì nó được kiểm tra bởi trình biên dịch Javac. Ví dụ: ClassNotFoundException, NoSuchFieldException...
  • Unchecked exceptions: là những excepton chỉ xảy ra khi chương trình chạy, nghĩa là trình biên dịch Javac không "phát hiện" ra khi biên dịch, do vậy programmer không thể xử lý khi viết code. Ví dụ: NullPointerException, ArrayIndexOutOfBoundsException, DivideByZeroException...

1. JVM xử lý Exceptions thế nào

Khi một method xảy ra ngoại lệ, trước tiên JVM tìm xử lý ngoại lệ phù hợp được xử dụng tại method ấy. Nếu không có thì JVM tiếp tục tìm xử lý ngoại lệ phù hợp ở các method trên (là method gọi lớp hiện tại). Nếu không có method nào có xử lý ngoại lệ phù hợp thì Thread mà đang thực hiện chuỗi method xảy ra ngoại lệ bị ngắt. Nếu thread ấy là thread main thì chết chương trình.

2. Lợi ích của Exceptions

2.1 Giúp tổ chức source code trong việc phát hiện, xử lý lỗi một cách hiệu quả

Xét method readFile có các chức năng như sau.

readFile {
    open the file;
    determine its size;
    allocate that much memory;
    read the file into memory;
    close the file;
}

Trên là method đọc và xử lý file một cách đơn giản, nhưng nó đã bỏ qua các lỗi tiềm ẩn như: lỗi khi mở file, lỗi xác định kích thước file, lỗi tràn bộ nhớ, lỗi không đọc được file, lỗi không thể đóng file. Nếu bắt và xử lý lỗi theo cách "truyền thống" thì method readFile được tổ chức như sau.

errorCodeType readFile {
    initialize errorCode = 0;
    open the file;
    if (theFileIsOpen) {
        determine the length of the file;
        if (gotTheFileLength) {
            allocate that much memory;
            if (gotEnoughMemory) {
                read the file into memory;
                if (readFailed) {
                    errorCode = -1;
                }
            } else {
                errorCode = -2;
            }
        } else {
            errorCode = -3;
        }
        close the file;
        if (theFileDidntClose && errorCode == 0) {
            errorCode = -4;
        } else {
            errorCode = errorCode and -4;
        }
    } else {
        errorCode = -5;
    }
    return errorCode;
}

Ta nhận thấy ngay rằng method trên được viết rất dài, rối ren vì các cặp điều kiện if-else.

Exceptions cho phép ta viết code xử lý luồng logic chính một cách liền mạch, khiến code trở nên đơn giản, dễ đọc hơn.

readFile {
    try {
        open the file;
        determine its size;
        allocate that much memory;
        read the file into memory;
        close the file;
    } catch (fileOpenFailed) {
       doSomething;
    } catch (sizeDeterminationFailed) {
        doSomething;
    } catch (memoryAllocationFailed) {
        doSomething;
    } catch (readFailed) {
        doSomething;
    } catch (fileCloseFailed) {
        doSomething;
    }
}

Một lưu ý nhỏ là cả hai cách viết code trên giống nhau về performance.

2.2 Giúp việc tùy chọn "nơi" xử lý lỗi một cách linh hoạt trong việc gọi các method lồng nhau

Giả xử method readFile được gọi từ chương trình chính thông qua các method như sau.

method1 {
    call method2;
}

method2 {
    call method3;
}

method3 {
    call readFile;
}

Nếu các method method2, method3 không quan tâm đến việc xử lý lỗi khi method readFile "bắn" ra mà ta muốn xử lý lỗi ở method method1 thì method2, method3 chỉ việc "forward" ngoại lệ cho các method phía trên như sau.

method1 {
    try {
        call method2;
    } catch (exception e) {
        doErrorProcessing;
    }
}

method2 throws exception {
    call method3;
}

method3 throws exception {
    call readFile;
}

2.3 Giúp gộp nhóm hoặc tạo ngoại lệ mới theo mục đích của programmer

Exception trong Java cũng là Object nghĩa là có class type của exception. Và mọi exception đều được extands từ class gốc là Excetpion.class

java.lang.Object
    java.lang.Throwable
        java.lang.Exception
            java.io.IOException
            java.rmi.NotBoundException

Vẫn là ví dụ về đọc file trong Java. Ta phải bắt 2 checked exception là FileNotFoundException (lỗi không tìm thấy file) và IOException (lỗi không đọc được file). Thông thường code được tổ chức như sau.

readFile() {
    BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader("file.txt"));
            StringBuilder sb = new StringBuilder();
            String line = br.readLine();

            while (line != null) {
                sb.append(line);
                sb.append("\n");
                line = br.readLine();
            }
            String everything = sb.toString();
            System.out.println(everything);
        } catch (FileNotFoundException ex) {
           Logger.getLogger(VunvMonthlyReport.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(VunvMonthlyReport.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            try {
                if(br != null) {
                    br.close();
                }
            } catch (IOException ex) {
                Logger.getLogger(VunvMonthlyReport.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

FileNotFoundException là lớp con của IOException nên ta có thể gộp việc bắt 2 ngoại lệ trên làm 1 như sau.

readFile() {
    BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader("file.txt"));
            StringBuilder sb = new StringBuilder();
            String line = br.readLine();

            while (line != null) {
                sb.append(line);
                sb.append("\n");
                line = br.readLine();
            }
            String everything = sb.toString();
            System.out.println(everything);
        } catch (IOException ex) {
            Logger.getLogger(VunvMonthlyReport.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            try {
                if(br != null) {
                    br.close();
                }
            } catch (IOException ex) {
                Logger.getLogger(VunvMonthlyReport.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

Hoặc ta có thể không xử lý ngoại lệ tại method readFile mà "bắn" ra ngoại lệ mới để method gọi nó xử lý.

readFile() throws LoiXuLyFile {
    BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader("file.txt"));
            StringBuilder sb = new StringBuilder();
            String line = br.readLine();

            while (line != null) {
                sb.append(line);
                sb.append("\n");
                line = br.readLine();
            }
            String everything = sb.toString();
            System.out.println(everything);
        } catch (Exception ex) {
            throw new LoiXuLyFile(ex);
        } finally {
            try {
                if(br != null) {
                    br.close();
                }
            } catch (IOException ex) {
               throw new LoiXuLyFile(ex);
            }
        }

Trong ví dụ trên tất nhiên class LoiXuLyFile được extends từ class Exception.class.

3. Bất lợi của việc sử dụng try-catch để bắt exceptions

Cái bất lợi ở đây ám chỉ sử dụng try-catch để bắt unchecked exceptions, vì các checked exceptions thì bắt buộc ta phải sử dụng. Tôi không tìm thấy bài viết, tài liệu nào nói về disadvantages của việc sử dụng try-catch block. Trong công việc thực tế tôi thấy có điểm bất lợi đó là khi ta xử dụng try-catch sai mục đích, đó là.

  • Khi xử dụng try-catch thì JVM sẽ tạo Exception table để lưu trữ thông tin cho try-catch ấy. Nếu try block bao bọc càng nhiều code thì càng tốn bộ nhớ.
  • Khi ta sử dụng try-catch block để bao đoạn code mà ta cho là có thể xảy ra Runtime exception. Việc đó đảm bảo chương trình không bị "chết" khi có ngoại lệ xảy ra, nhưng đôi khi vô tình ta đưa dữ liệu không mong muốn vào trong database.

Tài liệu tham khảo


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.