Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Rules for Developing Robust Programs with Java Exceptions TDT4735 Software Engineering, Depth Study Hoa Dang Nguyen and Magnar Sveen November 28, 2003 Teaching supervisor: Tor Stålhane Norwegian University of Science and Technology, NTNU Department of Computer and Information Science, IDI i Abstract Modern object oriented programming languages like Java and C# offer advanced exception handling mechanisms. As systems grow more complex and customer demand for robustness increases, understanding and knowing how to use exception handling techniques effectively is increasingly important. In the past, developers considered exceptions as an afterthought or an as-you-go addition, resulting in difficult debugging and unstable programs. Working on this project, we searched books, articles and the web for guidelines and pitfalls regarding the use of Java exceptions. We tested integrated development environments and other software designed to help analyze Java programs and the use of exceptions. Based on what we have learned, this paper formulates a set of rules to help system developers create robust Java programs with the use of exceptions. To build a robust system, the developers need to take exception handling into account throughout the development process. Exception handling is an integral part of a robust system, and can not be added as an afterthought. Java’s exception handling mechanisms are powerful, easy to use, and easy to misuse. This paper proposes some rules to help avoid or correct the misuses of exceptions. i Preface This document is the result of our work in the course TDT4735 Systemutvikling, Fordypning. The work has been performed at the Department of Computer and Information Science, Faculty of Information Technology, Mathematics and Electrical Engineering at the Norwegian University of Science and Technology (NTNU) during the autumn semester of 2003. Acknowledgements We are very thankful for the support, advice, feedback and comments from our teaching supervisor, Tor Stålhane. In addition we would wish to thank Catherine Pierce for invaluable help with the English language, and Alf Inge Wang and Mario Aparicio for letting us use the code from DIAS 2 and IpManager for testing purposes. Trondheim, 28. November 2003. ____________ Hoa Dang Nguyen ____________ Magnar Sveen ii Contents ABSTRACT PREFACE ACKNOWLEDGEMENTS CONTENTS II III III IV 1 INTRODUCTION 1 1.1 1.2 1.3 1.4 1 2 2 3 2 2.1 2.2 2.3 3 3.1 3.2 3.3 3.4 3.5 3.6 3.7 4 4.1 4.2 5 5.1 5.2 5.3 5.4 5.5 6 MOTIVATION AND BACKGROUND PROBLEM DESCRIPTION RELATED WORK STRUCTURE OF REPORT EXCEPTION HANDLING WHAT THEY ARE FOR HOW THEY WORK HISTORY JAVA EXCEPTIONS EXCEPTIONS AS OBJECTS THE THROW STATEMENT THE TRY-CATCH CONSTRUCT CLEANING UP WITH FINALLY CATCH OR DECLARE RUNTIME EXCEPTIONS CHAINED EXCEPTIONS CURRENT TRENDS DESIGNING FOR ROBUST JAVA PROGRAMS WITH EXCEPTIONS BEST PRACTICES FOR USING EXCEPTIONS ANALYSIS TOOLS JEX JIKES BYTECODE TOOLKIT TEAMSTUDIO ANALYZER FOR JAVA J2EE CODE VALIDATION FOR WEBSPHERE STUDIO CONCLUSION RULES 6.1 SYSTEM REQUIREMENTS PHASE 6.2 ARCHITECTURAL DESIGN 6.3 DETAILED DESIGN 4 4 4 5 6 6 7 8 8 9 9 10 12 12 15 21 21 24 25 26 28 29 29 30 30 iii 6.4 IMPLEMENTATION 6.5 TESTING 7 7.1 7.2 THE RULES IN PRACTICE IP MANAGER DIAS 2 30 33 34 34 37 8 CONCLUSIONS AND FURTHER WORK 40 9 REFERENCES 41 iv 1 Introduction Exception handling is something most people have heard about, but do not always know how to use properly. From our own experience and others, exception handling was the last mechanism we learnt and the last to be used. We will with this report show the importance and necessity of exception handling. 1.1 Motivation and Background We got the idea for this report after a meeting with our teaching supervisor, Tor Stålhane. He made us aware of the lack of guidance, guidelines, rules and tips within exception handling in Java. Java is a programming language that we have been familiar with since the freshman year. We have been programming with Java ever since, but did not realize how important exception handling was until recently. When we reflect on our work with exception handling, we can see the lack of understanding and use of exception handling. The literature which introduced us to Java only gave us a little knowledge about exception handling. After some research, we noticed how few guidelines that was available for exception handling. We saw at once the potential for writing a complete guideline or a set of rules for exception handling. With powerful development tools and increasing hardware performance, systems tend to become more and more complex in order to take advantage of these opportunities. Complex systems are harder to code, harder to debug and harder to maintain. The more complex the systems get the more unstable they will become. With the increasing demand for robust system from the customers, developers are forced to focus more on reliability and robustness. Exception code design and analysis is complex and can be hard to handle. The different components have to co-operate and interact with each other in faultless way. The process of exception handling is not straight forward but rather complex. When one component decides to throw an exception it can not handle, that exception can be caught anywhere in the program. Code violation can occur anywhere in the code and have to be handled. Developers have to plan in advance how exceptions are handled before they occur. Logging for failure that occurred also has to be considered thoroughly. It is not easy to known where or when to include exception handling in your development process. Exception handling is usually not addressed at the appropriate phase of system development. People tend to forget about exception handling and only take them in consideration when they encounter problems with their program. This will only cause the program to be more unstable and harder to debug. Even though the developers do plan exception handling ahead, they do not always know how to use them properly. Methodologies supporting proper use of exception handling are few and far between. The methodologies that are available today are not complete, they are rather scattered. You may find many books that include many good guides for using exceptions, but you may find other guides elsewhere not mentioned in the books. 1 1.2 Problem Description Many developers and programmers do not know how to use exceptions efficiently. Some see Java exceptions as a hassle, and only think about them when forced to do so by the Java compiler. The exceptions are inserted into the system too late, and are often caught and just ignored to stop the compiler from complaining. Other developers see the need for increasing the robustness of their program after it has been implemented, and try to plug exception handling into the system near the end of the process, usually much too late. We want to show that Java’s exception handling mechanism is a powerful and essential tool for creating robust programs. We will present a rule set to help developers use Java exceptions for more than just showing error messages and stack traces. We will show the importance of using these rules throughout the development process. Starting with the system requirements phase, we will suggest rules to help developers use exceptions in a better way through the different phases of architectural design, detailed design, implementation and testing and delivery of the finished product. 1.3 Related Work Although there is an overabundance of books about Java, and many of these include a chapter about how Java’s exception handling mechanisms work, there are only a few books that delve deeper into the subject of good use of exceptions. [PRJ2000] is a collection of practical suggestions, advice, examples and discussions about programming in the Java language. It has a section for exception handling that contains eleven good practices when using exceptions and the reasoning behind them. [EFJ2001] is a guide to effective use of the Java programming language. Its chapter about exceptions contains nine rules for using exceptions effectively, to improve a program’s readability, reliability, and maintainability. Of the publicly available articles about exception handling, many had a narrow focus on some particular aspect of exception handling, and thus did not help us much in our search for general guidelines and best practices. One article we think is a good read is [EAE2003]. It presents a simple component–based strategy addressing the various ways for using exceptions better in Java. The paper is based on the experience of the development of many real large software projects. Another article which we also find useful and interesting in relation to our work is [AEU2003]. This article discusses common trends in the use of exception handling in large Java applications, and proposes some solutions to the more common pitfalls. [EXS2003] compares Java exceptions with C# exceptions, and also presents a dozen strategies for using exceptions in Java. The World Wide Web is a great resource for information, and there are plenty of forums where you can get hints and tips and read about exception handling in Java. Many of the sites and articles on the net have more guidelines than the books you may find in your local store. 2 1.4 Structure of Report We start this paper by giving the reader a basic introduction to exception handling. Chapter 2 describes what exception handling is and why programming languages have this mechanism, in addition to a short history of exception handling. Chapter 3 describes Java’s exception handling mechanisms in detail. Chapter 4 looks at current trends in exception handling and summarizes information that is currently available. Several tips and guidelines are gathered from various books, articles and forums. We have tested several programs and toolkits concerning exception handling in Java in Chapter 5. Our main contribution is in Chapter 6 where we present a set of rules to help developers create more robust programs. We then look at the rules in the context of real life examples in Chapter 7, discussing what is good, what is bad and what could have been done better. Chapter 8 concludes the report, and looks at possible further work. 3 2 Exception Handling Following is a short introduction to exception handling. Sections 2.1 and 2.2 explain why programming languages have exceptions and how exception handling works. We will then provide the user with a short history of exception handling in section 2.3. 2.1 What they are for The main idea behind exception handling is to make programs more reliable and robust. Many programming languages provide exception handling mechanisms to allow software developers to define exceptional conditions. This helps the developers to structure the exceptional activities of the system component. It is important to have a well thought-out and consistent exception handling strategy for the sake of efficiency and good programming practice. Exception handling should not be considered as an add-on but as an integral part of the development process. The power of exceptions provides a framework on which to develop applications that are robust and dependable by design, rather than by accident. 2.2 How they work There exist two types of activities taking place during the execution of a program; normal and abnormal activities. In the normal activities, software components may receive service requests and produce responses. If the component does not satisfy a service request, it returns an exception. There are several exceptions that can occur, as represented in Figure 2.1.We can classify exceptions into three categories: interface, internal and failure. - Interface exceptions: When a request does not conform to the component’s interface the response will be an interface exception. Internal exceptions: This is the type of exception that is called by the component itself in order to invoke its own internal exceptional handler. Failure exceptions: This occurs when the component itself is unable to handle exceptions If the component is able to handle the exception it will return to normal state and the program will continue to run normally. When the component is unable to handle the exception it will try to throw it back to the caller. If the caller is unable to handle the exception it will try to throw it to the next caller. This will go on until the exception is handled or else the program will halt. 4 Service requests Normal responses Interface exceptions Failure exceptions Internal exceptions Abnormal Activity Normal Activity (fault tolerance by exception handling) Return to normal operation Service requests Normal responses Interface Failure exceptions exceptions Figure 2.1 Idealized fault-tolerant components taken from [EHM1999]. 2.3 History Already in the mid 1950’s we find something similar to exception handling, you may call it a predecessor to exception handling. It was the Lisp 1.5 developed by John McCarthy who introduced a function called “errset” which allowed the Lisp interpreter and compiler to exit when an error occurred. Later, in the mid 1960’s, a programming language, developed at IBM called PL/I, included facilities for dealing with control flow. The earlier languages had few facilities that dealt with exceptional conditions during execution. There were several problems with this mechanism, but the philosophy of programming language design for reliability demanded that this facility be included in the programming language definition. By the mid 1970’s, software engineering and programming languages communities had a strong concern about software reliability. In the Late 1970’s both Ada and CLU, inspired by John B. Goodenough’s paper [EXH1975], introduced another way to deal with exceptions. The exceptions were handled by the caller not where it was raised. Another feature was that the exception handling mechanisms were static instead of dynamic as PL/I was. Treatment of the exceptions in programming languages in the 1980’s influenced the software engineering research on the design of programming development environments. In the 1990’s when object-oriented programming was beginning to enter the world of software development, exceptions were considered as objected. The try-catch method is most commonly used in object-oriented programming today. For a more detailed reading see [IDE2003]. 5 3 Java Exceptions In this chapter we will look at how exceptions are implemented and used in the Java language. In Java, exceptions are first class citizens. This means that exceptions are directly supported by the language. Using Java Exceptions for what they are worth can greatly improve the robustness of a program. It helps distinguishing normal and abnormal behavior, and separates error-detection (throw) from error-handling (try-catch). In order to take advantage of Java’s flexible error-handling system, we need to understand how it works and some of the design decisions Sun did when making the exception handling mechanisms. 3.1 Exceptions as objects “With exceptions as objects, you have the power to encapsulate an unlimited variety of functionality specific to the problem.” - http://www.churchillobjects.com/c/11012k.html Java exceptions are objects. This is a relatively new way to represent error handling. C++ and Java are the only well known languages today that have exceptions as objects. Older languages use labels or have no real support for exceptional behavior. One example is Virtual Basic’s awkward ON ERROR GOTO functionality. Java exceptions are sometimes called throwables because raising an exception in Java is done with the throw keyword. Throwable (interface) Exception Error IOException ThreadDeath SQLException VirtualMachineError RuntimeException OutOfMemoryError NullpointerException ArrayIndexOutOfBoundsException Figure 3.1: Java API Exception Hierarchy 6 Figure 3.1 shows the fundamentals of Java’s Common APIs exception hierarchy. Inheriting from the Throwable interface, the Exception and Error classes serve quite different purposes. While both signal an exceptional behaviour, the Error class and its subclasses are used mainly by the Java Virtual Machine to indicate severe (and often unrecoverable) errors. The Exception branch is more commonly used. Most notable is the RuntimeException class with subclasses like NullPointerException, ArrayIndexOutOfBoundsException and ArithmeticException. These exceptions are usually the result of bad programming, and are not checked at compile time. See section 3.6 for more information about Runtime Exceptions. Exceptions that are not Runtime Exceptions are called Checked Exceptions (see section 3.5). When you use a method that throws a Checked Exception, you are required by the compiler to declare how the program is to respond, should the exception occur. Most checked exceptions are thrown by the programmer, and not by the virtual machine. These usually signal an error stemming from outside the program, for instance bad data by the user or failure to communicate with a database or another machine. 3.2 The throw statement When a situation occurs that the current class or method can not or will not handle, an exception is thrown. If the exception isn’t caught within the scope of the method, it will be thrown to the calling method. If this method doesn’t catch it either, it will be thrown to the next calling method – and so on, until the exception is caught or the program terminates with an error message. See Figure 3.2 for an illustration. Main Method Call Catch Method 1 Call Re-thrown Method 2 Exception Figure 3.2: Two method calls, an exception and a catch statement This system helps separate error detection from error handling. One method discovers the error and signals this by throwing an exception. This ends its responsibility regarding the situation. Any method in the chain of calls that is able to handle the exception can do so, or the user will be presented with an error message and a stack trace. 7 3.3 The try-catch construct The try-catch statement is Java's way of separating normal code from error-handling code. When a programmer suspects that a block of code might generate an exception, it is placed inside a try block. This is followed by one or more catch statements, each containing the code to be executed if the matching exception is thrown. See Example 3.1. Example 3.1 – The try-catch construct try { // code that might throw an exception } catch (OneException e1) { // code for handling OneException } catch (AnotherException e2) { // code for handling AnotherException } The catch statements are checked against the thrown exceptions in the order they are declared. 3.4 Cleaning up with finally Once an exception is thrown, the rest of the code in the try block is skipped. See Example 3.2. If an IOException is thrown in the third line, the file will not be closed in line four. Example 3.2 – Writing to a file without finally 1 2 3 4 5 6 7 try { file.open(); file.write("something"); file.close(); } catch (IOException ioe) { GUI.alertUser("Could not write to the file." ); } A better way to handle this is with the finally statement. The code contained within the finally block will always run, whether any exceptions have been generated or not. See Example 3.3. The file will be closed in line 7 independent of what happens inside the try block. Example 3.3 – Writing to a file with finally 1 2 3 4 5 6 7 8 try { file.open(); file.write("something"); } catch (IOException ioe) { GUI.alertUser("Could not write to the file." ); } finally { file.close(); } 8 The only thing that can prevent a finally block from executing is a call to System.exit, since this will shut down the virtual machine. 3.5 Catch or declare Unless a thrown exception is a subclass of RuntimeException, the Java compiler requires that all exceptions are caught or specifically thrown by the method. This means that if you call a method that throws an exception, you are required to declare how your program will respond if that exception occurs. Either you throw the exception further up the call stack, or you catch it and deal with it. Example 3.4 – Catch or specify /* Reads the contents of a textfile. If the file does not exist, * creates an empty file and returns an empty string. */ public String readFile(String filename) throws IOException { String fileContents = ""; FileReader filereader = null; try { filereader = new FileReader(filename) ; } catch (FileNotFoundException notfound) { new File(filename).createNewFile(); } if (filereader != null) { BufferedReader reader = new BufferedReader(filereader); while (reader.ready()) { fileContents += reader.readLine(); } } return fileContents; } Take a look at Example 3.4. The FileReader constructor can throw two checked exceptions, IOException and FileNotFoundException. One is caught and handled by the method. The other, IOException, is specifically declared to be thrown by the method. Had this not been specified, it would result in a compiler error. Likewise, another method calling this one will have to catch the IOException or declare that it throws it further. 3.6 Runtime exceptions As opposed to the exceptions in section 3.5, the ones inheriting from RuntimeException are not checked by the compiler. Runtime exceptions are problems detected by the runtime system. Common exceptions of this type are NullPointerException (trying to access an object through a null pointer reference), ArithmeticException (e.g. division by zero) and ArrayIndexOutOfBounds (trying to access an element in a list with an index that is either too large or too small). These exceptions can be numerous, possibly occurring anywhere in the code. For one, this makes it prohibitive to check the exceptions at compile time – and would require that the 9 programmer catch these exceptions everywhere they could occur. It also would make programmers spend all their time producing extremely cluttered code that did nothing. 3.7 Chained exceptions A new feature in Java Development Kit (JDK) 1.4.0 is known as chained exceptions. In short, exception chaining stores all exceptions from the initial cause up to the last. Throwables can now contain the throwable that caused them. One cause can have another cause, thus giving a causal chain. You can iterate through the chain, all the way back to the initial exception. Initial cause EXCEPTION EXCEPTION getCause() EXCEPTION getCause() Figure 3.3: A Chain of Exceptions According to Sun, "it is common for Java code to catch one exception and throw another" [SUN1]. The problem with this is that the second exception will lose information contained in the first. This makes both bug finding and recovery attempts harder. When the initial cause is lost, the programmer must spend time trying to dig up what really happened. Chained exceptions will let you know not only the initial cause, but also every exception that was triggered on the way up. This knowledge can also be used effectively inside the program. Imagine a method doing this: Example 3.5 – Chaining exceptions try { // code } catch (FileNotFoundException cause) { // do something throw new FileNotAvailableException(cause); } catch (AccessDeniedException cause) { // do something throw new FileNotAvailableException(cause); } It is now possible to find out why the file was not available, without forcing the calling method to catch two separate exceptions. It might be used like this: Example 3.6 – Using chained exceptions try { // code calling the method in Example 3.5. } catch (FileNotAvailableException exception) { // do something regardless of cause if (exception.getCause() instanceof AccessDeniedException) { // do something more 10 } } It has always been possible to chain exceptions, either by making your own, or with a wrapper class. Many developers have seen the usefulness of chained exceptions, and made their own non-standard approaches. With JDK 1.4 this is no longer necessary. 11 4 Current Trends As systems have grown more complex and customers’ demand for robustness has increased, more people have turned their attention to exception handling techniques. While exception handling in crude forms has existed since the mid 1960’s, the advanced exception handling techniques of today is a new field of research. There is no generally accepted agreement on how exceptions are to be used. However, more is written about exceptions every day, along with debates about what exceptions really are for [EJE2003] and if checked exceptions are a good idea or not [DJN2003]. In this chapter we have gathered the recent information to make an overview of the current trends in exception handling today. 4.1 Designing for robust Java programs with exceptions According to [EHO2003], the development of modern object oriented systems tends toward higher complexity and an increasing number of exceptional situations. Exception handling techniques are employed to deal with these problems, but there are some serious issues when applying them in practice that have yet to be solved. Exception handling is often not addressed at the appropriate phases of system development. Exception code design and analysis is complex, and methodologies supporting the proper use of exception handling are few and far between. We will take a look at these challenges in this section. Addressing exception handling early Exception handling is an important part of a large system. Handling exceptions in a good way helps debugging during development and increases the robustness of the finished product. For most projects these advantages are too important to haphazardly leave the exception handling up to each individual programmer. It is the system architect’s job to define how the system should respond to undesired events. [EAE2003] writes: Inexperienced programmers tend to invent and implement ad-hoc repair measures. If this happens at a large scale, the system is doomed to failure. Still, for many projects the exception handling is pushed too late into the development process. Decisions that normally belong to the architectural or detailed design phase are done during implementation. Java’s creator James Gosling says this is a culture thing, and blames some of these problems on the way Java and exceptions are taught [FAE2003]. Exception handling is the last mechanism to learn, and the last mechanism to use. A lack of notations and patterns According to [EHO2003] there is a lack of methodologies supporting the proper use of exception handling. This helps to explain why exception handling is not always considered during the design phases of development. With no tools available, it is easy to forget the task. With no standard notation, you need to create and agree upon a notation to include exception handling in your diagrams. With no support for exception handling in the standard notations used, you are breaking the standard to include them. 12 The Unified Modelling Language (UML) is the industry-wide standard for modelling software architectures, and up until recently you could not describe the logic flow of exceptions with UML. This has been mended in UML 2.0 [UML2003]. An exception handling notation has been introduced, and hopefully this will help developers account for exceptions when designing their software. A short introduction to this notation will be presented later in this chapter. Along the same lines, there are few available patterns for the use of exceptions. Patterns are well-known techniques, abstractions of common design occurrences, which document specific reoccurring problems and solutions. For thorough information about patterns, see [GAM1995]. Searching the large collection of patterns for Java [PIJ2001] we found not a single pattern for using exceptions, although there were a few for increasing program robustness. A pattern called safety facades was introduced by [EAE2003]. In the next section we will give a brief explanation of it. Safety facades A special case of the facade pattern [GAM1995], called safety facades, is presented in [EAE2003]. At the time of writing, this is to the best of our knowledge the only pattern that deals with exceptions at an architectural level. We give a short introduction to the pattern here. A call between two components can be safe or unsafe. An unsafe call is done directly, with no exception handling in between. These two components form a risk community. If one fails, so does the other. A risk community consists of all components that are linked by unsafe calls. Safe access to risk communities is provided by safety facades. The safety facade is responsible for exception handling. Exceptions within the risk community fly over all involved components and are finally caught by the safety facade. This means that all components within a risk community share the same exception handling mechanism. It is important to design the risk communities carefully. A system will have several layers of safety facades. Exceptions are handled by the nearest safety facade. The exceptions are resolved there or propagated to the next safety facade. The outermost safety facade will shut down the program if unable to deal with the exception. Defining safety facades and risk communities at the architectural design phase has several advantages. The system architect gains control over the big picture of exception handling in the system. Programmers know which components are responsible for taking care of which exceptions. Modelling exceptions in UML 2.0 Presented in UML 2.0 is a mechanism for describing how exceptions are handled, especially for describing the logic flow. With this notation we can use UML 2.0 to model exceptions with the use of class and interaction diagrams. The new version of UML also includes new developments in the activity diagram that deal with exception handling. An exception handler 13 in the activity diagram specifies something to execute when a given exception occurs during processing. Figure 4.1: Model of exception handling notation in UML 2.0 Figure 4.1 shows what a model of exception handling notation looks like in UML 2.0. Protected Node in the figure represents the try block in an activity diagram. The catch block represents the handler body. If an exception occurs, the set of handlers is examined for a possible match. If a match is found, the handler body is invoked and the handler catches the exception. If the exception is not caught, the exception is propagated to the enclosing protected node if one exists. For an example of using activity diagram for modeling exception handling in UML 2.0 see figure 4.2. Read [UML2003] for additional explanations. Figure 4.2: The URL viewer example, from [UML2003] With the new mechanism for exception handling in UML 2.0, the impact of using activity diagrams to communicate proper use of exception handling in the design of a system is huge. Most projects should consider the benefits of using activity diagrams in system modeling. Even though modeling with activity diagrams promotes functional programming, this is a good way for describing the design of exception handling. Activity diagrams have roots in flow charting and are often associated with functional decomposition. Since it was believed that they promoted functional programming, activity diagrams were not favored in the software system community. It is important to balance any modeling activity (like use case modeling and activity diagrams) that promotes functional programming with activities that promote good object-oriented design. 14 Interface versus implementation Ideally a method can be seen as an abstract contract, the interface, and the code as one way to implement that contract. However, many programmers forget about the interface and just think in terms of the code. It is then easy to forget that the throws clause is part of the interface. An implementation change may result in a method being called that throws a new checked exception. That does not mean that the exception should appear in throws clauses up the call stack. If the exception is just added to the throws clause without any further ado, the implementation is driving the interface. A method’s throws clause should be considered at the abstract contract level. Consider a method loadUserPreferences(int userid). The purpose of the method is to find a set of user preferences based on a given user identification number. Depending on how this method was implemented, it could throw such exceptions as SQLException, FileNotFoundException, MalformedURLException or SocketException. Throwing all these exceptions isn’t sensible, and throwing only one of them binds the implementation prematurely. This is where exception translation [EFJ2001] comes in handy. The low level exception, for instance SQLException, is caught by the method. The method chains the old exception to a new high level exception, for instance UserPreferencesNotAvailableException, and throws it. If the implementation changes at a later date, the method interface stays the same, and the caller of the loadUserPreferences method is presented with an exception that is appropriate to the abstraction level. 4.2 Best practices for using exceptions We have done some research about tips and guidelines available today and will present them in this chapter. The following tips and guidelines are taken from various articles, forums on the internet and several books: [EFJ2001], [PRJ1999], [DRJ2000], [EJA2001] and [EIJ2000]. The tips and guidelines will be divided into three parts where the first part will show you when you should use exceptions. Next we will advise you on the distinction in the use of checked and unchecked exceptions. Finally, the gathered tips and guidelines will show you how best to use exceptions. When should I use exceptions? Programmers do not always know when to use exceptions and when not to use them. When exceptions are misused, the programs will perform poorly, confuse users and will be harder to maintain. First of all, you should only use exceptions, as the name implies, for exceptional conditions. If your method encounters an abnormal condition that it cannot handle, it should throw an exception. Avoid using exceptions to indicate conditions that can reasonably be expected as part of the typical functioning of the method. Never use exceptions for ordinary control flow. 15 Bad idea Example 4.1 – Flow control Good idea try { int i = 0; while (true) a[i++].f(); } catch(ArrayIndexOutOfBoundsException e) { } for (int i = +; i < a.length; i++){ a[i].f(); } Consider Example 4.1 where exceptions are being abused in a horrible way. Instead of using a for-loop to go through all the elements available in the array and then exit, the code to the left uses a while-loop. The while-loop terminates by throwing, catching and ignoring an ArrayIndexOutOfBoundsException when it attempts to access the first array element outside the bounds of array. Once you have used exceptions you should never hide them. If you hide an exception then it would be nearly impossible to trace back to where the exception has occurred and find the original cause of the method’s failure. Furthermore you should never ignore an exception and hope it will go away, because it won’t. A thread will terminate if exception is not caught and there will be no record to show that exception has occurred. Take a look at Example 4.2 where the following output is: “In main, caught: Third Exception”. First and Second Exception will be ignored since //1 is hidden by exception thrown at //2. Exception at //2 is again hidden by exception at //3. One solution to this is to save a list of all exceptions generated and throw an exception that holds a reference to this list. Example 4.2 – Hidden exception class Hidden{ public static void main (String args[]){ Hidden h = new Hidden(); try { h.method(); }catch (Exception e){ System.out.println(“In main, caught exception: “ + e.getMessage()); } } public void method() throws Exception { try { throw new Exception(“First Exception”); }catch Exception e){ throw new Exception(“Second Exception”); }finally{ throw new Exception(“Third Exception”); } } } //1 //2 //3 Even though using exceptions can help you make your code easier to read by separating functional code from error-handling code, inappropriate use can make your code harder to read. This means that you do not have to use exceptions for every error condition. You should consider when it is intuitive to use exceptions and when not to use exceptions. 16 Bad idea Example 4.3 – Intuitive use of exception Good idea int data; MyInputStream in = new MyInputStream(“filename.ext”); while(true){ try{ data = in.getData(); } catch(NoMoreDataException e){ break; } } int data; MyInputStream in = MyInputStream(“filename.ext”); data = in.getData(); while(data!=0){ //do something with data data = in.getData(); } new Take a look at Example 4.3 where it is more intuitive, easier and faster to return zero rather than using an exception. Besides, using exceptions are more expensive in terms of the resources needed. The more exceptions you create the more handling you need to do and exception handling mechanism demands a lot of resources. Which exceptions class should I use? There are a lot of standard exceptions provided by Java. In this section we will concentrate on the distinction in the use of checked and unchecked exception, also known as runtime exception. The main rule for using checked exception is to use them for conditions from which the caller can reasonably be expected to recover. If you are throwing an exception for an abnormal condition that you feel client programmers should consciously decide how to handle, throw a checked exception. When using a checked exception is inappropriate, it is better to use unchecked exception. One technique for turning checked exception into unchecked exception is to break the method that throws the exception into two methods, the first of which returns a boolean indicating whether the exception should be thrown. Even though it is not always appropriate to do such transformation, it will make the API more pleasant to use where it is appropriate. Example 4.4 //Transform the calling sequence from : try{ obj.actio(args); } catch(TheCheckedException e) { //Handle exceptional condition …………… } // to this : if (obj.actionPermitted(args)) { obj.action(args); }else { //Handle exceptional condition …………… } You should use runtime exception to indicate programming errors. When using unchecked exception the compiler does not force the client programmers to either catch the exception or declare in a throws clause. All unchecked throwables you implemented should be subclass from RuntimeException either directly or indirectly. 17 How do I best use exception? Favor the use of standard exceptions As you may have noticed, Java provides a lot of standard exceptions and you should favor the use of these exceptions instead of creating new ones. There are many benefits for using preexisting exceptions. The main argument for using them is that it will make your code easier to read and use, since programmers will be dealing with exceptions that they are familiar with. Besides, using preexisting classes and fewer classes mean a smaller set of classes to deal with and less time spent loading them. If you find it inappropriate to use preexisting exceptions or the exceptions you make are more convenient for your code, then you are better off creating new ones. Catching exceptions When catching an exception you should try to gather as much information about that exception as possible. The more specific exception you throw the easier it is to handle. This means that you should try to avoid catching superclasses as Exception and Throwable if you can help it. Catch a superclass only if you are certain that all the exceptions you will catch by doing so, have the same meaning to your code. Always catch and handle RuntimeException or Error separately from other Exception or Throwable subclasses. Do not add exception handling at the end of the development cycle. Never create objects from the Exception class because an exception handler can not distinguish between the exceptions those objects represent. Remember to create exception objects from appropriate Exception classes. Throwing exceptions You should consider the type of exceptions to be thrown by a method from the perspective of the method’s caller rather than the class’s own perspective, when writing throws clauses. If you throw a bad type, the caller will not be able to handle the exception sensibly and might have to pass it on to its caller or the user. After finding out which type of exception to throw, you have to consider the object’s state to be thrown. The object has to return to a valid state before throwing an exception. The purpose of catching an exception is to try to recover from the problem that occurred and keep the system running. If you leave the object in a bad state the code will very likely fail anyway, even when the exception has been handled. Consider Example 4.5 where at //1 the method increases a counter for the number of objects in the list. If an exception is thrown after //1 then the object will be in an invalid state because the counter, numElements, is incorrect. The way to solve this problem is to move the counter at the end of the method after //2. Example 4.5 class Foo{ private int numElements; private MyList myList; public void add(Object o) throws SomeException{ //… numElements++; if(myList.maxElements() < numElements) { //Reallocate myList //Copy elements as necessary //Could throw exceptions } myList.addToList(o); //Could throw exception } } //1 //2 18 If you have to deal with failure that occurs inside a constructor, throw an exception object from that constructor. Do not attempt to throw objects from any class apart from Throwable or a Throwable subclass, otherwise the compiler will report an error. The compiler will also report an error when a method attempts to throw a checked exception object but does not list that object’s name in the method’s throws clause. You should throw an exception and never throw errors. Try/Catch/Finally The catch keyword must appear immediately after a try block’s closing brace character. Placing try/catch blocks inside of loops can slow down execution of code as you can see in Example 4.6. Method2, which puts the try block outside the loop, has proved to be more efficient during execution than method1. Bad idea Example 4.6 Good idea public void method1(int size){ public void method2(int size){ int[] ia = new int[size]; for(int=0;i<size;i++){ try{ ia[i]=I; } }catch (Exception e){ //Exception ignored on purpose } int[] ia = new int[size]; try{ for(int=0;i<size;i++){ ia[i]=I; } }catch (Exception e){ //Exception ignored on purpose } } } Do not issue a return, break or continue statement inside a try block. If you cannot avoid this, be sure the existence of a finally does not change the return value of your method. If a finally block exists, it is always executed. The main benefit of using finally is to avoid resource leaks as you can see from Example 4.7. If an exception occurred in a try block, the close call will never be reached unless you have finally. Bad idea Example 4.7 Good idea class WithoutFinally{ public void foo() throws IOException //Create a socket on any free port ServerSocket ss = new ServerSocket(0); try{ Socket socket = ss.accept(); //Other code here… }catch(IOException e){ ss.close(); throw e; } close(); }} Create useful error messages class WithFinally{ public void bar() throws IOException //Create a socket on any free port ServerSocket ss = new ServerSocket(0); try{ Socket socket = ss.accept(); //Other code here… }finally{ close(); } } 19 It is clearly an easy task to write throw or catch clauses, but writing good error-recovery information is more difficult. To write a good throw or catch clause is another problem. We will see here the benefit of writing good error-recovery information. Take a quick look at Example 4.8 where you can see a hard-code text string for throwing an exception. As you can see, this is a very commonly and undesirable way to build an error message. Most developers commit this type of mistake of placing explanatory messages with source code. Example 4.8 if (!file.exists()){ throw new ResourceException(“Cannot find file” + file.getname()); } The source code is not a place for explanatory messages, which are really more related to documentation. As long as the error messages are hard-coded among source codes, they will be created and owned by developers, often to the detriment of usability. Developers should not be writing error messages even more than they should not be writing documentation. How often have you seen error messages that are utterly incomprehensible except to the person who wrote the code? 20 5 Analysis Tools In section 5.1 we take a look at JeX, a static toolkit for analyzing exception flow in Java. Section 5.2 describes Jikes Bytecode Toolkit, a class library written entirely in Java that enables Java programs to create, read, and write binary Java class files. Finally in section 5.3 we try out Teamstudio Analyzer for Java, a best-practices audit tool that uncovers errors in Java code. 5.1 JeX JeX is a static toolkit for analyzing exception flow in Java. It was developed by Martin P. Robillard and Gail C. Murphy in 1998 at the Department of Computer Science at the University of British Columbia. About JeX JeX consists of four components: the parser, the Abstract Syntax Tree (AST), the type system and the JeX loader. The AST is used to identify the structures of classes and exceptions within a method or constructor. It also evaluates the expressions and invocations that may cause an exception to be thrown. The type system provides the AST with a list of all types that override a particular method. The parser component uses this for analyzing the exceptions in the code which JeX analyze. JeX loader loads the various components and connects them. See [AEF1999] for additional information. In order to use JeX, the user must specify a list of packages, a path to search for JeX files and a Java source code file in a configuration text file. JeX uses the following template (see Table 5.1). JeX template: <jexpath> The root directory where JeX is installed. <package> Package name to include for class-hierarchy analysis. Class hierarchy analysis is used to conservatively determine which method bodies can be executed for each method call. As many package descriptors as desired can be used. <genstub> Package name to generate stubs for at the beginning of an analysis. <dir> Directory where the Java source file/files are to be found. <file> The Java source file which JeX is supposed to analyze. Table 5.1: Template for JeX configuration file Additional information about JeX may be found at [JEX1]. JeX at work We’ve tested JeX with some small programs, one large program and an example program that is available at JeX’s homepage. Before we were able to test JeX with any of the programs, we 21 had to configure JeX and generate Java packages for JeX class-hierarchy. First of all we tested the example program that already had the configured text file that JeX uses. JeX worked fine in this example and there were no difficulties. The JeX-report that was generated by JeX only showed that there was one warning, which was expected, see figure 5.1. Figure 5.1: The content of JeX-report. Then we tested JeX on a program which was developed in 2000 by a group of students at the Norwegian University of Science and Technology. The reason for choosing this program was that it included a lot of exceptions. It was also programmed in Java and one of the authors of this report developed program. Here we encountered several problems. One of the problems was that JeX was developed with Java version 1.3 while the test-program was developed in Java version 1.4. Many packages included in Java 1.4 were not included in Java 1.3. Another problem was to set up the configuration file for JeX. The documentation found at JeX homepage was not clear enough to allow us to understand whether the set up was right or not. In the end, we gave up trying JeX with that program since we could not get JeX to work with it. Then we chose a smaller program to try JeX on. This time all the packages needed for generating stubs in order to test out JeX, worked on this program. Surprisingly JeX took an extremely long time to go through a small program with only three Java classes. The report showed us a lot of warnings which shouldn’t appear. The warnings were about methods it didn’t find even though they were there, see figure 5.2. We were not able to understand the reasons why. Beyond this it seemed that the program was fine and JeX reported no other errors. We expected JeX to give advice on how to optimize our exceptions in the code. It did not. 22 Figure 5.2: JeX-report for our small program. Conclusion JeX is an old toolkit that will probably not work with most programs developed by Java 1.4 and above. It may work if the packages used during development were included in Java 1.3. JeX has not been updated since 8 March 2002 which means it’s quite out of date. The documentation should have been better and easier to read. The report which JeX generates is not easy to understand and did not show as much useful information as we had hoped. JeX is slow even when the program is small. For a huge program it will probably take JeX hours to finish. One thing that is positive about JeX is that the JeX-files which JeX generates from Java source file are very interesting. Here we can find all of the exceptions that that method or constructor in the Java source file may encounter. This will help the developer to catch the right exception when it is thrown. JeX is best used during development since the user can insert the necessary package much easier than when the program is finished. We have found that it took us a long time to go through the source codes and insert packages for every file. JeX also makes it easier to change the code during development than changing finished products. 23 5.2 Jikes Bytecode Toolkit Jikes Bytecode Toolkit is developed by Chris Laffra, Doug Lorch, Dave Streeter, Frank Tip and John Field at alphaWorks, IBM. It is a work in progress, but released versions have already been used in several projects. About Jikes Jikes Bytecode Toolkit is a class library, written entirely in Java, which enables Java programs to create, read, and write binary Java class files. It also lets the programmer query and alter a high-level representation of the collection of the class files, and relations among them. Jikes provides a logical representation of the class file. Classes have methods, methods have code and code contains bytecode instructions. In Jikes these are represented by objects that can be read and manipulated. From http://www.alphaworks.ibm.com/tech/jikesbt: Jikes Bytecode Toolkits’ model is quite rich and provides a number of relationships, such as an easy way of finding each instruction where a given class is allocated, where a given method is invoked, and where a given field is accessed. It records which methods override which others, which will be inherited from other classes, and even which method implements a method declared in a given interface (even if via inheritance). Jikes and Exceptions Using Jikes, a programmer can browse a class' hierarchy to find which methods throw which exceptions, and which methods catch them. This isn't unique to Jikes. What makes Jikes special is the added possibility of inserting code in the class files without changing, or even needing, the source code. While the inserted code certainly can change the behavior of the program, this is not our intention here. Every throw, throws, try, catch and finally statement could have a log writer added. This would allow the program's execution and exceptional behavior to be logged and analyzed, without cluttering up the code or intruding on the program in any other way. Needless to say, this is work of much larger magnitude than this project. It might be an interesting work for a diploma. Since the analyzer would be independent of the source code, it could be applied to a many different programs. To best discover the behavior, the modified program would have to be inserted into a real situation - since that is where the exceptions are going to show up. Conclusion Jikes is an extension to the functionality of the Java programming language, and not an executable program. As such, it is not what we were looking for. However, Jikes may prove useful for creating such a program. It removes the need to write a code parser. Class files can 24 be searched for exception-handling. It even lets you add functionality without altering the workings of the program. An application's execution can be observed and exception handling details written to a database. 5.3 Teamstudio Analyzer for Java Teamstudio Analyzer for Java is a commercial product developed by Teamstudio, Inc. This section is based on a seven day free trial of the product and the contents of their homepage www.teamstudio.com. About Teamstudio Analyzer Teamstudio Analyzer is a best-practices audit tool that uncovers errors in Java code. It is not a standalone program, but is incorporated into different integrated development environments (IDEs). Among the supported IDEs are JBuilder, Eclipse, JDeveloper and Sun ONE. Once the program is installed it will check your code against a set of best practice rules. In addition to about two hundred rules that come with the package, programmers can write their own rules and insert them into the rule set. These rules are written as Java Classes. When using the IDE after installing Teamstudio Analyzer, an additional window will open. This window contains a list of detected errors, a description and a link to the line of code in question. The rules range from brackets that aren’t closed to unnecessary imports to missing or incomplete Javadoc. Teamstudio Analyzer and Exceptions Some breaches of best-practices for using exceptions are simple to detect. These are pretty much covered by Teamstudio Analyzer. More complex scenarios are a lot harder to find, and are not part of the Analyzer default rule set. Figure 5.3 shows Teamstudio Analyzer at work. Errors discovered and reported by the Analyzer are: ? ? ? ? ? Empty catch and finally blocks. Throwable, Error or generic Exception caught. Class names ending with Exception that doesn't inherit from Exception, or the other way around. Method declares that it throws an exception that isn't thrown in the code. Unnecessary catch block, the code tries to catch an exception that isn't thrown. 25 Figure 5.3: Teamstudio Analyzer detects code violations. Conclusion While Teamstudio Analyzer doesn't have much support explicitly for exceptions, it supports writing your own rules (as Java classes) and integrating them into the analysis and reporting engine. However, since Analyzer is a commercial product, this probably isn't of particular interest to anyone that doesn't already use Teamstudio's suite of tools. 5.4 J2EE Code Validation for WebSphere Studio IBM WebSphere Studio is a suite of tools for development needs, including Web development, enterprise-scale application development and development for wireless devices [IWS2003]. This software includes several validation tools, but the most important one of all and most relevant to this report is the J2EE Code Validation Preview. About J2EE Code Validation Preview J2EE Code Validation Preview for WebSphere Studio (J2EE Code Validation) is a tool that automatically detects common error patterns and coding violations of best practices in Web applications [JCV2003]. IBM has categorized error patterns and coding violations into several, broad rule categories. J2EE Code Validation uses specialized analysis code for each rule category, and in this technology preview, detects over four hundred violations. These violations are very hard to detect by conventional means and they cause serious performance degradation and system outages in real-world production environments. J2EE Code Validation integrates with WebSphere Studio Application Developer and helps developers identify and correct code defects early in the development cycle and before applications reach production. 26 J2EE Code Validation does not replace traditional code analysis tools. It augments them by conducting deep, static analysis of applications and by constructing all possible paths and all possible object manipulations. This type of analysis is more thorough and is capable of looking at a different set of problems. For example, using data flow analysis, J2EE Code Validation can find many common cases of rare conditions which source scanning tools cannot. J2EE Code Validation and Exception J2EE Code Validation does static program analysis (control and data flow) to check for many bad coding patterns. The analyzer points out where in the program the exception handling has been implemented poorly. Bad coding patterns include multiple exceptions handled by a single catch block, catch block not catching the exact exception thrown (but its supertype) or exceptions that have been swallowed etc. Once the analysis is completed and if there are any coding violations, a list of the result with explanations will pop up in the task bar. You can also see the analyzed steps that have been taken during the analysis in the status bar. The user can click on the specific item in the task bar for a detailed explanation of the coding violation. In the explanation box there will be some highlighted text which you can click on. When you click on the text, the source view will show you which part of the code contains the coding violation. You can also choose to see the paths that lead to the coding violation in the task bar. A tree structure will show you the paths in the application. When you click on it you can see the source code associated with the paths, figure 5.4 shows how it looks like. Figure 5.4: J2EE Code Validation at work 27 Conclusion J2EE Code Validation is a just a plug-in for WebSphere Studio, which means that you have to acquire the commercial product from IBM. There is a possibility for downloading the trial software which is over one GB. Even though J2EE Code Validation is a neat tool for analyzing coding violation including wrong exception handling, it is more suitable for people who are already using WebSphere Studio. It takes some time to really get to know the software with its tons of functions. WebSphere Studio is Java software and it is very slow when starting up and requires at least 512MB of ram for the code violation to run. 5.5 Conclusion Although these tools are very helpful, none of them fulfill the criteria for handling exceptions in a better way. First, JeX is out of date and only creates lists of inconsistent methods. It does not suggest how you could fix them or optimize them. Both J2EE Code Validation and Teamstudio Analyzer detect bad or wrong use of exceptions, but they also do not advise you how to fix or make them better. Jikes is not a tool or a program, but is a collection of class libraries, which can help develop a Java exception analyzing program like JeX. Table 5.2 gives a summary of disadvantages and advantages of the tools we have investigated. Name JeX - - - Disadvantages Setup up own configuration file Overlook several problem with exceptions slow bad documentation Not a toolkit or program Free Includes all classes of exception - Able to insert code in class files - Works with several - Require license Teamstudio Analyzer programs - Just a tool to integrate - Detects several severe with other exception violations development program - Add new rules - Points out error at - Require license J2EE Code Validation source code - Require WebSphere - Show paths lead to code Studio violation - User must be familiar - Check precondition of with WebSphere methods Studio Table 5.2 Advantages and disadvantages with the programs we have tested. Jikes - Advantages It’s free Detect error in method Include almost all exception class 28 6 Rules In this chapter we introduce a set of rules that we have assembled throughout our work with this project. With the waterfall model as the starting point (see Figure 6.1), we have made rules for using exceptions in each phase of the model. We hope with these rules, we could help system developers to be able to make more robust and reliable systems. Even though the main bulk of the rules belong to the implementation phase, rules in preceding and subsequent phases are also important. System Requirements (1) Architectural Design (2) Detailed Design (2) Implementation (16) Testing (1) Figure 6.1 Waterfall model, with the number of rules at each phase. 6.1 System Requirements Phase SR1: Clarify time spent on system robustness. Creating a robust system takes time and resources. It is important that the developers and the customer are clear from the start how important system robustness is to the project, and thus which resources should be allotted to this end. The developers can err on both sides of this, in one case creating a program the customer is not happy with – in the other, spending too much time and money on something the customer is not asking for. 29 6.2 Architectural Design AD1: Define risk communities and safety facades. It is the job of the system architect to define how the system as a whole should react to an exceptional situation. In a project where exception handling is not considered during the architectural design phase, these important issues will be up to each individual programmer. Setting up risk communities and safety facades makes it clear for everyone which modules are responsible for which kinds of exceptions. See section 4.2 for details. AD2: Create a system wide user alert system. Centralize the handling of user alerts; otherwise you will end up with error messages spread all over the code. A good user interface demands that the error messages presented to the user are coherent and informative. To achieve this you need an easy way to inspect and revise them all, as opposed to searching through thousands of lines of code looking for that one strange error message your users have been getting. (See Code Excerpt 7.1.3) 6.3 Detailed Design DD1: For each module, identify possible emergencies. Look at which external sources the module depends on, and what would happen should they stop working. [EAE2003] defines an emergency as a situation where the programmer of the component doesn’t know what to do – there is no local help available: the component where the emergency happened is unable to solve the problem. Emergencies range from programming errors to unreachable databases and crashed neighbor systems. DD2: Define how each module should respond to an undesired event. There are certain problems that the module will encounter, some the module may be able to solve and others that are impossible to handle. You have to separate these and define which of the problems that may be solved and which the module has to pass on to the safety façade (see rule AD1). It is important to limit the module to problems which it can handle. 6.4 Implementation IM1: Do not use exceptions for control flow. It is possible to use the exception handling mechanism for control flow also during normal program flow. This makes the code harder to read, harder to analyze, significantly slower and opens up for some horrible situations if an actual exception were to occur. Exception should, as the name states, only be used in exceptional situations. IM2: Use exceptions in exceptional situations. In languages with less powerful exception handling mechanism than Java, programmers have to signal an exceptional state through other means. Some common examples are returning a null pointer, the value ‘-1’ for normally unsigned integers or the empty string. This is not necessary in Java and should be avoided. If the caller of the method is unaware of these (often undocumented) peculiars, the program will most likely start behaving in unforeseen manners. Exceptions are part of the method interface and callers are forced to take them into account. (See Code Excerpt 7.2.2) 30 IM3: Do not catch the generic exceptions. A method might throw several different exceptions, and some programmers would be tempted to simply catch the generic Exception class – especially if all the thrown exceptions are handled in the same way. Resist the temptation before you create a monster that swallows all exceptions thrown at it. All warnings that something has gone wrong will disappear or be masked as something else entirely. (See Code Excerpt 7.1.2 and 7.2.4) IM4: Do not throw the generic exceptions. By throwing the generic Exception class, you remove all power from the user of the method. The programmer has no way of knowing what has gone wrong, why it went wrong or what can be done about it. In addition the user of the method is forced to violate rule IM2, allowing for even more problems. If there is no exception matching yours in the standard exception library, create your own exception. Never throw the Throwable, Exception or RuntimeExcecption classes. IM5: Ensure the object is in a stable state after throwing an exception. Clean up with finally after a try clause so you’re always in a consistent state. Protect yourself on the way up the call stack by deallocating any used resources. When the object is unstable after the exception has been handled, it will most likely fail and throw exceptions again. Another case is when other objects depend on that object and use it in an unstable state. This might cause a myriad of exceptions. (See Code Excerpt 7.2.1) IM6: Create abstract superclasses for related sets of exceptions Regularly review existing exceptions usage and introduce abstract superclasses where the same corrective action might be used for a set of different exception classes. Callers can then name a single superclass in a catch block instead of all the individual exception classes for which a corrective action is appropriate. Making the superclasses abstract enforces the throwing of specific concrete exceptions IM7: Do not leave a catch clause empty. Only catch an exception if you can and will handle it. If you are unable to handle the exception then pass it on, but never ignore it. Leaving the catch clause empty will trick the user and the program into thinking that the exception has been handled while it did nothing. Ignoring exceptions means that you will lose vital information and you might not even register that an error has occurred. (See Code Excerpt 7.2.4) IM8: Use runtime exceptions to indicate programming errors. When an abnormal condition occurs, in situations where the client is unable to handle exception, you should use runtime exceptions. This is an error and does not force the client to catch nor to declare it in a throws clause. There is no point in handling runtime exceptions since you will not be able to recover from them. IM9: Use checked exceptions for recoverable conditions. If the user can reasonably be expected to recover from the exceptions that occur, then use checked exceptions. This forces the programmer to deal with the exceptional condition. Checked exception is also used whenever a method is unable to fulfill its contract. The contract includes preconditions that the client must fulfill and post conditions that the method itself must fulfill. 31 IM10: Be careful when returning from a try clause with finally. Always remember that the finally clause will execute regardless of what happens in a try clause. So be careful when returning from try, since finally will produce a return value which will overwrite the return value from try. To avoid this pitfall, do not issue a return, break or continue statement inside the try clause, or just make sure that finally does not overwrite the return value of the method. IM11: Use separate try blocks for statements that throw the same exceptions. When several statements throw the same exceptions, there are no ways of telling which of the statements threw the exception. If you put each statement into separately try blocks, the debugging will be much easier. When an exception occurs you will know exactly where and which statement produced the exception. (See Code Excerpt 7.1.4) IM12: Use standard exceptions provided by Java instead of creating new ones. Using standard exceptions will make it easier for others to read since they will be dealing with exceptions that they are familiar with. Besides standard exceptions are already well documented which means that you do not need to document them again. If there are no suitable standard exceptions to use then you are of course encouraged to make your own. IM13: Do not propagate implementation specific exceptions. Throw exceptions that make sense in the context of the method. Consider a method that can throw a set of exceptions. Throwing all isn’t sensible, but throwing one of them binds the implementation prematurely. Higher layers should catch lower-level exceptions and throw exceptions that are more explainable in terms of higher-level abstraction. This is called exception translation [EFJ2001]. (See Code Excerpt 7.1.1) IM14: Use exception chaining when translating an exception. Once you have decided to translate an exception, you should always chain the old thrown exception to the new exception. In this way all exceptions are chained back to the lowest-level where the first exception was thrown. This makes it easier to trace back to the source of the problem. Debugging is also easier, since the entire stack trace is available. (See Code Excerpt 7.1.1) IM15: Use exceptions when a constructor fails. When the initialization of an object fails, your only way to signal this is through an exception. Swallowing exceptions in a constructor will result in live objects that have not been initialized properly. Needless to say the behaviors of these objects are near impossible to know in advance. (See Code Excerpt 7.2.3) IM16: Document the exceptions well. Use Javadoc @throws tag to document each exception. Also document precisely the condition for the exception to occur. A good documentation of exceptions will make it easier to handle the exceptions that occur. It is usually only the developer who has written the exceptions that understands them, but with good documentation others will too. 32 6.5 Testing TE1: Use a global flag to toggle debug information. Many catch blocks will contain debug information, often in the form of ex.printStackTrace(). This should not be part of the final build. Use a simple global flag to separate debugging code from normal code. It helps developers to clearly separate debug and normal code. After testing, turn the debug information off. You need not go through the code and risk removing too much or too little. Should it be necessary you can easily turn the debugging back on. (See Code Excerpt 7.1.1) 33 7 The rules in practice In this chapter we review the code of two large real life projects, IpManager and DIAS 2. We look at how they have used exceptions in light of the rules in Chapter 6, pointing out why it is good, why it is bad, and what can be done to improve the code. When starting this project, we intended do a real test of the rules we made. We had plans to create a test, apply our rules to a large project, and then test both the original program and the modified one to see if there was any improvement. In this way we would check if the rules improved the robustness of programs, or at least that particular program. We began working on our rule set and learned more about designing robust programs with Java exceptions. After many weeks we came to realize that our testing plan was not plausible. Our single most important rule is that in order to create a robust program, exception handling has to be accounted for throughout the process. It can not be added as an after-thought. Most of our rules aren’t simple fixes – they are meant to force developers and programmers to think differently about exceptions. The application of our rules can change how the entire system works in an undesired state. We opted for the second best alternative, examining and commenting on the program code to find examples where the rules are in use or ought to be used. We hope this will help clarify some of the rules, and give a better understanding of what can go wrong when using Java’s exception handling mechanisms. 7.1 IpManager IpManager is an Open Source project founded by Mario Aparicio and Salten Kraftsamband AS in 2003. It is an application for managing networks, subnets and nodes within a company network. It helps the administrator keep track of names and descriptions of nodes and networks, and shows the number of nodes in a subnet with a certain network mask. The Code Excerpts Code Excerpt 7.1.1 public void saveDocument( Document doc, String filename ) throws SaveException { try { // method body } catch( javax.xml.transform.TransformerException e ) { if ( DEBUG ) { System.err.println( e.getMessage() ); e.printStackTrace(); } throw new SaveException( e.getMessage() ); } } 34 Code Excerpt 7.1.1 shows in a good way how the rule IM13: Do not propagate implementation specific exceptions works. The implementation bound javax.xml.transform.TransformerException is translated to a SaveException that is meaningful in the context of the saveDocument method. This allows for the saveDocument method to change data representation later, without changing its interface. The implementation is separated from the interface, and the user of the saveDocument method is presented with an exception that makes sense. The example also shows how the rule TE1: Use a global flag to toggle debug information can be used. The stack trace would only be interesting to the programmer. Using this flag the programmer can easily switch off debug information once the program is released. In addition it makes the code easier to read for others, since it is clear what is debugging code and what is normal code. To further improve this particular piece of code we have two suggestions. One is described in the rule IM14: Use exception chaining when translating exceptions; the old TransformerException should be chained to the new SaveException. This way the entire stack trace can be found for exceptions further out the call stack, and programmers interested in what caused the SaveException have a way to find out. Second, following the naming convention for exceptions suggests that SaveException should be renamed to SaveFailedException. Code Excerpt 7.1.2 try { // method body } catch ( Exception e ) { System.err.println( e.getMessage() ); e.printStackTrace(); throw new XMLParseException( "Error when reading file:" + e.getMessage() ); } Code Excerpt 7.1.2 shows a violation of the rule IM3: Do not catch the generic exceptions. This code excerpt will swallow and translate all kinds of exceptions to an XMLParseException. Should an ArrayIndexOutOfBounds, NullPointerException or similar be thrown in the method body it would not be treated in any other way than the exceptions the programmer had intended to catch. Luckily the error message is echoed to the error stream; otherwise the bug fixer would have a colossal headache ahead of him. To fix this problem, the programmer should map out what exceptions are thrown in the method body and catch those exceptions specifically. The program would function like the programmer intended, and unexpected exceptions would not be swallowed. Code Excerpt 7.1.3 } else if( foundOne == true ) { throw new IllegalNetmaskException( "The netmask is illegal. Legal values should be: \n" + "255.255.255.X\n 255.255.X.0\n 255.X.0.0 or\n X.0.0.0" + "Where X is:\n 0, 128, 192, 224, 240, 248, 252, 254 or 255" ); } 35 Code Excerpt 7.1.3 shows the violation of rule AD2: Create a system wide user alert system. The error message is hard-coded into the exception. This makes it inconvenient should you want to inspect, alter or reuse some or more of the error messages presented to the end user. Gathering all error messages in one place has several advantages. Adding support for different languages based on user location, changing the user dialog for all messages or making sure all the messages are coherent and formatted in the same way – these changes and several others are made a lot simpler by having a centralized user alert system. Code Excerpt 7.1.4 private TreeNode buildTreeFromXMLDocument( Document doc ) throws XMLParseException { //some code try { for( int i = 0; i < networks.getLengt h(); i++ ) { currentNetwork = (Element)networks.item( i ); networkN = new NetworkNode(); networkN.networkName = currentNetwork.getAttribute ( "Name" ); networkN.networkAddress = (java.net.Inet4Address) java.net.InetAddress.getByAddress( U tilityClass.extractIpAddress( currentNetwork.getAttribute( "Address" ) ) ); networkN.netmask = UtilityClass.extractIpAddress( currentNetwork.getAttribute( "Netmask1" ) ); networkN.onesNetmask = Integer.parseInt(currentNetwork.getAttribute( "Netmask2" ) ); networkN.description = currentNetwork.getAttribute( "Description" ); networkTreeNode = new DefaultMutableTreeNode( networkN ); rootTreeNode.add( networkTreeNode ); addresses = currentNetwork.getChild Nodes(); // a lot more code like this } } catch ( Exception e ) { System.err.println( e.getMessage() ); e.printStackTrace(); throw new XMLParseException( "Error when reading file:" +e.getMessage() ); } return rootTreeNode; } Code Excerpt 7.1.4 violates the rule IM3: Do not catch the generic exceptions. That was also the case for Code Excerpt 7.1.2, so the same comments apply here. However, this one also shows an example of IM11: Use separate try blocks for statements that throw the same exceptions. Note that the code within the try block in the excerpt is shortened considerably from the original source code. If an exception is raised anywhere in this massive block of code, the programmer trying to debug the code will not know where in the try block the error occurred. A common practice to work around this problem is to insert debugging lines between every line of code in the try block, to see where the execution of the code stops. 36 A better and less crude way to get this information is to use several smaller try blocks around the critical code. 7.2 DIAS 2 Distributed Intelligent Agent System 2 (DIAS 2) is part of a diploma thesis by Bård Smidsrød Moen and Anders Aas at NTNU during autumn 1999. The diploma involves design and implementation of a mobile agent architecture supporting interoperability. Central issues in DIAS are inter-agent communication, dispatching, disposing of agents and how other agent systems and clients interacts. The Code Excerpts Code Excerpt 7.2.1 try { AccessController.beginPrivileged(); DEFAULT_USERNAME = System.getProperty( "user.name"); username = System.getProperty( "username", DEFAULT_USERNAME); password = System.getProperty( "password", ""); } finally { AccessController.endPrivileged(); } Code Excerpt 7.2.1 shows the rule IM5: Ensure the object is in a stable state after throwing an exception in practice. The finally block ensures that the resource with privileged access is unlocked regardless of the outcome of the method calls in the try block. Note that the try block is not followed by a catch block. This is not necessary, and in this case the programmer has no intention of mending an error at this point in the code. The focus is on making sure that the resource is unlocked. Code Excerpt 7.2.2 public URL getAdpURL() { try { URL ret = new URL(adpName); return ret; } catch (MalformedURLException mue) { System.out.println("Could'nt retrive the ADP's URL" +mue); return null; } } Code Excerpt 7.2.2 breaks the rule IM2: Use exceptions for exceptional situations. The code translates a MalformedURLException to a returned null pointer. The caller of this method has to check if the returned value is a null pointer, but this is not part of the methods interface, and has to be documented separately. If the caller fails to check for a null pointer, the program will crash with a NullPointerException once a malformed URL is used. If the 37 caller checks for a null pointer, the decision to throw a new exception or fix the situation has to be made. The same functionality is attained by simply propagating the MalformedURLException. Exceptions are part of the method interface, so the possibly undocumented feature of a returned null pointer is avoided. If the caller of getAdpURL can not mend the situation, that method throws it further. The other solution would result in the first exception being thrown, caught immediately afterwards, translated to a returned null pointer, just to see a new exception thrown one step up in the call stack. This is not only awkward and hard-to-read code, it is very slow code. Code Excerpt 7.2.3 public AMPInfo(String strAmpInfo) { XmlDocument xmlInfo = new XmlDocument(); try { // large constructor body } catch (SAXException se) { System.out.println("AgentInfo::AgentInfo:" + "The agentID could'nt be parsed \n"+se); } catch (IOException ioe) { System.out.println("AgentInfo::AgentInfo:" + "An IO exception has been thrown \n"+ioe); } } Code Excerpt 7.2.3 shows an example of breaking the rule IM15: Use exceptions when a constructor fails. If a SAXEception or an IOException is thrown in the constructor body, the AMPInfo object will not be properly initialized. A warning is echoed to System.out, informing the user of the problem if the user is monitoring that channel. The program has no way of knowing something went wrong during the initialization of the object, and will continue as if nothing was wrong. Putting it a bit flippantly, a veritable exception generator has been let loose in the system. One or more NullPointerExceptions is likely to occur. Worse still is if the incomplete object does not trigger an exception, but is used by other subsystems in good faith, feeding them incoherent and false data. Code Excerpt 7.2.4 public void parseMessage(String str) throws ParseException { try { KQMLStreamTokenizer parser = new KQMLStreamTokenizer( new BufferedReader(new StringReader(str))); parseMessage(parser); } catch (Exception e) { e.printStackTrace(); } } Code Excerpt 7.2.4 violates the rule IM7: Do not leave a catch clause empty. A method that only dumps the stack trace to System.out is empty for all practical considerations. No matter 38 what happens further down the call stack, this method will print the stack trace and end the crisis; fixed or not. Imagine a program feeding tens of thousands of messages into this method while an underlying system is down. The System.out channel will be flooded with messages, and afterwards the program continues as if everything worked out as planned. It also shows yet another example of the rule IM3: Do not catch the generic exceptions (see Code Excerpt 7.1.2 for a discussion), but this one has an interesting twist. The method declares that it throws a ParseException, but thanks to the catch clause that accepts all kinds of exceptions, this method will never throw that exception. In fact, it will never throw any kind of exception. This is obviously an oversight by the programmer, but it would never have been missed if the programmer was not tempted to catch the generic exception, because then the compiler would have complained. There might seem to be an easy fix to this problem. Simply throw a new ParseException in the catch block. See Code Excerpt 7.2.5 for why this is not such a good idea. Code Excerpt 7.2.5 public void parseMessage(KQMLStreamTokenizer parser) throws ParseException { try { // method body } catch (Exception e) { e.printStackTrace(); throw new ParseException(); } } The method in Code Excerpt 7.2.5 is the one being called in line five in Code Excerpt 7.2.4. Notice that this catch block does in fact throw a ParseException. Any and all exceptions that are thrown in the method body will be translated to a ParseException, and all information contained in the old exception is lost. At the very least the rule IM14: Use exception chaining when translating exceptions should be followed, chaining the old exception to the new one. Now take a look at Code Excerpt 7.2.4 again. What happens if the easy fix mentioned there (making it throw a new ParseException) is applied? The program will throw the first exception, catch it along with all other general exceptions, and then throw a new, nearly identical exception. This is slow, it is cumbersome, it makes bug finding harder, and it is not necessary. 39 8 Conclusions and Further Work For a long time, developers have ignored and misused the powerful error handling mechanism that Java offers. Being a third generation programming language, Java is one of the few programming languages that provide very good exception handling. Developers should take advantage of this and make better use of them and using them more often. Many developers tend to avoid using exception or use them as little as possible because they lack the knowhow. Other developers consider exceptions as afterthought or an as-you-go addition, which often results in difficult debugging and unstable programs. Exception handling is an integral part of a robust system. It should not and cannot be added as an afterthought. The biggest mistake for most developers is that they deal with exceptions far too late in the development. Not until they have reached the implementation phase do they become aware of how important a role exception handling plays in the development process. To build a robust and reliable system, developers need to take exception handling into account throughout the entire development process. It is important that developers understand and know how to use the exception handling mechanisms. This information is not always obvious and there is great potential for improvement in this field. Not only do they need to know how to use this powerful mechanism, they also have to understand how important a role it plays in the development process. Wrong use of exceptions can lead to severe problems, like bug ridden, unstable programs. We present in this paper a set of rules to help developers reach their goals and improve their programs. We believe that developers following these rules will develop more robust and reliable systems. Developers are forced to take exceptions into account already at the system requirements phase. The sooner exceptions are taken into consideration, the easier they are to implement and to manage. The rules at each stage, from system requirements to testing, are equally important to follow. We inspected two real life projects in Chapter 7, and saw that there was definite room for improvement in the exception handling of both projects. If the projects were to abide fully by the rules in this paper, large parts of the code would have to be completely rewritten. This made it not only prohibitive for us to test our rules formally, but it emphasizes the importance of designing with exceptions in mind from the start. Further Work We were not able to formally test our rules by modifying an existing program (see Chapter 7). This makes it very interesting to see how the rules fare in a case study. An idea is to follow two groups of developers during their process of developing a program. One group gets a presentation of this paper and the rules, and is encouraged to comply with the rules. The other group is not asked to do this, but is simply watched. Following the two groups through all the phases of development, and looking at the robustness of the final products, might give some interesting insights into what part exception handling plays in the stability of a program and if the rules actually help developers create better programs. 40 9 References [SUN2001] Java Community Process Maintenance Review for J2SE 1.4. http://java.sun.com/j2se/1.4/jcp/j2se-1_4-mr_docs-spec/core_libraries.html, 2001. [JEX2002] Jex, A Tool for Analyzing Exception Flow in Java Programs, http://www.cs.ubc.ca/~mrobilla/jex/, 2002 [EHO2003] A. Romanovsky, C. Dony, J. L. Knudsen, A. Tripathi: Exception Handling in Object Oriented Systems: Towards Emerging Application Areas and New Programming Paradigms. ECOOP, 2003. [FAE2003] B. Venners: Failure and Exceptions, A Conversation with James Gosling. http://www.artima.com/intv/solid.html, 2003. [GAM1995] E. Gamma, Helm, Johnson, Vlissides: Design Patterns. Addison-Wesley, 1995. [EAE2003] J. Siedersleben: Errors and Exceptions – Rights and Responsibilities. ECOOP, 2003. [EFJ2001] J. Bloch: Effective Java Programming Language Guide. Addison-Wesley, 2001. [IWS2003] IBM WebSphere Studio, http://www106.ibm.com/developerworks/websphere/zones/studio/bigpicture.html [JCV2003] J2EE Code Validation Preview for WebSphere Studio, http://www106.ibm.com/developerworks/websphere/downloads/j2ee_code_validation.html [EIJ2000] G. Pal, S. Bansal: Exceptions in Java: Nothing exceptional about them. http://www.javaworld.com/javaworld/jw-08-2000/jw-0818-exceptions.html, 2000 [DJN2003] B. Eckel: Does Java need Checked Exceptions?, http://www.mindview.net/Etc/Discussions/CheckedExceptions, 2003. [EJE2003] J. R. Kiniry: Exceptions in Java and Eiffel: Two Extremes in Exception Design and Application. ECOOP, 2003. [PRJ1999] P. Haggar: Practical Java Programming Language Guide. Addison-Wesley, 1999. [DRJ2000] M. P. Robillard, G. C. Murphy: Designing Robust Java Programs with Exceptions. ACM, 2000. [EJA2001] C. Ratliff: Exceptions in Java. http://www.lily.org/users/cratliff/mejug/Exceptions%20in%20Java.ppt, 2001. [AEU2003] D. Reimer, H. Srinivasan: Analyzing Exception Usage in Large java Applications. ECOOP, 2003. [UML2003] R. Miller: What's New in UML 2? Model Exceptions. http://community.borland.com/article/0,1410,30169,00.html, 2003. [EXH1975] John B. Goodenough: Exception Handling: Issues and a Propsed Notation. ACM, 1975. [AEF1999] M. P. Robillard, G. C. Murphy: Analyzing Exception Flow in Java Programs. UBC, 1999. [IDE2003] B. G. Ryder, M. L. Soffa: Influences on Design of Exception Handling. ACM, 2003. [EHM1999] A. F. Garcia, D. M. Beder, C. M. F. Rubira: An Exception Handling Mechanism for Developing Dependable Object-Oriented Software Based on a Meta-Level Approach. UNICAMP.1999 [EXS2003] J. D Luca: The Coad Letter: Modeling and Design Edition, Issue 90, 41 [PIJ2001] Exceptional Strategies. http://bdn.borland.com/article/0,1410,29664,00.html#s8, 2003. M. Grand: Overview of Design Patterns, http://www.clickblocks.org/patterns1/pattern_synopses.htm, 2001. 42