I. Les exceptions

Il existe sur Developpez.com un très bon cours java, dont un chapitre traite exclusivement des exceptions : Chapitre Exceptions.

Un tutoriel donne aussi quelques bonnes pratiques sur la gestion des exceptions : Tutoriel.

Vous y trouverez les généralités nécessaires pour la bonne compréhension des exceptions en Java, et des bonnes pratiques pour les gérer. Nous allons voir ici quelques aspects « philosophiques » sur les exceptions, et comment véritablement les gérer dans un environnement Java Android.

I-A. Pourquoi les exceptions ?

I-A-1. Un outil de gestion des erreurs

Comme son nom l'indique, une exception signifie que quelque chose d'inattendu s'est produit pendant l'exécution du code, et que celle-ci n'a pu arriver à son terme.

Ce peut être dû à l'environnement ou à une mauvaise utilisation du code, ou simplement encore, à une « erreur simple ».

Il existe en effet deux écoles en programmation. L'une suggère de réserver les exceptions à des événements inattendus, l'autre utilise les exceptions pour signaler les erreurs « normales ».

Prenons l'exemple d'un payement par carte de crédit. Des exceptions signaleront forcément les cas où le service n'est pas joignable, ou une erreur dans les données transmises. Mais quid des événements du style « pas assez d'argent à la banque » ? La première école utilisera un champ d'erreur dans l'objet retourné par la fonction, l'autre utilisera une exception.

La « bataille » est cependant moins « forte » dans le monde Java, car le langage distingue deux types d'exceptions. Les exceptions « classiques » (nécessitant d'être déclarées par la fonction), et les exceptions « runtime ». Ainsi, Java propose un mécanisme de « déclaration » des erreurs possibles, et à mon avis, autant l'utiliser.

I-A-2. Un outil de débogage et de développement

Les exceptions ne sont pas de simples objets sans contenu, une exception maintient une foule d'informations très utiles au débogage (entre autres) :

tout d'abord une « stacktrace » : c'est-à-dire l'état de la pile des appels de fonctions au moment où l'exception a été construite (« new »). Cette trace inclut les noms de fichiers et lignes, mais aussi les noms de classes et de fonctions. Une information inestimable ;

ensuite l'exception peut contenir une « cause ». Par exemple, une exception « HTTP » pourrait avoir comme cause une exception « réseau » (un problème de communication) ou une exception « serveur » (le serveur a refusé la demande) ;

le « logging » devient alors primordial, et utiliser les fonctions Android prévues à cet effet : Log.e(TAG, « un message », exception) permet d'obtenir toutes les informations de l'exception dans le « logcat », absolument indispensable pour le débogage.

Voici un exemple de trace produit par une exception dans le logcat :

Stack Trace
Sélectionnez
06-05 09:24:19.939: E/AndroidRuntime(1554): FATAL EXCEPTION: main
06-05 09:24:19.939: E/AndroidRuntime(1554): android.content.res.Resources$NotFoundException: Resource ID #0x0
06-05 09:24:19.939: E/AndroidRuntime(1554):     at android.content.res.Resources.getValue(Resources.java:1013)
06-05 09:24:19.939: E/AndroidRuntime(1554):     at android.content.res.Resources.loadXmlResourceParser(Resources.java:2098)
06-05 09:24:19.939: E/AndroidRuntime(1554):     at android.content.res.Resources.getLayout(Resources.java:852)
….
06-05 09:24:19.939: E/AndroidRuntime(1554):     at android.os.Handler.dispatchMessage(Handler.java:92)
06-05 09:24:19.939: E/AndroidRuntime(1554):     at android.os.Looper.loop(Looper.java:137)
06-05 09:24:19.939: E/AndroidRuntime(1554):     at android.app.ActivityThread.main(ActivityThread.java:4745)
06-05 09:24:19.939: E/AndroidRuntime(1554):     at java.lang.reflect.Method.invokeNative(Native Method)
06-05 09:24:19.939: E/AndroidRuntime(1554):     at java.lang.reflect.Method.invoke(Method.java:511)
06-05 09:24:19.939: E/AndroidRuntime(1554):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
06-05 09:24:19.939: E/AndroidRuntime(1554):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
06-05 09:24:19.939: E/AndroidRuntime(1554):     at dalvik.system.NativeStart.main(Native Method)

