Khắc phục lỗi trôi lệnh nhập trong java năm 2024
Ngoại lệ là sự kiện không tránh khỏi, xuất hiện thường xuyên trong cuộc sống hàng ngày. Lấy ví dụ, khi nấu ăn, việc ta kiểm tra và điều chỉnh nhiệt độ, thời gian và nguyên liệu có thể được xem như một dạng luồng công việc (flow), tương tự như luồng công việc mà chúng ta định nghĩa trong lập trình. Show
Khi mọi thứ diễn ra như dự kiến, chúng ta có thể hoàn thành món ăn một cách trôi chảy, theo đúng trình tự và hướng dẫn của công thức. Tuy nhiên, có những lúc chúng ta phải đối mặt với những tình huống không lường trước được, ví dụ như thiếu nguyên liệu, gia vị được thêm quá mức, hoặc một số dụng cụ bị hỏng. Tùy vào từng tình huống cụ thể, chúng ta cần tìm ra các giải pháp riêng biệt để xử lý từng ngoại lệ. Trong lập trình, cũng tương tự như vậy. Không phải lúc nào, mã lệnh của chúng ta cũng diễn ra suôn sẻ như kì vọng. Có vô số lý do có thể làm cho chương trình phải dừng lại và xử lý ngoại lệ. Việc xử lý ngoại lệ là cực kỳ quan trọng, giúp đảm bảo rằng mã lệnh có thể tiếp tục thực thi, hoặc ít ra, cung cấp thông báo lỗi rõ ràng khi đối mặt với ngoại lệ không mong muốn. Tóm lại, việc hiểu và xử lý ngoại lệ không chỉ là một phần quan trọng của lập trình mà còn là một kỹ năng cần thiết trong cuộc sống, giúp chúng ta nhanh chóng phản ứng và thích ứng với mọi tình huống không lường trước được. Chúng ta cùng tìm hiểu thêm về ngoại lệ trong lập trình nói chung và trong Java nói riêng, cũng như cách xử lý ngoại lệ trong bài viết này. 1. Exception là gì?Trong lập trình nói chung, exception - hay ngoại lệ - là một sự kiện có thể xảy ra trong quá trình thực thi của chương trình, và nó có thể làm gián đoạn luồng xử lý thông thường của chương trình đó. Khi những exception này xuất hiện trong lúc thực thi trường trình, chúng ta cần có một cơ chế để xử lý các ngoại lệ này, tránh việc các exception này làm gián đoạn, tổn hại hay chết chương trình. Quá trình trên gọi là exception handling (xử lý ngoại lệ) Exception có thể phát sinh do nhiều lý do khác nhau, chẳng hạn như truy cập vùng nhớ không hợp lệ (NullPointerException) hay chia cho số không (ArithmeticException). Có rất nhiều những lý do khác nhau khiến mã nguồn của chúng ta gặp exception. Điều này khiến cho việc exception handling (xử lý ngoại lệ) trở nên vô cùng cần thiết. 2. Exception trong Java là gì?Trong Java, Throwable là lớp cơ sở dành cho tất cả các đối tượng mà có thể được ném (throw) hoặc bắt (catch) bởi câu lệnh try-catch. Throwable có hai lớp con trực tiếp gồm Exception và Error. Trong bài viết này, chúng ta sẽ làm rõ cả hai khái niệm trên. 2.1. Định nghĩa Exception và Error trong JavaTrong Java, Exception là một class rất đặc biệt được dùng để đại diện cho các ngoại lệ trong quá trình thực thi chương trình. Khi một class Exception được phát hiện, chúng ta cần phải xử lý ngoại lệ đó trong trương trình. Bên cạnh đó, Java còn cùng cấp 1 lớp nữa tên là Error, đại diện cho một lỗi rất nghiêm trọng mà chương trình không nên bắt trong quá trình thực thi. Trong bài này cũng sẽ đề cập đến khái niệm error. Cả error và exception đều kế thừa từ lớp throwable, cho phép chúng ta thực thi ném (throw) và bắt (catch) bởi câu lệnh try/catch. Nói cách khác, khi chúng ta phát hiện được Exception, chương trình sẽ
6
7, và nhiệm vụ của chúng ta cần làm là
8 đó, và xử lý chúng. Cuối cùng, ngay cả khi có Exception xảy ra hay không, chúng ta có thể vẫn phải xử lý một số logic để hoàn tất việc xử lý chương trình. Đối với Error, mặc dù có thể throw/catch, nhưng chương trình không nên xử lý với Error theo cách mà chương trình xử lý với Exception. Chúng ta sẽ nói rõ hơn ở phần sau. 2.2. Lý do Exception và Error xuất hiện trong chương trìnhException và Error đều biểu diễn một vấn đề mà chương trình có thể gặp phải. Tuy nhiên, mục đích và bối cảnh sử dụng của cả 2 có chút khác biệt. Exception là những sự cố mà lập trình viên có thể dự đoán và xử lý được. Chúng thường xuất hiện do dữ liệu đầu vào không hợp lệ, vấn đề khi kết nối tới một dịch vụ ngoài, v.v. Tất cả những trường hợp trên đều có thể dẫn đến việc exception được ném ra, làm gián đoạn luồng xử lý bình thường của chương trình. Error biểu diễn những vấn đề nghiêm trọng mà chương trình không thể giải quyết hoặc xử lý được. Những lỗi này thường liên quan đến vấn đề về môi trường chạy của JVM, như hết bộ nhớ heap hoặc StackOverflow. Một số error vẫn có thể xử lý được ở application layer, một số thì không thể xử lý được. Đa phần những lỗi này xảy ra rất khó dự đoán, khó nhận biết và gậy tổn hại đến hệ thống. 2.3. Tầm quan trọng của Exception HandlingException Handling chủ yếu giúp bảo vệ chương trình khỏi các sự cố và lỗi không dự đoán được, giữ cho chương trình tiếp tục hoạt động mà không bị gián đoạn, ngay cả khi gặp phải vấn đề. Việc này đặc biệt quan trọng trong các ứng dụng cần sự ổn định và độ tin cậy cao, ví dụ như các hệ thống quản lý cơ sở dữ liệu, các ứng dụng ngân hàng trực tuyến, hoặc các hệ thống điều khiển trực tuyến. Khi một ngoại lệ xảy ra, chương trình có thể tạo ra thông báo lỗi có ý nghĩa, ghi lại thông tin chi tiết về sự cố, và thậm chí có thể phục hồi hoặc khôi phục trạng thái hệ thống. Bên cạnh đó, việc xử lý ngoại lệ còn tăng cường khả năng bảo trì và phát triển chương trình. Khi các lỗi và sự cố được xử lý một cách hiệu quả và minh bạch, các lập trình viên có thể dễ dàng tìm ra nguyên nhân gốc rễ của vấn đề và phát triển các giải pháp và cải tiến một cách có hệ thống, giảm thiểu rủi ro và tăng hiệu suất phát triển. 3. Cơ chế Ngoại Lệ trong JavaTrong Java, chúng ta có thể sử dụng các cú pháp như
6,
0,
1 và
2 để xử lý các Exception. Các từ khoá đó ứng với mỗi một giai đoạn trong quá trình phát hiện và xử lý exception. Lưu ý, Java có phân biệt các kiểu ngoại lệ riêng biệt, mình sẽ đề cập đến các kiểu exception ở chương 4. Dưới đây là giải thích từng bước về cơ chế Exception trong Java 3.1. ThrowỞ một số điều kiện cụ thể, chương trình sẽ thực hiện ném ra một ngoại lệ. Ví dụ chúng ta phát triển một function có tên là
3, function này nhận 2 tham số
4 và
5, kết quả trả về là
6. Chương trình được viết như sau:
Chương trình trên sẽ gặp bất thường nếu số
7, do phép chia không thể chia cho không. Đây là một exception. Để xử lý exception này, chương trình của chúng ta cần xác định điều kiện cụ thể cho trường hợp đó, ở trường hợp này là khi
7. Khi đó, chúng ta cần viết code như sau
Có hai từ khoá mà các bạn cần lưu ý, là
9 ở dòng 1 và từ
6 ở dòng thứ ba.
Tóm lại, 2 từ khoá
6 và
9 được dùng để khai báo exception có thể xảy ra tại một function, và chuyển hướng kiểm soát exception này đến calling method. 3.2. Try/catchKhi called method thực hiện ném một exception, calling method sẽ có trách nhiệm xử lý exception đó. Calling method có thể xử lý trực tiếp, hoặc tiếp tục throw exception đó. Tiếp tục ví dụ ở trên, giả sử chúng ta call đến function
3 một cách trực tiếp như sau
Chương trình này sẽ không thể hoạt động được, do function
3 đã throw exception, nhưng function
8 không thực hiện xử lý exception đó. Dưới đây là đoạn code giúp function
8 có thể xử lý được exception của function
3.
Có hai từ khoá mà các bạn cần lưu ý, là
0 ở dòng thứ 2 và từ
1 ở dòng thứ năm.
3.3. FinallyTrong xử lý ngoại lệ của Java, từ khoá
2 được sử dụng để tạo một khối mã mà sẽ được thực thi sau khi thực hiện xong khối
0 và khối
1, không phụ thuộc vào việc có ngoại lệ xảy ra hay không. Điều này đảm bảo rằng mã trong khối finally luôn luôn được thực thi. Quay trở lại ví dụ trên về phép chia cho số 0 ở trên, đoạn mã trên có thể được bổ sung như sau:
Xin lưu ý từ khoá
2 ở dòng 9, câu lệnh in ra dòng "Finally block executed." sẽ luôn được hiển thị ra bất chấp code của chúng ta có rơi vào exception hay không. Điều này cho phép chúng ta xử lý một số logic bắt buộc, ví dụ như dọn dẹp tài nguyên (đóng file sau khi mở, đóng kết nối cơ sở dữ liệu). Finally là thành phần optional, nó không nhất thiết phải được sử dụng trong toàn bộ quá trình exception handling nếu không cần thiết. 4. Các loại Exception trong JavaCả hai lớp Error và Exception đều kế thừa từ một lớp cha, có tên là Throwable. Trong lớp Exception, chúng ta quan tâm đến 2 loại exception chính là RuntimeException, và các exception còn lại. RuntimeException là exception mà chỉ có thể phát hiện được trong runtime, JVM sẽ bỏ qua các lỗi này ở compiler. Các lớp exception kế thừa lớp RuntimeException có tên gọi là Unchecked Exception. Các other exception , kế thừa từ lớp exception được gọi là checked exception, do compiler phát hiện trong quá trình compile code. Do được phát hiện trong quá trình compile, các lập trình viên buộc phải xử lý try / catch các exception này. 4.1. Checked ExceptionChecked Exceptions là những exception mà compiler kiểm tra tại thời điểm biên dịch. Nếu một phương thức có khả năng phát sinh một Checked Exception, Java yêu cầu bạn hoặc phải xử lý ngoại lệ đó (sử dụng
9), để rõ ràng cho bản thân và cho người khác biết rằng có một tình huống ngoại lệ có thể xảy ra. Java sẽ không thể complier thành công nếu các Checked Exception không được xử lý hoàn toàn, do đó, chúng ta cần phải xử lý toàn bộ các checked exception trước khi complier. Mục tiêu chính của Checked Exception là để thông báo cho lập trình viên về các vấn đề có thể xảy ra, và buộc lập trình viên phải tự quyết định cách xử lý các tình huống đó.
Trong ví dụ trên,
1 là một Checked Exception. Do đó, Java buộc chúng ta phải xử lý ngoại lệ này (hoặc sử dụng
9 để bắt ngoại lệ hoặc sử dụng
9 để khai báo ngoại lệ). Các loại checked exception tiêu biểu bao gồm:
Xuất hiện khi có lỗi nhập/xuất dữ liệu, ví dụ khi đọc/ghi file. Các lớp con: FileNotFoundException, EOFException, UnsupportedEncodingException v.v.
Xuất hiện trong quá trình complier, khi JVM không thể tìm thấy class được yêu cầu.
Xuất hiện khi có lỗi trong quá trình truy vấn cơ sở dữ liệu
Phát sinh khi một thread đang chờ, ngủ, hoặc bận rộn, và một thread khác đã làm gián đoạn nó.
Là lớp cha của các ngoại lệ có thể phát sinh khi sử dụng API Reflection, như InvocationTargetException, InstantiationException, v.v.
Là một exception trong việc phân tích URL, nó sẽ kiểm tra URL có chính xác hay không.
Xuất hiện khi có lỗi phân tích (parse) chuỗi sang một đối tượng, thường xuất hiện khi chuyển đổi chuỗi sang đối tượng Date hoặc số. 4.2. Unchecked ExceptionKhác với checked exception có thể được JVM tìm ra và cảnh báo đến các lập trình viên, unchecked exception lại không được JVM phát hiện ra trong quá trình complier. Tất cả các unchecked exception đều kế thừa từ lớp RuntimeException, và chỉ được phát hiện trong runtime, không được kiểm tra trong compile-time. Compiler sẽ không báo lỗi hay cảnh báo nếu bạn không xử lý (ví dụ: không sử dụng
9 hoặc không khai báo
Các bạn chắc vẫn còn nhớ về ví dụ phép chia cho số 0 ở chương đầu.
Compiler sẽ không báo lỗi tại thời điểm biên dịch, nhưng khi bạn chạy chương trình, JVM sẽ ném ra
6 tại runtime nếu
7. Các loại unchecked exception tiêu biểu bao gồm:
Xảy ra khi có một phép toán không hợp lệ, ví dụ như chia một số cho zero.
Xảy ra khi chương trình cố gắng truy cập hoặc sử dụng một đối tượng mà thực sự là null.
Xảy ra khi chương trình cố gắng truy cập vào một chỉ số của mảng không tồn tại.
Xảy ra khi chương trình cố gắng ép kiểu (cast) một đối tượng sang một kiểu không tương thích.
Xảy ra khi chương trình cố gắng chuyển đổi một chuỗi không phải là số sang một kiểu số (ví dụ: Integer.parseInt("abc")).
Xảy ra khi một đối tượng không ở trong trạng thái thích hợp để thực hiện một hoạt động nào đó. Nhìn chung, những Unchecked Exception này thường xuất phát từ các lỗi lập trình và thiết kế, và lập trình viên có trách nhiệm phát hiện và xử lý chúng để tránh các lỗi runtime có thể xảy ra. Cá nhân mình nhận thấy đa phần lỗi xuất phát từ những Unchecked Exception này. 4.3. ErrorError là một lớp con của Throwable, nó ngang hàng với Exception. Do đó, về mặt bản chất nếu liệt kê Error là một Exception là không đúng. Tuy nhiên mình vẫn sẽ đề cập đến Error như một lỗi có thể xuất phát trong lúc code để cùng phân tích và xử lý. Giống với Exception, Error cũng kế thừa lớp Throwable, nên chúng ta có thể dùng cú pháp
8 để bắt được error trong quá trình thực thi code. Tuy nhiên, Error được xác định là một lớp riêng biệt với mục đích rất cụ thể: chúng ta không nên cố gắng bắt nó. Theo tài liệu chính thống từ Oracle – công ty quản lý và phát triển phiên bản chính thức của Java, Error biểu thị một vấn đề nghiêm trọng xảy ra khi thực thi chương trình mà ta không nên cố gắng xử lý. Đây là một tình trạng bất thường mà một ứng dụng không nên gặp phải. An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions. The ThreadDeath error, though a "normal" condition, is also a subclass of Error because most applications should not try to catch it. (by Oracle) Dưới đây là một số loại error phổ biến mà bạn có thể gặp trong Java:
Đây chỉ là một số error phổ biến trong Java, và có nhiều error khác tồn tại. Điều quan trọng cần nhớ là, trong hầu hết các trường hợp, các lỗi này biểu thị các vấn đề mà ứng dụng không nên (hoặc không thể) xử lý và thường cần được giải quyết ở cấp độ hệ thống hoặc cấu hình. 5. Custom một Exception hoặc ErrorNgoài các error, exception có sẵn, Java cho phép chúng ta tạo thêm các error và exception để lập trình viên có thể tuỳ biến và sử dụng trong các mục đích khác nhau. 5.1. Custom một ExceptionĐể có thể tạo ra một exception tuỳ ý, đầu tiên, bạn tạo một lớp mới kế thừa từ
9 với trường hợp checked exception, hoặc lớp
0 với trường hợp unchecked exception.
Tiếp theo, chúng ta sẽ sử dụng
1 này như sau
Ở ví dụ trên, chúng ta đã tạo và sử dụng một class tên
1. Function
3 đã khai báo
4, vì thế function
8 buộc phải xử lý
8 mà function
3 đã ném ra. 5.2. Custom một ErrorTương tự như việc tạo ra một exception tuỳ chỉnh, chúng ta cũng có thể tạo một error tuỳ chỉnh bằng cách tạo ra một class kế thừa class
8.
Tuy nhiên, xin lưu ý rằng việc tạo và sử dụng lớp error tùy chỉnh không được khuyến khích, vì
8 thường dành cho các vấn đề nghiêm trọng mà JVM gặp phải và không nên được sử dụng trong mã ứng dụng thông thường. 6. Thực hành một số Exception thường gặp6.1. Checked ExceptionDưới đây là một số ví dụ về việc sử dụng checked exception 6.1.1. Tìm kiếm fileĐể có thể tìm kiếm vào trong một file, chúng ta sử dụng class
0
0 Class
1 sẽ throw một exception là
1 Vì thế, khi chúng ta muốn tìm kiếm một file, chúng ta cần xử lý exception
1. Vì thế, code đầy đủ trong trường hợp này là:
1 6.1.2. Tìm một classĐể tim một class khác trong Java, chúng ta có thể dùng hàm
4
2 Function
4 sẽ throw một exception tên là
6 Do đó, chúng ta cần xử lý exception này khi sử dụng.
3 6.2. Unchecked Exception6.2.1. Kiểm tra giá trị nullKhi chúng ta đang cố truy cập vào một giá trị null, exception
7 sẽ được ném ra. Tuy nhiên do là unchecked exception, chúng ta có thể không cần catch exception này.
4 6.2.2. Truy cập vào index không tồn tạiKhi chúng ta cố gắng truy cập đến một index không có trong một array, exception
8 sẽ được ném ra.
5 6.3. ErrorThực tế, khi một
8 xảy ra, nó thường chỉ ra một vấn đề nghiêm trọng và ứng dụng thường nên được dừng lại và kiểm tra lại. Do đó, bất cứ thời điểm và dòng code nào trong ứng dụng cũng có thể xuất hiện
8, vì thế
8 mới khó đoán và khó xử lý. Dưới đây là một số mô phỏng thực tế có thể xảy ra
8. 6.3.1. Hết bộ nhớKhi hệ thống hết bộ nhớ,
3 sẽ được ném ra. Chúng ta có thể mô phỏng lỗi này bằng cách cố gắng cấp phát một mảng lớn.
6 6.3.2. Tràn bộ đệm (stack overflow)Khi độ sâu của ngăn xếp gọi vượt quá giới hạn cho phép,
4 sẽ được ném ra. Một cách thông thường để gây ra lỗi này là thông qua đệ quy không có điều kiện dừng:
7 7. Best practice về Exception trong JavaKhi làm việc với exception trong Java, việc tuân theo một số best practice sẽ giúp mã của bạn dễ đọc, dễ bảo trì và tránh được các lỗi tiềm ẩn. Một số cách triển khai thiếu hiệu quả sẽ có tác dụng ngược lại, vì thế hãy thận trọng khi sử dụng exception. Dưới đây là một số best practice bạn nên theo dõi: 7.1. Sử dụng một Exception đúng nghĩaKhi bạn sử dụng một loại exception không phù hợp với tình huống thực sự xảy ra. Giả sử bạn muốn kiểm tra giá trị tuổi của một người:
8 Việc sử dụng Exception sai với bối cảnh mà chúng ta gặp phải dễ dẫn đến các nhầm lẫn trong quá trình triển khai code sau này. Ở ví dụ trên, chúng ta nên tạo một exception mới, có tên là
5.
9 Với trường hợp này, exception
5 rất rõ nghĩa, đúng bối cảnh sử dụng, tránh các nhầm lẫn không đáng có và gây ra hiểu nhầm không cần thiết. 7.2. Tránh việc "ăn" Exception - Eating ExceptionsKhi chúng ta bắt gặp một exception, chúng ta buộc phải xử lý nó. Đó là lý do mà exception tồn tại. "Ăn" exception là hành động ngược lại, chúng ta cố tình phớt lờ, bỏ qua nó, "ăn" nó khiến exception này dường như không tồn tại trong hệ thống. Việc bỏ qua exception sẽ gây ra rất nhiều hệ luỵ, ví dụ như:
Và một vài vấn đề khác nữa. Ví dụ về một trường hợp "ăn" exception
0 Hiển nhiên việc không làm gì trong khối
1 là một hành động "ăn" exception, nó làm mất đi dấu vết về một vấn đề trong hệ thống. Nếu không làm gì về mặt logic, ít nhất chúng ta cũng nên viết Log để hệ thống có thể truy vết trong trường hợp cần thiết.
1 7.3. Sử dụng Checked Exception cho các tình huống cố gắng phục hồiBài toán giả định đặt ra là bạn đang muốn kết nối đến một cơ sở dữ liệu. Sử dụng unchecked exception trong trường hợp này không bắt buộc người gọi phương thức phải xử lý ngoại lệ hoặc phải khai báo nó. Điều này dẫn đến việc người dùng hoặc ứng dụng có thể không biết về khả năng xảy ra ngoại lệ và không có cơ hội phục hồi từ sự cố đó.
2 Sử dụng checked exception (ví dụ:
8,
9 hoặc một exception tùy chỉnh) cho tình huống trên, ví dụ:
3 Khi sử dụng checked exception, calling method bị ép buộc phải xử lý ngoại lệ. Điều này thông báo cho chúng ta về khả năng xảy ra ngoại lệ và buộc chúng ta phải có cơ chế phục hồi, ví dụ: bằng cách thử kết nối lại hoặc hiển thị thông báo lỗi cho người dùng cuối. 7.4. Viết rõ chi tiết trong thông điệp ExceptionViệc đưa ra đúng exception đôi khi vẫn chưa đủ ngữ cảnh cho chúng ta, do đó việc viết một message chính xác cũng cần được đảm bảo. Ví dụ về cách dùng không chính xác
4 Việc viết ra một message lỗi là Error occurred rất chung chung, khó hiểu và vì thế, có thể làm chúng ta mất thời gian để hiểu toàn bộ thông tin lỗi. Thay vào đó, một message tốt sẽ dễ dàng cho chúng ta biết được bối cảnh đang diễn ra.
5 Kết luận về ExceptionException, hay ngoại lệ, là một sự kiện bất thường xảy ra trong quá trình thực thi chương trình và có thể làm gián đoạn quá trình này. Các exception xuất phát từ nhiều nguyên nhân khác nhau, chẳng hạn như lỗi người dùng, lỗi phần cứng, hoặc lỗi logic trong mã nguồn. Mục tiêu chính của việc sử dụng exception là cung cấp một cơ chế để phát hiện và xử lý các tình huống bất thường mà không làm gián đoạn toàn bộ chương trình. Hơn nữa, nó giúp mã nguồn trở nên gọn gàng, dễ đọc và dễ bảo trì hơn bằng cách tách biệt giữa logic chính của chương trình và logic xử lý lỗi. Để triển khai exception một cách hiệu quả, người lập trình nên tuân theo một số nguyên tắc quan trọng.
Exception là một công cụ mạnh mẽ trong lập trình, cho phép ta xử lý các tình huống không mong muốn một cách linh hoạt và hiệu quả. Khi được sử dụng đúng cách, exception không chỉ giúp mã nguồn trở nên sạch sẽ và dễ đọc hơn, mà còn tăng cường độ bền và ổn định của ứng dụng. Tài liệu tham khảo:
Exception trong Java đóng vai trò quan trọng trong việc xử lý lỗi và ngoại lệ, giúp quản lý lỗi một cách hiệu quả. Nắm vững cách sử dụng và xử lý exception là một kỹ năng cần thiết cho mọi lập trình viên Java. Bài viết đã giới thiệu khái niệm, cách sử dụng và cách xử lý exception, đồng thời chia sẻ những lời khuyên quan trọng khi làm việc với lỗi và ngoại lệ. Nắm bắt điều này sẽ giúp bạn xây dựng ứng dụng Java ổn định và đáng tin cậy hơn. Hãy tiếp tục rèn luyện và áp dụng kiến thức này vào công việc lập trình hàng ngày của bạn nhé! Bạn hãy thường xuyên theo dõi các bài viết hay về Lập Trình & Dữ Liệu trên 200Lab Blog nhé. Cũng đừng bỏ qua những khoá học Lập Trình tuyệt vời trên 200Lab nè. |