English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
The reason why exceptions are a powerful debugging tool lies in their ability to answer the following three questions:
1, what went wrong?63;
2, where did the error occur?63;
3, why did the error occur?63;
In the case of effective use of exceptions, the exception type answers 'what' was thrown, the exception stack trace answers 'where' it was thrown, and the exception information answers 'why' it was thrown. If your exception does not answer all of the above questions, then you may not be using them well.
There are three principles that can help you make the most of exceptions during debugging. These principles are:
1, specific and clear
2, early throwing
3, delayed capture
To illustrate the three principles of effective exception handling, this article discusses the fictional personal financial management class JCheckbook, which is used to record and track bank account activities such as deposits, withdrawals, and bill issuance.
Specific and clear
Java defines an exception class hierarchy, starting with Throwable, extending to Error and Exception, and then Exception extends to RuntimeException. As shown in the figure1As shown.
These four classes are generalized and do not provide much error information. Although it is grammatically correct to instantiate these classes (such as: new Throwable()), it is still better to treat them as base classes and use their more specialized subclasses. Java has provided a large number of exception subclasses, and if you need more specificity, you can also define your own exception classes.
For example, the java.io package defines the IOException subclass of the Exception class, which is more specialized, such as FileNotFoundException, EOFException, and ObjectStreamException, which are subclasses of IOException. Each describes a specific type of I/O errors: They are file loss, incorrect file end, and incorrect serialization object stream. The more specific the exception, the better our program can answer the question of 'what went wrong'.
It is very important to be as clear as possible when handling exceptions. For example: JCheckbook can handle FileNotFoundException by asking the user for the file name again, and for EOFException, it can continue to run based on the information read before the exception was thrown. If ObjectStreamException is thrown, the program should prompt the user that the file is corrupted and suggest using a backup file or another file.
Java makes it easy to explicitly catch exceptions because we can define multiple catch blocks for the same try block, thus handling each type of exception appropriately.
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 clear information about caught exceptions to the user by using multiple catch blocks. For example: if a FileNotFoundException is caught, it can prompt the user to specify another file. In some cases, the additional coding work brought by multiple catch blocks may be an unnecessary burden, but in this example, the additional code indeed helps 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 more generalized error information to the user when IOException is thrown. In this way, the program can provide as much specific information as possible, but also has the ability to handle unexpected other exceptions.
Sometimes developers catch general exceptions and display the exception class name or print stack information to seek 'specificity'. Never do that! Users seeing java.io.EOFException or stack information will only have a headache instead of getting help. It should catch specific exceptions and give users clear information in 'layman's terms'. However, the exception stack can be printed in your log file. Remember, exceptions and stack information are used to help developers, not users.
Finally, it should be noted that JCheckbook does not catch exceptions in readPreferences() but leaves the catching and handling of exceptions to the user interface layer, so that users can be notified through dialog boxes or other methods. This is called 'deferred catching', which will be discussed later.
quick fail
The exception stack trace provides the exact sequence of method calls that caused the exception, including the class name, method name, code file name, and even line numbers, in order to accurately locate the scene where the exception occurred.
java.lang.NullPointerException at 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 the FileInputStream class throws a NullPointerException. However, note that FileInputStream.close() is part of the standard Java class library, and the likely cause of this exception may be in our code rather than the Java API. Therefore, the problem may lie in one of the previous methods, fortunately, it is also printed in the stack trace.
Unfortunately, NullPointerException is one of the least informative (but also the most frequently encountered and frustrating) exceptions in Java. It doesn't mention what we care about most: where is null. So we have to backtrack a few steps to find out where the error occurred.
By stepping back through the stack trace and checking the code, we can determine that the error was caused by passing an empty filename parameter to readPreferences(). Since readPreferences() knows it cannot handle an empty filename, it immediately checks this condition:
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 exceptions early (also known as 'quick fail'), exceptions can be clear and accurate. The stack trace immediately reflects what went wrong (provided an invalid parameter value), why it went wrong (the filename cannot be a null value), and where it went wrong (the beginning part of readPreferences()). In this way, our stack trace can provide an accurate account:
java.lang.IllegalArgumentException: filename is null at 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)
Additionally, the exception information contained within ("The filename is empty") makes the exception information more informative by explicitly answering the question of what is empty, which is something that the NullPointerException thrown in our previous code could not provide.
Implementing quick failure by immediately throwing an exception when an error is detected can effectively avoid unnecessary object construction or resource consumption, such as files or network connections. Similarly, the cleanup operations brought about by opening these resources can also be omitted.
Deferred catching
A mistake that both beginners and experts may make is to catch exceptions before the program is capable of handling them. The Java compiler indirectly encourages this behavior by requiring that checked exceptions must be caught or thrown. The natural approach is to immediately wrap the code in a try block and use a catch to catch exceptions to avoid compiler errors.
The problem is, what to do with the exception after catching it? The worst thing to do is to do nothing. An empty catch block is equivalent to throwing the entire exception into a black hole, and all information that could explain when, where, and why an error occurred would be lost forever. Writing the exception to the log is slightly better, at least there is a record to check. But we can't expect users to read or understand log files and exception information. It is also not appropriate to display an error information dialog box for readPreferences(), because although JCheckbook is currently a desktop application, we plan to make it a web-based HTML application. In that case, displaying an error dialog is obviously not an option. At the same time, whether it is HTML or C/In the S version, configuration information is read from the server, while error information needs to be displayed to the Web browser or client application. The readPreferences() method should take these future needs into account during design. Appropriately separating user interface code and program logic can improve the reusability of our code.
Catching exceptions too early before handling them conditionally usually leads to more severe errors and other exceptions. For example, if the readPreferences() method in the preceding text immediately catches and logs a possible FileNotFoundException when calling the FileInputStream constructor, the code would become as follows:
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 caught FileNotFoundException without being able to recover from it at all. If the file cannot be found, the method below clearly cannot read it. What will happen if readPreferences() is required to read a non-existent file? Of course, FileNotFoundException will be logged, and if we look at the log file at that time, we will know. However, what will happen when the program tries to read data from the file? Since the file does not exist, the variable in is empty, and a NullPointerException will be thrown.
When debugging a program, our instinct tells us to look at the information at the end of the log. That will be a NullPointerException, which is very annoying because this exception is very general. The error message not only misleads us about what went wrong (the real error is FileNotFoundException rather than NullPointerException), but also misleads us about the source of the error. The real problem is outside the several lines where NullPointerException is thrown, and there may be several method calls and class destructions in between. Our attention is drawn away from the real error by this small fish until we look back at the log to find the source of the problem.
Since the real job of readPreferences() is not to catch these exceptions, what should it be? It seems a bit counterintuitive, but in fact, the most appropriate action is often to do nothing and not catch the exception immediately. Pass the responsibility to the caller of readPreferences(), letting it study the appropriate method to handle the missing configuration file. It may prompt the user to specify another file, or use a default value, or possibly warn the user and exit the program if necessary.
The method of passing the responsibility of exception handling upstream in the call chain is to declare the exception in the throws clause of the method. When declaring the possible exceptions, be as specific as possible. This is used to identify the exception types that the program calling your method needs to know and be prepared to handle. For example, the 'deferred catch' 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 IOException, but we explicitly declare that the method may throw FileNotFoundException. IllegalArgumentException is not mandatory to declare because it is a non-checked exception (i.e., a subclass of RuntimeException). However, we declare it to document our code (these exceptions should also be annotated in the JavaDocs of the method).
Of course, your program needs to catch exceptions in the end, otherwise it will terminate unexpectedly. But the trick here is to catch exceptions at the appropriate level so that your program can either recover meaningfully from the exception and continue without causing deeper errors; or provide clear information to the user, including guiding them to recover from the error. If your method is not up to the task, then do not handle the exception, leave it to be caught and handled at the appropriate level.
Summary
Experienced developers know that the biggest difficulty in debugging programs is not in fixing defects, but in finding the hiding places of defects in a massive amount of code. By following the three principles in this article, you can make your exceptions help you track and eliminate defects, making your program more robust and user-friendly. That's all for this article, and I hope it can bring some help to your learning or work.
Statement: The content of this article is from the Internet, and the copyright belongs to the original author. The content is contributed and uploaded by Internet users spontaneously. This website does not own the copyright, has not been manually edited, and does not assume any relevant legal liability. If you find any content suspected of copyright infringement, please send an email to: notice#oldtoolbag.com (Please replace # with @ when sending an email to report violations, and provide relevant evidence. Once verified, this site will immediately delete the infringing content.)