Comme on le voit, chaque ligne décrit un des appels de fonctions ayant conduit à l'exception, par exemple, au moment de l'exception, le programme était dans la fonction run() de la classe MethodAndArgsCaller, déclarée dans la classe com.android.internal.os.ZygoteInit (fichier « ZygoteInit.java », ligne 786). La toute dernière ligne est toujours : dalvik.system.NativeStart.main(Native Method) puisque c'est le point d'entrée de tous les programmes Java.

I-B. Rappels de syntaxe

I-B-1. Levée d'une exception

En Java, une exception est simplement « levée » par l'utilisation du mot-clé « throw ». L'objet passé à la commande throw doit hériter de « Throwable » (« Exception » hérite de « Throwable »).

I-B-2. Déclaration d'exception

Toute fonction qui lève une exception non « runtime » (donc qui n'hérite pas de « RuntimeException »), doit impérativement le déclarer. Ceci est fait en rajoutant le mot-clé « throws » après la déclaration de la fonction, suivi par les classes d'exception que la fonction est susceptible de lancer.

Par exemple :

 
Sélectionnez
public void myFunction(int param) throws IOException, AnotherException;

I-B-3. Try/Catch/Finally

À tout moment, il est possible d'intercepter les exceptions levées par une séquence de code.

Un bloc « try » est créé, dans lequel on mettra la séquence à protéger, suivie au moins d'un bloc « catch » ou « finally ».

Un bloc « catch » sera exécuté si une exception du type « catché » est levée dans le code protégé.

Le bloc « finally » est toujours invoqué, qu'une exception ait été levée, ou non, catchée ou non.

Sans bloc « catch » ou « finally » le bloc « try » devient alors inutile et provoquera une erreur de compilation.

Par exemple :

 
Sélectionnez
try {
   …
   code pouvant envoyer les exceptions IOException, et NumberFormatException
   …
} catch (IllegalArguementException pe) {
   // si une exception de type NumberFormatException a été levée, on passera ici
   // parce que NumberFormatException hérite de  IllegalArguementException
} finally {
   // on passera toujours ici :
   // à la fin du bloc try si aucune exception n'a été levée,
   // après le bloc catch si une exception de type "IllegalArguementException" a été levée
   // ou dès la levée de toute autre exception
}

Il est possible de rajouter autant de blocs « catch » que nécessaire… Ceux-ci sont vérifiés dans l'ordre de leur déclaration… Ainsi si une exception A hérite de B, alors le bloc « catch » de B devra apparaître après le bloc « catch » de A.

Il n'est pas nécessaire d'avoir un bloc « catch » si on veut s'assurer qu'une séquence de code soit toujours exécutée (par exemple pour libérer des ressources), un bloc « finally » est suffisant.

II. Au niveau de la fonction

Pour voir comment gérer les exceptions dans votre programme, commençons au niveau le plus bas : la fonction.

II-A. Généralités

Toute fonction a des préconditions (conditions devant être remplies pour que la fonction puisse s'exécuter) et des postconditions (conditions remplies à l'issue de l'exécution de la fonction).

La règle générale est que si la fonction est incapable de fournir ses postconditions, elle doit lever une exception, sans avoir aucun autre effet de bord (comme si elle n'avait jamais été appelée).

Concernant les préconditions, c'est un peu comme vous le sentez (après tout c'est votre programme). Ou vous partez du principe que la fonction sera toujours invoquée correctement (pas de précondition à vérifier dans ce cas), ou vous préférez être tranquille et donc protéger les appels contre une mauvaise utilisation (et souvent contre vous-même).

II-A-1. Gérer les préconditions

À l'entrée dans la fonction, il est temps de vérifier que les paramètres passés suivent toutes les préconditions.

Si ce n'est pas le cas, une exception de type « RuntimeException » (qui n'a donc pas besoin d'être déclarée par la fonction) devrait être levée. L'utilisation d'une exception de type « runtime » évite de polluer les blocs try/catch des fonctions appelantes (un défaut de préconditions étant une erreur du programmeur).

L'exception la plus utilisée dans ce cas de figure est sans doute « IllegalArgumentException ».

Attention, il existe un paramètre caché dans tous les appels de fonctions membres : « this » ! En effet, il est possible qu'on doive aussi vérifier l'objet sur lequel est appelée la fonction. Dans ce cas on utilise souvent « IllegalStateException » pour signaler que l'objet n'est pas dans un état correct. Encore une fois, c'est une « RuntimeException ».

Pour finir, il se peut que la machine virtuelle Java ne remplisse pas les préconditions (par exemple ne supporte pas un certain charset, ou une langue précise…), là encore nous pourrons utiliser une des innombrables RuntimeException (voire créer nos propres RuntimeException si besoin).

Dans tous les cas, le message de l'exception se doit d'être parfaitement compréhensible par tous les intervenants du projet : ce sont eux qui sont visés par ces exceptions, et en aucun cas, l'utilisateur de l'application.

II-A-2. Gérer les postconditions

II-A-2-a. Erreurs d'état

La fonction peut très bien ne pas fonctionner si un fichier est en lecture seule… ou si une heure donnée est dépassée, ou n'importe quoi d'autre. Si une telle erreur « d'état » est rencontrée et empêche de satisfaire les postconditions, une exception doit être levée.

Comme ce n'est pas une erreur de programmation, et que cet état peut intéresser l'utilisateur (pas de réseau par exemple), une exception normale sera levée.

Pour que l'appelant de la fonction puisse distinguer les erreurs entre elles (et éventuellement agir différemment selon le type d'erreur), il faut surtout faire attention à ne pas mélanger les exceptions entre elles. La classe d'une exception signifie quelque chose, et en aucun cas, par exemple, on n'utilisera IOException pour signifier une erreur de contenu de données !

Du coup, la fonction devra déclarer l'envoi de ces exceptions, afin que, justement, l'appelant puisse les gérer au mieux.

II-A-2-b. Erreurs des sous-fonctions

Pendant l'exécution de la fonction, celle-ci va certainement appeler d'autres fonctions qui elles aussi peuvent envoyer des exceptions. Souvent des problèmes d'entrée/sortie (les exceptions les plus fréquentes), mais aussi des erreurs de données (parsing, format…).

Si la fonction est capable de gérer l'erreur, l'appel doit être protégé par un bloc try/catch, bien faire attention de ne « catcher » que les exceptions vraiment récupérables. Il est toutefois utile d'utiliser une méthode de log pour informer un programmeur que l'exception a eu lieu (et pourquoi). Par exemple :

 
Sélectionnez
try {
    // algorithme 1
    appel de fonction qui risque d'envoyer ParseException
} catch (ParseException ex) {
    Log.i("MAFONCTION", "Erreur de parsing, utilisation de l'algorithme 2",ex);
    // algorithme 2
}

Si la fonction est incapable de gérer l'erreur, elle doit arrêter son traitement, et, point sans doute le plus essentiel, revenir à l'état initial (comme si la fonction n'avait jamais été appelée). Par exemple une fonction « move » (sur un objet graphique par exemple) qui lève une exception, doit s'assurer qu'aucune coordonnée n'a été modifiée avant de lancer l'exception.

Ces exceptions sont en général gérées de manière globale à la fonction (un bloc try/catch/finally englobant la fonction en entier)… Les parties « catch » se chargeront de faire revenir la machine virtuelle à l'état initial et la partie « finally » à fermer l'ensemble des ressources potentiellement ouvertes :

 
Sélectionnez
InputStream is = null;
try {
   is = open(...);
   // algorithme
} catch (Exception1 error1) {
   // retour à l'état initial
} catch (Exception2 error2) {
   // retour à l'état initial
} finally {
   if (is != null) try { is.close(); } catch (Exception ex) { Log.w("MAFONTION", "Couldn't close the stream !",ex); }
}

Si l'exception n'a pas besoin d'être traduite, et qu'il n'y a pas besoin de revenir à un état précédent (fonction sans effet de bord), il n'est bien sûr pas nécessaire de la « catcher », il suffit de déclarer l'exception comme étant lancée par la fonction.

Si l'exception n'a pas besoin d'être traduite, mais que l'on doit faire des choses pour « revenir » à l'état initial, elle doit être « catchée » (pour revenir à l'état initial) puis immédiatement renvoyée (voir dans le chapitre suivant une autre méthode).

Si l'exception a besoin d'être traduite en une autre exception, non seulement elle doit être « catchée », mais surtout passée en tant que « cause » à la nouvelle exception (après retour en arrière de l'état de la fonction bien entendu).

En aucun cas, la fonction ne doit s'arrêter sans lever d'exception pour signaler justement à l'appelant que tout ne s'est pas passé correctement.

II-A-2-c. Le paradigme de transaction

Plutôt que de devoir catcher toutes les exceptions et les relancer pour revenir à un état « précédent », il existe une manière de gérer en une fois toutes les exceptions. C'est le principe « transactionnel » utilisé dans les bases de données :

 
Sélectionnez
boolean success = false;
int storedValue = this.myValue;
try {
    // algorithme (qui risque d'envoyer des exceptions)...
     
    // tout à la fin:
    success = true;
} finally {
   // ..
   // fermeture des ressources
   // ..
   if (!success) {
       // restauration de l'objet
       this.myValue = storedValue;
   }
}

En cas d'exception pendant l'algorithme, le code ne passera jamais sur « success=true », donc le bloc finally sera exécuté (il l'est toujours) avec « success » valant « false », la fermeture des ressources sera effectuée, et l'objet sera remis à son état initial.

Par contre, si aucune exception n'a eu lieu, success est bien passé à « true », et le bloc finally (toujours exécuté) va donc ne rien faire du tout (à part fermer les ressources).

Attention, si l'objet peut être accessible par plusieurs « threads » en même temps, il est indispensable d'implémenter un système de « lock » pendant la « transaction » (simplement déclarer la fonction comme étant « synchronized » peut suffire). Sinon un thread peut voir un objet dans un état « temporaire » qui ne sera pas forcément appliqué à la fin de la transaction !

Il est à noter que l'approche inverse peut-être implémentée : stocker tout le long de l'algorithme des valeurs « temporaires », et durant le bloc « finally », appliquer (en cas de succès) les valeurs temporaires à l'objet. Cette approche, si elle permet de s'affranchir des problématiques de lock entre threads (voir ci-dessus), a l'inconvénient d'empêcher l'appel d'autres méthodes de l'objet durant la transaction (en effet celles-ci verront toujours l'état de l'objet avant la transaction, et pas son état temporaire actuel).

II-B. La fonction par l'exemple

Prenons un cas simple : une fonction « String login(String user, String password) ». La fonction doit recevoir deux éléments non vides (login & password) et fournir une « clé » de validation. Elle n'a aucun effet de bord. La postcondition est donc « fournir une clé d'authentification »… Si a un moment cette fonction est incapable de le faire, elle doit lever une exception. Retourner « null » n'est bien entendu pas une option.

La fonction fera hélas appel à un (mauvais) web service, qui renvoie une réponse HTTP « OK » mais le contenu « ERROR » en cas d'erreur de login, ou un token valide si tout s'est bien passé. Un bon web service renverrait une erreur HTTP pour signaler une erreur de login.

Commençons par la fonction « simple » sans aucune gestion des exceptions :

 
Sélectionnez
    /**
     * @param username 
     * @param password
     * @return The authentication token.
     */
    public String login(String username, String password)
    {
        HttpClient  client = new DefaultHttpClient();
          
        String url = String.format("https://my.webservice.com/login.php?user=%0&pwd=%1",username,password); //$NON-NLS-1$
         
        HttpGet getRequest = new HttpGet(url);
        HttpResponse response = client.execute(getRequest);
        if (response.getStatusLine().getStatusCode()>=400)
            return null;
        HttpEntity entity = response.getEntity();
        String token = entity.toString();
        if ("ERROR".equalsIgnoreCase(token))
            return null;
         
        return token;
    }

II-B-1. Gestion des exceptions « existantes »

Déjà, la fonction ne compilera pas… Si vous utilisez Eclipse, celui-ci vous dira immédiatement que l'appel à client.execute() risque d'envoyer les exceptions « ClientProtocolException » et « IOException » qui ne sont pas gérées…

Malheureusement Eclipse va proposer de « générer » un code de gestion de ces exceptions par un simple try/catch autour des appels problématiques. Le résultat étant désastreux puisque par défaut les exceptions seront simplement passées sous silence. Ainsi, sur un « ClientProtocolException », l'appel à « execute » ne va rien renvoyer, et nous allons appeler response.getEntity() sur un objet null (NullPointerException et le programme va se fermer).

De plus, toutes les exceptions ne seront pas gérées de la même façon. Voyons cela en détail.

II-B-1-a. Renvoi des exceptions « utiles » à l'appelant

Que signifient les exceptions levées par le code ?

IOException signifie qu'une exception d'entrée/sortie (le service n'est pas joignable, il s'est produit une erreur TCP/IP, etc.) a eu lieu… Il est évident qu'une telle exception est utile à l'appelant, et devrait donc lui être passée directement.

On va donc simplement rajouter « throws IOException » à la déclaration de la fonction :

 
Sélectionnez
    /**
     * @param username 
     * @param password
     * @return The authentication token.
     * @throws IOException if an error occured during the transfer
     */
    public String login(String username, String password) throws IOException
    {
        HttpClient  client = new DefaultHttpClient();
          
        String url = String.format("https://my.webservice.com/login.php?user=%0&pwd=%1",username,password); //$NON-NLS-1$
         
        HttpGet getRequest = new HttpGet(url);
        HttpResponse response = client.execute(getRequest);
        if (response.getStatusLine().getStatusCode()>=400)
            return null;
        HttpEntity entity = response.getEntity();
        String token = entity.toString();
        if ("ERROR".equalsIgnoreCase(token))
            return null;
         
        return token;
    }

II-B-1-b. Disparition des exceptions « inutiles »

Dans le cas de ClientProtocolException, nous savons pertinemment qu'elle ne se produira jamais : la fonction maîtrise la construction de l'URL, elle ne peut pas renvoyer cette exception… Il va donc falloir la gérer.

Si l'URL avait été passée en paramètre, il aurait fallu bien entendu traduire cette exception (ou la déclarer comme possible) !

Première chose : c'est bien entendu l'ensemble de la fonction qui échoue… n'ayons pas peur, et protégeons l'ensemble de la fonction.

 
Sélectionnez
    /**
     * @param username 
     * @param password
     * @return The authentication token.
     * @throws IOException if an error occured during the transfer
     */
    public String login(String username, String password) throws IOException
    {
        try {
            HttpClient  client = new DefaultHttpClient();

            String url = String.format("https://my.webservice.com/login.php?user=%0&pwd=%1",username,password); //$NON-NLS-1$

            HttpGet getRequest = new HttpGet(url);
            HttpResponse response = client.execute(getRequest);
    
            if (response.getStatusLine().getStatusCode()>=400)
                return null;

            HttpEntity entity = response.getEntity();
            String token = entity.toString();
            if ("ERROR".equalsIgnoreCase(token))
                return null;
         
            return token;
        } catch (ClientProtocolException ex) {
            Log.e("LOGIN","HTTPS non supporté !",ex);
            return null;
        }
    }

Voilà, à partir de maintenant le code compile correctement. Mais nous n'en avons pas fini pour autant. Il faut maintenant vérifier les préconditions et postconditions.

II-B-2. Rajout des exceptions manquantes

II-B-2-a. Préconditions

Parmi les préconditions de la fonction, il y a bien sûr le fait de recevoir un « username » et un « password »… nous allons donc simplement le vérifier en utilisant une « RuntimeException ». Les exceptions « runtime » n'ont pas besoin d'être déclarées par la méthode et expriment souvent un défaut dans le code. La plus adaptée dans le cas est bien sûr IllegalArgumentException :

 
Sélectionnez
    /**
     * @param username 
     * @param password
     * @return The authentication token.
     * @throws IOException if an error occured during the transfer
     */
    public String login(String username, String password) throws IOException
    {
         if (username == null) throw new IllegalArgumentException("Username cannot be null");
         if (password == null) throw new IllegalArgumentException("Password cannot be null");

        try {
            HttpClient  client = new DefaultHttpClient();

            String url = String.format("https://my.webservice.com/login.php?user=%0&pwd=%1",username,password); //$NON-NLS-1$

            HttpGet getRequest = new HttpGet(url);
            HttpResponse response = client.execute(getRequest);
    
            if (response.getStatusLine().getStatusCode()>=400)
                return null;

            HttpEntity entity = response.getEntity();
            String token = entity.toString();
            if ("ERROR".equalsIgnoreCase(token))
                return null;
         
            return token;
        } catch (ClientProtocolException ex) {
            Log.wtf("LOGIN","HTTPS non supporté !",ex);
            return null;
        }
    }

Attention, la gestion des préconditions n'est pas obligatoire, mais vous évitera pas mal d'heures de débogage dans le cas de gros projets.

II-B-2-b. Postconditions

C'est là où les exceptions « normales » vont entrer en jeu. Quand la fonction est incapable de livrer un état correspondant à la postcondition (ici un token d'authentification correct), elle doit lancer une exception.

Si nous lisons la fonction, il y a trois cas où elle sera incapable de délivrer un token : si le status-code de l'appel HTTP est >= 400 (erreur HTTP), si ClientProtocolException a été lancé, ou simplement si le web service a retourné « ERROR ».

Dans le premier cas, cela veut dire qu'une erreur de communication s'est produite… nous traduirons donc ce cas par un lancement de IOException. Le message étant contenu dans le statut HTTP, nous l'utiliserons directement.

Le second cas est une erreur de configuration et sera traduite par une erreur « runtime » IllegalStateException (sans oublier de passer l'exception initiale en « cause » afin de ne rien perdre). À noter que dans ce cas, le log peut-être supprimé (puisque l'exception sera traduite et passée à l'appelant, rien ne sera « perdu »).

 
Sélectionnez
    /**
     * @param username 
     * @param password
     * @return The authentication token.
     * @throws IOException if an error occured during the transfer
     */
    public String login(String username, String password) throws IOException
    {
         if (username == null) throw new IllegalArgumentException("Username cannot be null");
         if (password == null) throw new IllegalArgumentException("Password cannot be null");

        try {
            HttpClient  client = new DefaultHttpClient();

            String url = String.format("https://my.webservice.com/login.php?user=%0&pwd=%1",username,password); //$NON-NLS-1$

            HttpGet getRequest = new HttpGet(url);
            HttpResponse response = client.execute(getRequest);
    
            if (response.getStatusLine().getStatusCode()>=400)
                throw new IOException(response.getStatusLine().getReasonPhrase());

            HttpEntity entity = response.getEntity();
            String token = entity.toString();
            if ("ERROR".equalsIgnoreCase(token))
                return null;
         
            return token;
        } catch (ClientProtocolException ex) {
            throw new IllegalStateException("HTTPS non supporté !",ex);
        }
    }

Il ne reste donc plus que le cas du Login failure… Pour ce cas précis, il nous faut un moyen d'indiquer à l'appelant qu'il n'a pas eu son token à cause d'une erreur d'authentification (et non environnementale ou de développement)… Pour cette raison, nous allons créer une exception spécifique : AuthenticationException, déclarée comme pouvant être lancée par la fonction, et que nous n'enverrons que dans ce cas précis :

 
Sélectionnez
    /**
     * Exception thrown when invalid credentials were given.
     */
    public static class AuthenticationException extends Exception {
        /**
         * @param msg
         */
        public AuthenticationException(String msg) {
            super(msg);
        }
    }

    /**
     * @param username 
     * @param password
     * @return The authentication token.
     * @throws IOException 
     * @throws AuthenticationException
     */
    public String login(String username, String password) throws IOException, AuthenticationException
    {
        try {
            HttpClient  client = new DefaultHttpClient();
          
            String url = String.format("https://my.webservice.com/login.php?user=%0&pwd=%1",username,password); //$NON-NLS-1$
         
            HttpGet getRequest = new HttpGet(url);
            HttpResponse response = client.execute(getRequest);
            if (response.getStatusLine().getStatusCode()>=400)
                throw new IOException(response.getStatusLine().getReasonPhrase());
            HttpEntity entity = response.getEntity();
            String token = entity.toString();
            if ("ERROR".equalsIgnoreCase(token))
                throw new AuthenticationException("Invalid username/password");
            return token;
        } catch (ClientProtocolException ex) {
            throw new IllegalStateException("HTTPS non supporté !", ex);
        }
    }

Et voilà, notre fonction est maintenant finie, protégée contre les mauvais appels, les erreurs éventuelles, et surtout sans jamais aucune perte d'information.

Il reste encore du travail, notamment, le passage des paramètres est erroné (un mot de passe avec « & » ou « ? » risque de faire planter la requête), mais en tout cas aucune exception ne sera oubliée.

III. Au niveau du « framework »

L'appelant d'une fonction utilisant les mêmes principes, les exceptions vont donc « naturellement » remonter jusqu'à un niveau ou vous ne pourrez plus rien changer… parce que la fonction hérite d'une fonction prédéfinie (au pire le « main » du programme Java).

C'est le cas en particulier quand vous programmez dans un « framework » (comme Android). Et pour cause, que signifierait que « onCreate » n'a pas fonctionné ? Que l'on doit quitter l'application ?… et que « onClick » n'a pas fonctionné ? Que l'on doit faire comme si l'utilisateur n'avait pas cliqué ?

Ces fonctions ne peuvent donc pas renvoyer d'exception. Et si c'est le cas, l'application sera immédiatement interrompue (application xxxx has stopped…).

Mais ce ne sont pas que les seuls exemples, nous trouverons de telles restrictions dans les tâches de background, dans les threads… Regardons les cas possibles :

III-A. Le thread UI

Dans les cas de l'UI rien n'est changé quant à la gestion des sous-fonctions… un bloc try/catch englobant sera souvent utilisé… Par contre, impossible ici de renvoyer l'exception (ou une autre), il va nous falloir simplement « interrompre » l'utilisateur, et lui dire pourquoi.

Il y a plusieurs moyens d'y parvenir : utilisation d'un « Toast », d'une « Boite de dialogue », d'une TextView…

Libre à vous de choisir la meilleure implémentation, mais dans tous les cas, ce devra être fait. Il est très ennuyeux de ne pas réaliser l'action demandée par l'utilisateur et de ne pas lui dire pourquoi.

Bien entendu, le message à l'utilisateur doit être simple, concis, explicite, sans obligatoirement fournir tous les détails. C'est pour cette raison qu'il est impératif de « loguer » l'exception entière !

Ainsi notre fonction login (hormis le fait qu'elle enverrait un « NetworkOnMainThreadException » dans ce cas-là) pourrait être utilisée comme suit :

 
Sélectionnez
public void onClick(View v)
{
    String username = usernameEdit.getText().toString();
    String password = passwordEdit.getText().toString();
    try {
        this.authToken = login(username,password);
    } catch (IOException ex) {
        Log.w("LOGIN","Login network error !",ex);
        Toast.makeToast(this,R.string.networkError,Toast.TOAST_LONG).show();
    } catch (AuthenticationException ex) {
        Log.i("LOGIN","Login failure !",ex);
        Toast.makeToast(this,R.string.invalidAuthentication,Toast.TOAST_LONG).show();
    }
}

Comme on le voit, les deux exceptions ne produiront pas le même message (localisé) pour l'utilisateur, et dans les deux cas un « log » sera produit, avec l'exception entière, une simple information pour l'authentification non valide (erreur « normale »), et un warning dans l'autre (erreur « anormale » n'empêchant pas toutefois le programme de marcher).

III-B. L'AsyncTask

Le cas de l'AsyncTask est relativement simple, puisqu'il fournit déjà le framework pour « repasser » dans le mode UI (et donc afficher un texte à l'utilisateur).

On verra donc un code dans le style :

 
Sélectionnez
class LoginTask extends AsyncTask<LoginData,Void,String>
{
    private Exception error;
    protected String doInBackground(LoginData … data) {
        try {
           return login(data[0].getUserName(),data[0].getPassword());
        } catch (Exception ex) {
           Log.w("LOGINTASK","Failed to login",ex);
           this.error = ex;
           return null;
        }
    }

    public void onPostExecute(String token) {
        if (this.error == null) {
           // authentication success !
        } else {
           // authentication failed ! (error in this.error)
        }
    }
}

III-C. Les threads & Runnable

Tous les « Runnable » doivent voir leur fonction « run() » entièrement protégée. Un manquement à cette règle provoquera une mort prématurée du thread, sans que quiconque soit au courant.

Comme dans le cas des AsyncTask, il vous faudra certainement stocker l'erreur en tant qu'état du thread, afin de pouvoir récupérer l'erreur et la propager par la suite (à l'utilisateur peut-être).

Par contre le passage de cette information au gestionnaire du Runnable risque d'être plus ardu (messages par Handlers, etc.).

IV. Cas particuliers

IV-A. Base de données/SQLite

IV-A-1. Exceptions SQLiteException

Les appels à SQLite renvoient systématiquement des exceptions de type SQLException (ou SQLiteException). Ces exceptions sont « runtime », donc Eclipse ne vous avertira jamais qu'une fonction risque de lancer une exception SQLiteException.

Cela peut simplifier le code, mais c'est (à mon avis) plus un risque de voir son application planter (« Désolé, l'application xxxx a dû être fermée ») qu'autre chose.

Il va donc vous falloir faire particulièrement attention lors de l'utilisation des bases de données. D'autant que si les SQLiteException signalant une erreur d'appel (mauvais SQL, colonne manquante…) peuvent rester « runtime », il n'en est pas de même pour les exceptions signalant un problème de données (contraintes).

IV-A-2. Utiliser les transactions

La gestion du « retour » à l'état initial en base de données est grandement simplifiée par l'existence des transactions. Leur utilisation devrait être obligatoire à chaque fois que l'on compte modifier une base de données (insert, update, delete)…

Les transactions utilisent le principe de l'état temporaire (voir paradigme des transactions ci-avant). Durant une transaction, vous pouvez faire toutes les modifications que vous désirer sur la base de données, rien ne sera définitif, et seulement à la fin de la transaction, les modifications sont toutes validées en une fois, ou aucune (en cas d'erreur).

Le code suivant sera, par exemple, certain de remettre la base de données à son état initial :

 
Sélectionnez
public void ecriture(...)
{
    database.beginTransaction();
    try {
        … quelque chose qui risque de lever une exception (comme une écriture en base de données) …
        database.setTransactionSuccessful();
     } finally {
        database.endTransaction();
     }
}

En cas d'exception, la base de données ne sera pas modifiée.

Si tout se déroule correctement, toutes les opérations faites dans le bloc try seront validées/sérialisées par la base de données.

IV-B. Les web service

L'utilisation d'un web service (basé sur HTTP donc), utilise plusieurs « niveaux » d'erreur, qui devront tous être traduits en exceptions. Nous l'avons vu dans notre exemple de fonction, mais revenons ici sur les « cas différents » :

IV-B-1. Échec de la communication réseau

C'est sans doute le cas le plus embêtant, car ces erreurs ne sont détectées qu'après un « timeout », assez long en général. La couche réseau va probablement envoyer une exception de type IOException. Il n'y a donc pas grand-chose à faire.

IV-B-2. Échec du web service lui-même

Il se peut que le web service ne soit pas parfaitement au point, et que de ce fait un erreur HTTP 500+ soit levée. Là encore, pas grand-chose à faire côté client (puisque c'est un problème du web service lui-même). Vous pourrez donc traduire cette erreur en IOException sans hésitation puisque le problème est lié à la communication elle-même.

IV-B-3. Échec de l'appel au web service

Il se peut que l'appel au web service échoue parce que vous l'utilisez mal (c'est le cas par exemple du code HTTP 400). Ou pour toute autre raison.

De plus, selon les spécifications du web service, celui-ci peut refuser le traitement demandé, ne pas le faire, et pourtant renvoyer un code HTTP 200 (OK). Il vous faudra alors vérifier dans les postconditions d'appel au web service, comment celui-ci indiquera les erreurs éventuelles.

Dans tous les cas, il vous faudra traduire ces erreurs en une exception signifiant que l'opération n'a pas pu aboutir.

Attention aux codes 401 et 403 spécifiquement dédiés aux authentifications et droits d'accès.