The reason why exceptions are a powerful debugging method is that they answer the following three questions:
1. What went wrong?
2. Where did you make a mistake?
3. Why did something go wrong?
In the case of effective use of exceptions, the exception type answers "what" is thrown, the exception stack trace answers "where" is thrown, the exception information answers "why" is thrown, and if your exception does not answer all of the above questions, then you may not use them well.
There are three principles that can help you maximize the use of good exceptions during debugging. These three principles are:
1. Specific and clear
2. Throw it early
3. Delay capture
In order to explain these three principles of effective exception handling, this article discusses it by fabricating a personal financial manager JCheckbook, which is used to record and track bank account activities such as deposits and withdrawals and bill issuance.
Concrete and clear
Java defines a hierarchy of an exception class, which starts with Throwable, extends Error and Exception, and Exception extends RuntimeException. As shown in Figure 1.
These four classes are generalized and do not provide much error information. Although instantiating these classes is syntactically legal (such as: new Throwable()), it is best to treat them as virtual base classes and use their more specialized subclasses. Java has provided a large number of exception subclasses. If you want to be more specific, you can also define your own exception class.
For example: The java.io package defines the subclass of the Exception class IOException, which is more specialized and subclass of IOException such as FileNotFoundException, EOFException and ObjectStreamException. Each describes a specific class of I/O errors: file loss, exception file ending and error serialized object streams. The more specific the exception, our program will be able to answer the question "what went wrong".
It is also important to try to be as clear as possible when catching exceptions. For example: JCheckbook can handle FileNotFoundException by re-querying the user's file name. For EOFException, it can continue to run based on the information read before the exception is thrown. If an ObjectStreamException is thrown, the program should prompt the user that the file is corrupt and a backup file or other file should be used.
Java makes it easy to explicitly catch exceptions, because we can define multiple catch blocks for the same try block, so that each exception can be handled appropriately separately.
File prefsFile = new File(prefsFilename); try{ readPreferences(prefsFile);}catch (FileNotFoundException e){ // alert the user that the specified file // does not exist}catch (EOFException e){ // alert the user that the end of the file // was reached}catch (ObjectStreamException e){ // alert the user that the file is corrupted}catch (IOException e){ // alert the user that some other I/O // error occurred}JCheckbook provides users with explicit information about the capture of exceptions by using multiple catch blocks. For example: If FileNotFoundException is caught, it can prompt the user to specify another file. In some cases, the additional encoding effort brought by multiple catch blocks may be a non-essential burden, but in this example, the extra code does help the program provide a more user-friendly response.
In addition to the exceptions handled by the first three catch blocks, the last catch block provides the user with more generalized error information when the IOException is thrown. This way, the program can provide specific information as much as possible, but also has the ability to handle other unexpected exceptions.
Sometimes developers catch normalized exceptions and display the exception class name or print stack information for "specificity". Don't do this! Users will only have a headache when seeing java.io.EOFException or stack information, rather than getting help. Specific exceptions should be captured and the user should be prompted with "human words". However, the exception stack can be printed in your log file. Remember, exceptions and stack information are used to help developers rather than users.
Finally, it should be noted that JCheckbook does not catch exceptions in readPreferences() , but leaves the catch and handle exceptions to the user interface layer, so that users can be notified using dialog boxes or other methods. This is called "delay capture", which will be discussed below.
Throw out early
Exception stack information provides the exact order of the method call chain that causes the exception to occur, including the class name, method name, code file name and even line number of each method call, in order to accurately locate the scene where the exception occurs.
java.lang.NullPointerExceptionat java.io.FileInputStream.open(Native Method) at java.io.FileInputStream.<init>(FileInputStream.java:103) at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:225) at jcheckbook.JCheckbook.startup(JCheckbook.java:116) at jcheckbook.JCheckbook.<init>(JCheckbook.java:27) at jcheckbook.JCheckbook.main(JCheckbook.java:318)
The above shows the case where the open() method of FileInputStream class throws a NullPointerException. However, note that FileInputStream.close() is part of the standard Java class library, and it is likely that the problem of this exception is that our code itself is not the Java API. So the problem is likely to appear in one of the previous methods, and fortunately it is also printed in the stack information.
Unfortunately, NullPointerException is the least informative (but also the most common and crashing) exception in Java. It doesn't mention what we care about most at all: where is null. So we had to take a few steps back and find out where something went wrong.
By step backing the tracking stack information and checking the code, we can determine that the cause of the error is that an empty filename parameter was passed into readPreferences() . Since readPreferences() knows that it cannot handle empty file names, check the condition immediately:
public void readPreferences(String filename)throws IllegalArgumentException{ if (filename == null){ throw new IllegalArgumentException("filename is null"); } //if //...perform other operations... InputStream in = new FileInputStream(filename); //...read the preferences file...} By throwing an exception early (also known as "failure quickly"), the exception is clear and accurate. The stack information immediately reflects what went wrong (illegal parameter values were provided), why it went wrong (the file name cannot be null), and where it went wrong (the first part of readPreferences() ). In this way, our stack information can be provided truthfully:
java.lang.IllegalArgumentException: filename is nullat jcheckbook.JCheckbook.readPreferences(JCheckbook.java:207) at jcheckbook.JCheckbook.startup(JCheckbook.java:116) at jcheckbook.JCheckbook.<init>(JCheckbook.java:27) at jcheckbook.JCheckbook.main(JCheckbook.java:318)
In addition, the exception information contained in it ("file name is empty") makes the exception richer by explicitly answering the question of what is empty, which is not available to us in the previous code.
Failure is achieved by throwing an exception immediately when an error is detected, unnecessary object construction or resource usage, such as file or network connection, can be effectively avoided. Similarly, the cleaning operations brought about by opening these resources can be saved.
Delayed Capture
One mistake that both novices and masters can make is to catch the program before it has the ability to handle the exception. The Java compiler indirectly facilitates this behavior by requiring that the checked exception must be caught or thrown. The natural way is to immediately wrap the code in a try block and use catch to catch exceptions to avoid the compiler's error.
The question is, what should I do if I get the exception after being caught? The worst thing to do is do nothing. An empty catch block is equivalent to throwing the entire exception into the black hole, and all information that can explain when, where, why errors are wrong will be lost forever. It is a little better to write exceptions into the log, at least there are records to check. But we can't expect users to read or understand log files and exception information. It is also not appropriate to have readPreferences() display an error message dialog, because while JCheckbook is currently a desktop application, we also plan to turn it into an HTML-based web application. In that case, displaying an error dialog is obviously not an option. At the same time, regardless of HTML or C/S version, the configuration information is read on the server, and the error message needs to be displayed to the web browser or client program. readPreferences() should also take these future needs into consideration when designing. Proper separation of user interface code and program logic can improve the reusability of our code.
Catching an exception prematurely before conditional handling it often results in more serious errors and other exceptions. For example, if readPreferences() method above immediately captures and records the FileNotFoundException that may be thrown when the FileInputStream constructor is called, the code will become like this:
public void readPreferences(String filename){ //... InputStream in = null; // DO NOT DO THIS!!!try{ in = new FileInputStream(filename);}catch (FileNotFoundException e){ logger.log(e);} in.read(...); //...}The above code captures it without the ability to recover from FileNotFoundException. If the file cannot be found, the following method obviously cannot read it. What happens if readPreferences() is asked to read a file that does not exist? Of course, FileNotFoundException will be recorded, and if we looked at the log file at that time, we would know. However, what happens when a program tries to read data from a file? Since the file does not exist, the variable in is empty, and a NullPointerException will be thrown.
When debugging a program, instinct tells us to look at the information at the end of the log. That would be NullPointerException, and what is very annoying is that this exception is very unspecific. The error message not only misleads us what went wrong (the real error is FileNotFoundException instead of NullPointerException), but also misleads the source of the error. The real problem is outside of the number of rows at the NullPointerException thrown, which may have several method calls and class destruction. Our attention was drawn from the real mistake by the little fish until we looked back at the log to find the source of the problem.
Since what readPreferences() really should do is not catch these exceptions, what should it be? It seems a bit counterintuitive, and the most appropriate way is to do nothing and don't catch exceptions immediately. Leave the responsibility to the caller of readPreferences() and let it study the appropriate way to deal with missing configuration files. It may prompt the user to specify other files, or use the default value. If it really doesn't work, it may warn the user and exit the program.
The way to pass the responsibility for exception handling upstream of the call chain is to declare the exception in the throws clause of the method. When declaring possible exceptions, be careful, the more specific the better. This is used to identify the type of exception that the program that calls your method needs to know and prepare to handle. For example, the "delay capture" version of readPreferences() might look like this:
public void readPreferences(String filename)throws IllegalArgumentException,FileNotFoundException, IOException{ if (filename == null){ throw new IllegalArgumentException("filename is null"); } //if //... InputStream in = new FileInputStream(filename); //...}Technically, the only exception we need to declare is an IOException, but we explicitly declare that the method may throw a FileNotFoundException. IllegalArgumentException is not necessary to be declared because it is a non-checking exception (i.e., a subclass of RuntimeException). However, it is declared to document our code (these exceptions should also be marked in the JavaDocs of the method).
Of course, eventually your program needs to catch the exception, otherwise it will terminate unexpectedly. But the trick here is to catch exceptions at the right level so that your program can either meaningfully recover from exceptions and continue without causing deeper errors; or be able to provide users with clear information, including guiding them to recover from errors. If your method is not competent, then don't handle the exception, leave it behind to catch and handle it at the right level.
Summarize
Experienced developers know that the biggest difficulty in debugging programs is not to fix defects, but to find out the hiding places of defects from a massive amount of code. As long as you follow the three principles of this article, your exceptions can help you track and eliminate defects, make your program more robust and user-friendly. The above is the entire content of this article, and I hope it will help you in your study or work.