Introduction
Caractéristiques de Java
-
Typage statique, mais :
- référence nulle possible (contrairement au C++)
- cast dynamique possible
- Support des exceptions : permet de remplacer la valeur de retour d'une méthode pour des cas peu courants
Quelques conseils pour une programmation plus sûre
- Éviter la duplication de code : préférer la création de nouvelles méthodes ou classes au copier-coller
- Vérifier les données en entrée de provenance externe pour éviter les failles de sécurité ou déni de service
-
Tester son code :
- Vérifier le fonctionnement interne de méthodes par des assertions
- Réaliser des tests unitaires pour vérifier le comportement de méthodes et classes (test de valeurs de sortie pour certaines valeurs d'entrée)
Usage des esceptions
- Remplace une valeur de retour d'une méthode pour des cas de survenue rare
-
Plutôt que de retourner une valeur : return maValeur;
- on lève une exception : throw new MyException(arg1, arg2, ..., argN);
- Une exception est un objet standard Java instantiable via une classe
-
Toutes les exceptions ont pour ancêtre commun Throwable
- Throwable a pour sous-classes Error et Exception
Error
- Les exceptions dérivées de Error n'ont pas vocation à être capturées (évènements anormaux exceptionnels de la JVM)
-
Quelques sous-classes d'Error :
- AssertionError pour des assertions non satisfaites
- LinkageError pour l'appel d'une méthode sur une classe modifiée
-
VirtualMachineError lorsque la JVM a un problème :
- OutOfMemoryError s'il n'y a plus d'espace libre pour de nouvelles allocations sur le tas
- StackOverflowError si la pile est trop remplie
- ...
Débordement de pile ou surcharge du tas ?
package fr.upem.jacosa.safety; public class Overflowing { public static void recursiveMethod(int k) { byte[] tab = new byte[k]; recursiveMethod(k); tab[0] = 1; } public static void main(String[] args) { int k = Integer.parseInt(args[0]); recursiveMethod(k); } }
Cela dépend de k, de la taille de la pile et de la mémoire allouée pour le tas
StackOverflowError et OutOfMemoryError
StackOverflowError
(Presque) toujours une erreur de programmation généralement liée à :
- Une méthode récursive sans condition d'arrêt (récursion infinie)
- Une méthode avec trop de variables locales (plus rare)
Toutefois le changement de la taille de la pile est possible : java -Xss10M ... pour une pile de 10 Mo par exemple
OutOfMemoryError
Programme trop gourmand en allocation mémoire sur le tas
Solutions :
- Être astucieux pour économiser de la mémoire
-
Ou si ce n'est pas possible, augmenter la taille du tas :
- java -Xms1G -Xmx2G par exemple pour un tas initialement de 1 Go pouvant être étendu jusqu'à 2G
Classe Exception
- Toutes les exceptions dont la capture a un potentiel intérêt dérivent de cette classe
-
Quelques exemples de sous-classes de Exception :
- IOException : pour des erreurs d'E/S sur des flots
- SAXException, XMLParseException : exceptions de parsing d'un fichier XML
- ...
- RuntimeException : il s'agit d'exceptions dites non-vérifiées (unchecked) dont la capture est facultative
Classe RuntimeException : la mère de (presque) toutes les exceptions non-vérifiées
- NullPointerException : exception la plus courante lorsque l'on accède à une référence nulle
- ClassCastException : lorsqu'un cast dynamique échoue (survient généralement lorsque l'on a oublié un instanceof)
- IllegalArgumentException : la valeur d'un ou plusieurs arguments d'une méthode ne sont pas valides (on indique dans la Javadoc de la méthode les valeurs attendues pour les arguments)
- IllegalStateException : l'objet est dans un état non valide pour réaliser l'opération demandée
- IndexOutOfBoundsException : lorsqu'on accède à un indice non-valide d'un tableau ou liste
- UnsupportedOperationException : utilisé pour indiquer que la fonctionnalité d'une méthode n'est pas implantée (par exemple les itérateurs ont une méthode remove() qui peut lever cette exception
- ...
NullPointerException
- Contrairement à C++, les références d'objet en Java peuvent être nulles
- NullPointerException levée lorsque l'on accède à un champ ou une méthode d'une référence nulle
- 🆕 Depuis Java 14, le message des NullPointerException est plus explicatif (facilite le débuggage) ; activation de la fonction nécessaire avec l'option -XX:+ShowCodeDetailsInExceptionMessages (sera activée par défaut dans Java 15)
-
Comment l'éviter ?
- En testant préalablement la nullité de la référence : if (a != null) ...
- En n'oubliant pas d'initialiser les champs des objets (astuce : utiliser final pour les champs impose leur initialisation dans le constructeur)
-
Pour comparer des objets, utiliser les méthodes statiques de java.util.Objects :
- static boolean Objects.equals(Object a, Object b)
- static boolean Objects.deepEquals(Object a, Object b)
Les exceptions les plus fréquentes
D'après Takipi, les exceptions les plus fréquentes interrompant un programme sont :
- NullPointerException (70%) : star incontestée des exceptions Java
- NumberFormatException (55%) : attention à la conversion des String en nombre !
- IllegalArgumentException (50%) : arguments non valides pour l'appel d'une méthode
-
RuntimeException (23%) : exception générique fourre-tout non-vérifiée
- Levée lorsque l'on ne souhaite pas créer soi-même une classe pour un nouveau type d'exceptions non-vérifiée
- Utilisée pour transformer une exception vérifiée capturée en exception non-vérifiée
- IllegalStateException (22%) : utilisée pour exprimer l'état non-valide d'un objet lorsqu'une action est réalisée
- NoSuchMethodException (16%) : méthode non-trouvée lors d'une introspection
- ClassCastException (15%) : échec d'une opération de cast (oubli de tester avec l'opérateur instanceof
- Exception (15%) : exception levée lorsque le développeur est trop fainéant pour trouver une exception plus adaptée
- ParseException (13%) : problème d'analyse d'un fichier (XML par exemple)
- InvocationTargetException (13%) : exception levée lors d'un appel de méthode par introspection pour envelopper une exception lancée par la méthode appelée
Vie et mort d'une exception
-
Naisssance
- Par l'instruction throw new MyException(...)
-
Vie
- L'exception est propagée dans la pile d'appel jusqu'à être capturée
-
Mort
- L'exception est capturée par un bloc catch (TypeException e) {} ; TypeException doit être une classe ancêtre de l'exception levée (ou la classe de l'exception elle-même) pour que le bloc puisse la capturer.
-
Dans le catch :
- On fait des opérations diverses pour gérer l'exception
-
Si une exception n'est jamais capturée : la JVM termine le programme et affiche la stack trace
- ☞ la stack trace contient les numéros de ligne des instructions uniquement si la classe est compilée en mode debug javac -g
-
Résurrection
-
Dans le catch :
- Il est possible de relever la même exception (throw e) après avoir exécuté du code
-
On peut également lever une nouvelle exception d'un type différent : on spécifie alors l'ancienne exception comme cause de cette exception:
- On dit que l'on encapsule ou enveloppe l'exception dans une nouvelle exception
- throw new NewException("message", oldException);}
- La pratique de l'encapsulation est courante pour transformer une exception vérifiée en exception non-vérifiée (encapsulation dans RuntimeException)
- La cause d'une exception (exception enveloppée) est consultable avec la méthode getCause()
-
Dans le catch :
try-catch-finally
try { // Appel de méthodes pouvant lever des exceptions } catch (Exception1 e1) { // Pour traiter les exceptions de type Exception1 } catch (Exception2 e2) { // Pour traiter les exceptions de type Exception2 // Exception2 peut être un ancêtre de Exception1 // (exception plus générale) // Par contre Exception2 NE PEUT PAS être // un descendant de Exception1 // (l'exception est capturée // par le premier catch pouvant la capturer) } finally { // Le code dans le bloc finally // est executé inconditionnellement // qu'il y ait eu levée d'exception ou non // Utile pour ne pas oublier de libérer des ressources }
Try with resources
// Il est possible d'instantier des objets au début du try // qui seront fermés automatiquement par l'appel de leur méthode close() // dans l'ordre inverse de leur déclaration // Les classes concernées doivent implanter l'interface AutoCloseable try (InputStream is = new FileInputStream(monFichier); Scanner s = new Scanner(is);) { int i = s.nextInt(); System.out.println(i); } catch (IOException | InputMismatchException | NoSuchElementException e) { // Il est possible de regrouper // plusieurs types d'exceptions dans un même catch // e a pour type statique // l'ancêtre commun de tous ces types (ici Exception) // catch est executé après avoir fermé automatiquement // les ressources System.err.println( "L'entier n'a pas pu être lu à cause d'une exception: " + e.getMessage()); } finally { // Bloc finally exécuté // après la fermeture des ressources // et l'éventuel catch si exception }
Exceptions vérifiées et non-vérifiées
Toutes les exceptions sont vérifiées sauf celles héritant :
- de RuntimeException
- et de Error
Une méthode pouvant lever une exception vérifiée TypeException doit :
- soit la capturer dans un bloc catch,
- soit indiquer qu'elle la propage à la méthode appelante en spécifiant dans sa signature throws TypeException
Créer ses exceptions
- Ne pas abuser du mécanisme (pas un substitut aux valeurs de retours ou aux if-else, switch-case car moins performant)
- Lever des exceptions de classe pré-existante dans le JDK si approprié
- Sinon créer sa propre classe d'exception ayant pour ancêtre la classe la plus appropriée (l'exception doit-elle être vérifiée ou non ?)
- Normalement le code d'une nouvelle classe d'exception doit se limiter à la redéfinition de quelques constructeurs (pour l'instantiation avec un message, avec une exception cause).
L'ampoule cassée
public class BrokenBulbException extends IllegalStateException { private final long since; // When the bulb was broken (milliseconds since the Unix epoch) public BrokenBulbException(String message, Throwable cause, long since) { super(message, cause); this.since = since; } public long getBrokenDate() { return since; } } ... public class LimitedBulb { private boolean alive; private long deathDate; ... public void switchBulb() { updateLiveness(); if (!alive) throw new BrokenBulbException("The bulb is broken", null, deathDate); ... } }