Les exceptions du framework

Overview

Dans le framework, il existe un certain nombre de classes d'exceptions génériques. Il est plus simple et plus standard de les utiliser plutot que d'en créer des nouvelles dans des cas généraux.

Par exemple une methode save ne devrait pas lancer d'exceptions SQL à moins d'être dans un package SQL. Lancer une exception générique GB_Save Exception est plus appropriée.

Design par interface: Lors de la création d'interfaces, des exceptions génériques sont souvent utiles et pratiques.

public void save(Object a_value)
    throws GB_SaveException
{
    try {
        // ...
    } catch (SQLException ex) {
        throw new GB_SaveException("Error saving " + a_value, ex);
    }
}

Les exceptions disponibles

Voir package: com.loribel.commons.exception.

On distingue 2 types d'exceptions:

  • GB_Exception extends Exception
  • GB_RuntimeException extends RuntimeException

Exceptions les plus courantes (dérivent de GB_Exception):

  • GB_SaveException
  • GB_LoadException
  • GB_XmlException
  • GB_ConvertorException
  • GB_ConfigException
  • GB_BOException
  • GB_ApplyException
  • GB_TaskException
  • GB_IntrospectionException

Exceptions et GUI

Lorsqu'il survient une exception à l'éxécution d'une commande usager (bouton, menu, ...), Il est conseillé de catcher l'exception et d'afficher un message d'erreur. Pour cela vous pouvez utiliser la classe utilitaire com.loribel.commons.gui.GB_ErrorDialog qui permet d'afficher facilement une boite de message pour les erreurs. Si vous passer une exception non nulle en paramètre, la boite de dialogue contient un bouton détails pour afficher la stack trace de cette exception.

public void actionPerformed(ActionEvent a_event)
{
    try {
        // ...
    } catch (Throwable ex) {
        Component l_parent = ...;
        GB_ErrorDialog.showErrorMsg(l_parent, ex);
    }
}
public void actionPerformed(ActionEvent a_event)
{
    try {
        // ...
    } catch (Throwable ex) {
        String l_msg = "Mon message d'erreur";
        String l_title = "Titre de la boite de dialogue";
        int l_typeError = JOptionPane.WARNING_MESSAGE;
        Component l_parent = ...;
        GB_ErrorDialog.showErrorMsg(l_parent, l_msg, ex, l_title, l_typeError);
    }
}

NullPointerException

Overview

D'après mon expérience personnelle, dans les cas où aucune précaution n'est prise, 80% des exceptions runtime qui font planter un programme viennent d'un NullPointerException. 3/4 d'entre elles sont corrigées ou auraient pu être évitées avec un simple if. Le reste, qui représente des vrais bugs, est souvent difficile à debugger.

On présente ici quelques règles simple pour que le programme soit plus robuste, et plus simple à débugger en cas de bug:

Gestion des boucles

Avant de boucler sur une collection ou un tableau, toujours s'assurer qu'il n'est pas null. Dans 99% des cas, quand on code une boucle, il devrait arriver la même chose que la liste soit vide ou nulle.

On peut soit tester si la collection n'est pas nulle avant de boucler sur les éléments, ou utiliser les templates suivants qui utilise des fonction utilitaires comme CTools.getSize(Collection) qui retourne le nombre d'éléments d'une collection, et 0 si la collection est vide ou nulle.

public void test(
        List a_list)
{
    int len = CTools.getSize(a_list);
    for (int i = 0; i < len; i++) {
        String l_item = (String) a_list.get(i);
        // ...
    }
}
public void test(
        Collection a_items)
{
    if (a_items != null) {
        for (Iterator it = a_items.iterator(); it.hasNext();) {
            Object l_item = it.next();
            // ...
        }
    }
}
    
public void test(
        String[] a_items)
{
    int len = CTools.getSize(a_items);
    for (int i = 0; i < len; i++) {
        String l_item = a_items[i];
        // ...
    }
}    
}

Fixer des conventions sur les retours de méthode de type 'collection'

Il est bon de fixer des conventions pour les retour de méthodes qui retourne des type de 'collection' (Collection ou Array). Si on regarde le code java, en général, lorsque la méthode retourne un tableau, le retour n'est jamais null, la méthode retourne un tableau vide si aucun élément n'est retourné.

Convention à suivre:

  • Un retour de méthode de type Array ne peut jamais être null. Toujours retourner un tableau vide si aucun éléments ne doit être retourné.
  • Un retour de méthode de type Collection peut être null. si aucun éléments ne doit être retourné, retourner null.
public Object[] test()
{
    List retour = new ArrayList();
    // ...
    int len = retour.size();
    if (len == 0) {
        return new Object[0];
    }
    return retour.toArray(new Object[len]);    
    }
}

Ne jamais supposer comme acquis qu'un retour de fonction ne peut être null

Rare doivent être les cas où l'on doit supposer que le retour d'une fonction est toujours non null.

Comme on l'a vu avec les collections, avant de boucler sur une collection, il faut s'assurer qu'elle soiet non nulle. Cette règle s'applique auusi sur les autres objets, avant d'appeler des méthodes sur un objet, il faut s'assurer qu'il n'est pas null. On ne peut pas souvent supposer que l'objet retourné par une fonction par exemple est toujours non null.

2 Cas possibles:

  • l'objet en question n'est pas null: le code normal doit s'exécuter.
  • l'objet est null: C'est le cas où il faut se poser la question. Est-ce que ça fait du sens d'avoir un objet null à cet endroit? Si oui, faire le traitement adéquoi. Sinon lancer une Exception, on peut lancer un NullPointerException avec un message plus explicit qui facilitera le debuggage.
public void capitalizeName()
{
    Person l_person = getPerson();
    if (l_person == null) {
      return;
    }
    String l_name = l_person.getName();
    l_name = GB_StringTransformTools.toCapitalize(l_name);
    l_person.setName(l_name);    
}

Accepter null comme valeur de paramètre

Dans certains cas, il peut être utile de considérer qu'un paramètre peut être null. Un exemple facile, une méthode qui transforme une String (mettre en capitale, ...). La méthode reçoit un paramètre de type String et retourne la String transformée. Cela fait bien du sens que si le paramètre passé est null, la méthode retourne null. Cela permet d'appeler la méthode sans risque de NullPointer. Dans le framework, de nombreuses méthodes respectent cette règle, par exemple voir les méthodes de la classe STools.

public static String toCapitalize(
        String s)
{
    if (STools.isNull(s)) {
        return s;
    }

    String retour = s.substring(1);
    String firstLetter = s.substring(0, 1);
    retour = firstLetter.toUpperCase() + retour;
    return retour;
}

Règles générales pour utiliser les exceptions

Overview - Les principales règles à suivre

  • #1: Use unchecked exceptions for recoverable situations.
  • #2: Use unchecked for programming logic problems
  • #3: Reserve the use of java.lang.Error for underlying virtual machine problems.
  • #4: Always throw a subclass of java.lang.Exception or java.lang.RuntimeException.
  • #5: Create exception classes to represent what went wrong, not where.
  • #7: Do not catch and ignore exceptions
  • #8: Do not catch checked exceptions and throw an unchecked exception.
  • #9: Catch unchecked exceptions and throw a checked exception
  • #10: Do not propagate exceptions from one architectural layer to another

Error

When a dynamic linking failure or some other "hard" failure in the virtual machine occurs, the virtual machine throws an Error. Typical Java programs should not catch Errors. In addition, it's unlikely that typical Java programs will ever throw Errors either.

Article - Exceptional Strategies

Microsoft's C# language does not include an equivalent of Java's checked exceptions. Does this encourage lazy and error-prone code or is it an astute recognition that Java's checked exceptions are more trouble than they are worth?

Java Exceptions in a Nutshell The Java language has three categories of exception,

  • instances of the java.lang.Exception class and its subclasses
  • instances of the java.lang.RuntimeException class and its subclasses
  • instances of the java.lang.Error class and its subclasses
The Java compiler treats the first category of exceptions, the so-called checked exceptions, differently from the latter two, the unchecked exceptions. When a method creates a checked exception object and throws it (raises it), the compiler expects the method to either catch (handle) that exception later in its code or declare in its signature that it throws objects of that exception class or one of its superclasses. A compile-time error occurs if these conditions are not met. Unchecked exceptions are free from these constraints; they can be thrown anywhere and propagate up the stack of nested method calls until they are caught by an appropriate catch block or they reach the top of the call stack and cause the program to terminate.

Conventional Java wisdom teaches that unchecked exceptions should be used for errors in programming logic(Strategy #2) or when errors occur within the underlying virtual machine or operating environment (Strategy #3). Checked exceptions should be used for recoverable situations (Strategy #1). For example, a checked exception should be used when a user has supplied invalid data values so that the user can be asked to supply valid values instead. Also a checked exception should be used where a time-out may occur and the caller might want to try again to see if the condition causing the time-out has cleared.

Checks out of Fashion?

Microsoft chose not to include the concept of checked exceptions in C#.

As a result there seems to be a growing number of voices arguing that checked exceptions cause more problems than they solve and are a failed experiment.

When a change in a method's implementation causes that method to throw a new checked exception, developers have a choice.

  • Add the exception to the list of classes in the throws clause at the end of the method's signature.
  • Catch the exception and instead either throw an unchecked exception(Strategy #8), or a checked exception that is already declared in the methods signature (Strategy #12).
  • Catch the exception and either ignore it, or simply log the exception and carry on as if nothing had happened (Strategy #7).
Option 1 may cause a severe ripple affect because the callers of the method must now catch and handle that exception or declare that they throw it too. If the calling methods change their signature to include the declaration then their callers are affected and so on. In a large body of code this could result in small changes in many classes. Object-oriented programming is all about encapsulation and localizing change; this option would seem to be at odds with this ideal.

Option 2 may hide the real cause of the exception from someone trying to determine why an exception is being raised. The usual solution to this is to pass the original exception as an argument to the new exception's constructor so that it can be retrieved by error handling code and detailed exception information is not lost.

Option 3 often means an incorrect result goes unrecognised until it causes a worse problem somewhere else making the original problem much harder to track down. This is almost universally acknowledged to be a bad practise.

Those arguing against checked exceptions point to the frequent occurrence of developers picking option 3 and suggest that checked exceptions actively encourage this bad practise because it is the least work to implement. They also point out that option 2 can tempt developers to create unsuitable inheritance relationships between exceptions to avoid using option 1.

So is there a case for ridding the world of checked exceptions? In my opinion, the answer has to be, "No".

Firstly, it should be remembered that exceptions should only be used for exceptional circumstances and checked exceptions should only be used for the subset of those where an application can be reasonably expected to recover (Strategy #1). This should be a relatively small number of cases.

Secondly, if a method does not list all the exceptions it throws it can be very hard to know exactly what exceptions are thrown by that method. This is especially true if the source code of the class being called is not available (part of a third party component, product, or framework).

For this very reason, many people recommend listing the unchecked exceptions thrown by a method even though this is not enforced by the compiler.

Thirdly, the throws clause forms part of the interface of a method and the contract with its callers. Any change to the interface of a method may cause a ripple affect through its callers. We often make the parameters and return type of a method more generic than strictly necessary to avoid changing a method signature to frequently. We can follow the same sort of strategy for exceptions by declaring that they throw a superclass of the exact exception thrown (Strategy #12). Of course, in Java we are limited to single inheritance; maybe it would be better if the throws clause took Java interfaces instead of specific classes. It may have also be better if java.lang.Exception and java.lang.RuntimeException were abstract classes so that the temptation to use them instead of more useful specific subclasses is removed (Strategy #4).

It may be that checked exceptions have been overused in a similar way to which inheritance was overused when object-oriented programming first burst into mainstream software development. However, I believe much of the problem with exception handling is caused by indiscipline and the abandoning checked exceptions will create as many problems as it solves.

For example, a developer adding or changing the type of an unchecked exception thrown deep in a program may cause a program to unravel all the way to a generic high-level catch block or cause a thread to exit abruptly. For non-recoverable situations such as programming logic problems this may be the best that can be done but it is not acceptable for simple recoverable situations such as a time-out on a database or network request, or small problem in a set of data supplied by a user of the system.

In conclusion, I'd prefer to keep checked exceptions in Java. Exceptions in Java may not be perfect but I like having the choice of using checked or unchecked exceptions.

Here are a dozen strategies for using exceptions in Java. I'd be happy to receive suggestions for others and to hear about your views and opinions on the subject in the forums at www.thecoadletter.com or by e-mail. A version of my original guidelines on exceptions can be found at http://www.nebulon.com for those who enjoy ancient history :-)

Java Exception Strategy

Java Exception Strategy #1: Use unchecked exceptions for recoverable situations.

Use a subclass of java.lang.Exception to represent a transient or recoverable problem. The name of the new class should end with the suffix Exception to distinguish it from business-as-usual classes. Example: NullParameterRuntimeException - thrown when a parameter of a method is expected to have a non-null value.

Java Exception Strategy #2: Use unchecked for programming logic problems Use a subclass of java.lang.RuntimeException to represent a programming problem.

The name of the new class should end with the suffix RuntimeException to make it clear that this exception is not required by the Java compiler to be declared in the throws clause of method signatures.

Example: NullParameterRuntimeException - thrown when a parameter of a method is expected to have a non-null value.

Java Exception Strategy #3: Reserve the use of java.lang.Error for underlying virtual machine problems.

Under normal application development do not create or throw instances of java.lang.Error or its subclasses. Reserve these types of exception for problems with the underlying virtual machine.

Example: java.lang.OutOfMemory - the exception thrown when the virtual machine has exhausted its allocated memory space.

Java Exception Strategy #4: Always throw a subclass of java.lang.Exception or java.lang.RuntimeException.

Do not explicitly create and throw instances of java.lang.RuntimeException or java.lang.Exception. Always throw a more specific subclass instead. This enables error handling code for specific exceptional conditions to be written easily. In general, if error handling code has to inspect the contents of an exception object to determine what action to take then the exceptions being thrown are not specific enough.

Example:

    throw new Exception( "Specific message" ); // No !!!

throw new SpecificException(); // Yes !!!

Java Exception Strategy #5: Create exception classes to represent what went wrong, not where.

Name exception classes so that they represent the type of exceptional circumstance that took place and not the class, package, component where the exceptional circumstance took place. Where the exception occurred should be part of the exception details. This promotes the reuse of exception classes and avoids having multiple different exception classes representing the same problem in different places. Example: BalanceTooLowForDebitException and not BankingAccountException.

Java Exception Strategy #6: Use the same parameterized message template for all instances of an exception class.

Objects of the same checked exception class should represent a specific exceptional situation and therefore always use the same message template. Values within the message may be different but the message template itself should be the same. The constructor of the exception class should take parameters for the message as arguments and not the message text itself. An exception class should know how to obtain its own message template text from a class constant, properties file, or other persistent store. If you find yourself wanting an exception to have a different message in a different context, create a separate exception class for that context. Having exceptions manage their own message text also makes listing exception messages for localization, user guides, programming guides, etc., much easier than if the messages are scattered throughout the whole of the source code.

Example: Instances of BalanceTooLowForDebitException should always use the message template "Account {0} does not have a high enough balance to process a debit of {1} {2}" where {0} is replaced by the account number, {1} is replaced by the debit amount, and {2} is replaced by the currency.

Java Exception Strategy #7: Do not catch and ignore exceptions

Only in relatively rare circumstances is it correct to catch and ignore an exception. If an exception cannot be handled sensibly at that point in the code, the exception should be allowed to propagate up the method call stack until it can be handled sensibly. Simply displaying an exception's details on a system console or in an error log and then allowing processing to proceed is not good considered 'handled sensibly'.

Example: challenge code similar to the following:

    try { ... }
    catch (SomeException e1)      // catch any instances of SomeException thrown in the try block
    { }                           // totally ignore the SomeException objects caught
    catch (SomeOtherException e2) // catch any instances of SomeOtherException thrown in the try block
    {
        e2.printStackTrace();     // log an exception's details but otherwise ignore it
    }

Java Exception Strategy #8: Do not catch checked exceptions and throw an unchecked exception.

It only makes sense to catch a checked exception and immediately throw an unchecked exception if, at that point in the code, the occurrence of a checked exception indicates a defect in the program logic. Catching a checked exception and immediately throwing an unchecked exception only to avoid adding to the throws clause of a method signature is never valid. Example: challenge code similar to the following:

    try { ... }
    catch (SomeCheckedException e1)  // catch instances of a checked exception
    {
         throw new SomeRuntimeException( e1 ); // throw an unchecked exception
    }

Java Exception Strategy #9: Catch unchecked exceptions and throw a checked exception

When a runtime exception has been thrown that the system might be able to recover from sensibly. Catch the runtime exception and throw a checked exception.

Example: catch a runtime exception representing a network time-out and throw a checked exception so that the calling methods know that they can catch a time-out and request a retry if it makes sense for them to do so.

Java Exception Strategy #10: Do not propagate exceptions from one architectural layer to another

Catch exceptions thrown by another architectural layer and throw an exception from the current architectural layer. Always pass the caught exception as an argument to the constructor of the new exception so that detailed information is not lost. This preserves the separation of concerns that the architecture layers provide.

Example: Catch an SQLException thrown by JDBC code and throw a SaveException that holds the original SQLException object in an instance variable.

Java Exception Strategy #11: 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 continues to enforce the throwing of specific concrete exceptions.

Example: Create an abstract superclass InvalidDebitValueException for exceptions like BalanceTooLowForDebitException and NegativeDebitValueException.

Java Exception Strategy #12: Consider declaring that a method throws a more generic exception

Before adding a specific exception class to the throws clause of a method, consider adding a superclass of that exception instead. Closely related to strategy #11, careful thought about what can go wrong in a method's execution and the sort of corrective action that needs to be taken, can help avoid ripple affects later if the method's implementation is changed. In the same way that parameters and return types of a method may be declared to be of more general types than strictly necessary to avoid a change in implementation causing a change in the method's signature, so with exceptions named in the throws clause.

Example: declare a method that debits an account as throwing an InvalidDebitValueException instead of a BalanceTooLowForDebitException.

Then should the implementation of the account be changed so that only debits upto a certain daily amount are allowed, a new subclass of InvalidDebitValueException can be introduced, DailyDebitAmountExceededException, without causing a change to the method's signature.

Helping Developers Apply the Strategies

Create two general abstract classes, one that extends java.lang.Exception and another that extends java.lang.RuntimeException. Insist that all exceptions thrown be your application code be subclasses of these two new classes. These two classes can also include the code needed to retrieve the exception's message template from a property file or database, parse it and insert the parameters supplied. They can also support the nesting of other exceptions within themselves. Doing this helps developers apply strategies 4,5 and 9.

Well run code inspections or pair programming can help developers apply the other strategies in the list.

Some automated code auditing tools may be able to identify ignored exceptions and the explicit creation of java.lang.Exception, java.lang.RuntimeException and java.lang.Error instances, or explicit creation of classes ending in 'Exception' that are not subclasses of your two general abstract exception classes.

For a large project/system it may be worth assigning someone the job of managing exceptions so that existing exceptions are reused where appropriate and not used where inappropriate.

Chained Exception Facility (new in jdk 1.4)

This new facility provides a common API to record the fact that one exception caused another, to access causative exceptions, and to acess the entire "causal chain" as part of the standard stack backtrace, ensuring that preexisting programs will provide this information with no additional effort on the part of their authors.

To address these issues, we have added two new methods and two new constructors to Throwable:

  • Throwable getCause(),
  • initCause(Throwable),
  • Throwable(Throwable),
  • Throwable(String, Throwable).

Other "general purpose" exception classes (like Exception, RunTimeException and Error) have been similarly outfitted with (Throwable) and (String, Throwable) constructors. However, even exceptions without such constructors will be usable as "wrapped exceptions" via the initCause method.

The implementation of Throwable.printStackTrace has been modified to display backtraces for the entire causal chain of exceptions. New method getStackTrace provides programmatic access to the stack trace information provided by printStackTrace.