BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles Prestidigitation Java

Prestidigitation Java

De temps en temps, nous rencontrons tous du code dont le comportement est inattendu. Le langage Java contient beaucoup de particularités, et même les dévelopeurs expérimentés peuvent être pris par surprise.

Soyons honnêtes, nous avons tous eu un collègue junior qui est venu nous voir pour demander "quel est le résultat de l'exécution de ce code ?", nous prenant au dépourvu. Maintenant, au lieu de l'usuel "je pourrais te répondre mais je pense qu'il serait plus éducatif que tu le découvres par toi-même", nous pourrions le distraire un moment ("mmh, je crois que j'ai vu Angelina Jolie se cacher derrière notre serveur de build, tu peux aller jeter un coup d'oeil rapidement ?") pendant que nous parcourons en catimini cet article.

"Java Sleigh of Hand" va présenter certaines de ces particularités, afin d'aider les développeurs à mieux se préparer à faire face à des portions de code produisant des résultats inattendus.

Chaque "tour" expose un code qui paraît simple, mais dont le comportement à la compilation et/ou à l'exécution n'est pas banal. Chacun d'eux fera la lumière sur la logique derrière le pourquoi et le comment. Le niveau de complexité va des simples remarques aux casse-tête plus sérieux.

Identificateurs fous

Nous sommes familiers avec les règles qui définissent un identificateur Java valide :

  • Un identificateur est un ensemble d'un ou plusieurs caractères constitué de lettres, chiffres, symboles de devise ou underscores (_).
  • Un identificateur doit commencer par une lettre, un symbole de devise ou un underscore.
  • Les mots-clés Java ne peuvent pas être utilisés comme identificateurs.
  • Il n'y a pas de limite au nombre de caractères pouvant être utilisés dans un identificateur.
  • Les symboles Unicode de \u00c0 à \ud7a3 peuvent aussi être utilisés.

Ces règles sont tout à fait simples, mais il existe des cas délicats qui pourraient faire froncer les sourcils. Par exemple, rien n'empêche un développeur d'utiliser un nom de classe comme identificateur de variable :

//Les noms de classe peuvent être utilisés comme nom de variables
String String = "String";
Object Object = null;
Integer Integer = new Integer(1);
//Et si on rendait le code illisible ?
Float Double = 1.0f;
Double Float = 2.0d;
if (String instanceof String) {
      if (Float instanceof Double) {
          if (Double instanceof Float) {
                System.out.print("Can anyone read this code???");
            }
      }
}

Tous les éléments qui suivent sont aussi des identificateurs autorisés :

int $ =1;
int € = 2;
int £ = 3;
int _ = 4;
long $€£ = 5;
long €_£_$ = 6;
long $€£$€£$€£$€£$€£$€£$€_________$€£$€£$€£$€£$€£$€£$€£$€£$€£_____ = 7

De plus, gardez à l'esprit que le même nom peut être utilisé simultanément pour une variable et un label. Le compilateur sait auquel vous faites référence car il analyse le contexte.

int £ = 1;
£: for (int € = 0; € < £; €++) {
     if (€ == £) {
         break £;
     }
}

Et bien sûr, rappelez-vous que les règles pour les identificateurs s'appliquent aux noms de variables, méthodes, labels et classes :

class $ {}
interface _ {}
class € extends $ implements _ {}

Ainsi, vous venez d'apprendre une super manière de créer du code qui soit à peine lisible par qui que ce soit, vous compris !

D'où vient cette NullPointerException ?

L'Autoboxing a été introduit en Java 5 et nous a facilité la vie dans le contexte des allers-retours entre les types primitifs et leurs équivalents objet (les wrappers) :

int primitiveA = 1;
Integer wrapperA = primitiveA;
wrapperA++;
primitiveA = wrapperA;

Le Runtime ne s'est pas particulièrement adapté à ces changements, le gros du travail étant réalisé par le compilateur. Celui-ci inspecterait le fragment de code précédent et produirait quelque-chose comme :

int primitiveA = 1;
Integer wrapperA = new Integer(primitiveA);
int tmpPrimitiveA = wrapperA.intValue();
tmpPrimitiveA++;
wrapperA = new Integer(tmpPrimitiveA);
primitiveA = wrapperA.intValue();

L'autoboxing s'applique aussi évidemment aux invocations de méthodes :

public static int calculate(int a) {
     int result = a + 3;
     return result;
}
public static void main(String args[]) {
     int i1 = 1;
     Integer i2 = new Integer(1);
     System.out.println(calculate(i1));
     System.out.println(calculate(i2));
}

C'est super, nous pouvons passer des wrappers de classe Number à toutes nos méthodes qui prennent des types primitifs en paramètre, et laisser le compilateur faire la traduction :

public static void main(String args[]) {
     int i1 = 1;
     Integer i2 = new Integer(1);
     System.out.println(calculate(i1));
     int i2Tmp = i2.intValue();
     System.out.println(calculate(i2Tmp));
}

Essayons maintenant ce code avec une légère variation :

public static void main(String args[]) {
     int i1 = 1;
     Integer i2 = new Integer(1);
     Integer i3 = null;
     System.out.println(calculate(i1));
     System.out.println(calculate(i2));
     System.out.println(calculate(i3));
}

Comme vu précédemment, ce code se transforme en :

public static void main(String args[]) {
    int i1 = 1;
     Integer i2 = new Integer(1);
     System.out.println(calculate(i1));
     int i2Tmp = i2.intValue();
     System.out.println(calculate(i2Tmp));
     int i3Tmp = i3.intValue();
     System.out.println(calculate(i3Tmp));
}

Et bien entendu, ce code nous amène à notre vieille amie la NullPointerException. De même avec quelque chose d'encore plus simple :

public static void main(String args[]) {
     Integer iW = null;
     int iP = iW;
}

Donc faites vraiment attention dans votre utilisation de l'autoboxing. Elle peut conduire à des NullPointerExceptions dans le code à des endroits où c'était impossible avant que cette fonctionnalité ne soit introduite. Pire, il n'est pas toujours facile d'identifier ce type de code. Si vous devez convertir un type enveloppant vers un type primitif, et que vous n'êtes pas certain que le wrapper puisse être null, protégez votre code !

Mes Wrappers ont une crise d'identité...

Continuons avec l'autoboxing et voyons le code suivant :

Short s1 = 1;
Short s2 = s1;
System.out.println(s1 == s2);

Il affiche tout naturellement "true". Maintenant, rendons-le un peu plus intéressant :

Short s1 = 1;
Short s2 = s1;
s1++;
System.out.println(s1 == s2);

La sortie passe à "false". Une minute ! S1 et s2 ne devraient-ils pas référencer le même objet ? La JVM est folle !

Appliquons le processus de traduction vu dans le précédent tour.

Short s1 = new Short((short1);
Short s2 = s1;
short tempS1 = s1.shortValue();
tempS1++;
s1 = new Short(tempS1);
System.out.println(s1 == s2);

Hmmm... c'est plus logique maintenant, n'est-ce pas ? Soyez toujours très prudent avec l'autoboxing !

Regarde Maman ! Pas d'exceptions !

Celui-là est vraiment simple, mais il prend certains développeurs Java expérimentés par surprise. Regardons le code suivant :

NullTest myNullTest = null;
System.out.println(myNullTest.getInt());

Devant ce code, la plupart des gens supposeraient qu'il cause une NullPointerException. Est-ce le cas ? Voyons le reste du code :

class NullTest {
  public static int getInt() {
    return 1;
  }
}

Rappelez-vous toujours que l'usage de variables et méthodes de classe dépend uniquement du type de la référence. Donc, même si votre référence est null, vous pouvez quand même invoquer ces derniers. En termes de bonnes pratiques, il est avisé d'utiliser NullTest.getInt() au lieu de myNullTest.getInt()`, mais on ne sais jamais quand on va tomber sur ce genre de code.

Varargs et Tableaux, Mutatis Mutandis

La fonctionnalité d'arguments variables a introduit un concept puissant qui a aidé les développeurs à simplifier leur code. Néanmoins, sous le capot, les varargs ne sont rien de plus et rien de moins que des tableaux.

public void calc(int... myInts) {}
calc(123);

Le code qui précède est transformé par le compilateur en quelque-chose comme :

int[] ints = {123};
calc(ints);

Les invocations vides reviennent à passer un tableau vide en paramètre.

calc();
//is equivalent to
int[] ints = new int[0];
calc(ints);

Et bien sûr, ce qui suit causera une erreur de compilation vu que les deux méthodes représentent des signatures équivalentes :

public void m1(int[] myInts) { ...&nbsp;&nbsp;&nbsp; }
public void m1(int... myInts) { ...&nbsp;&nbsp;&nbsp; }

Constantes modifiables

La plupart des développeurs supposent que dès que le mot-clé final est utilisé sur une variable, il indique une constante, c'est à dire une variable qui se voit assigner une valeur de manière immuable. Cela n'est pas tout à fait correct. Le mot-clé final appliqué à une variable indique que celle-ci ne peut se voir assigner une valeur qu'une unique fois.

class MyClass {
  private final int myVar;
  private int myOtherVar = getMyVar();
  public MyClass() {
    myVar = 10;
  }
  public int getMyVar() {
    return myVar;
  }
  public int getMyOtherVar() {
    return myOtherVar;
  }
  public static void main(String args[]) {
    MyClass mc = new MyClass();
    System.out.println(mc.getMyVar());
    System.out.println(mc.getMyOtherVar());
  }
}

Le code qui précède va afficher la séquence 10 0. De fait, quand on traite avec des variables "final", nous devons distinguer celles qui ont une valeur par défaut assignée à la compilation, qui fonctionnent comme des constantes, de celles qui ont leur valeur initialisée à l'exécution.

Subtilités de la Redéfinition (Overriding)

Gardez à l'esprit que depuis Java 5, il est possible qu'une méthode redéfinie ait un type de retour différent de la méthode d'origine. La seule règle est que le type de retour de la méthode redéfinie soit un sous-type de celui de la méthode d'origine. Ainsi, le code suivant est devenu valide avec Java 5 :

class A {
  public A m() {
    return new A();     }
  }

  class B extends A {
    public B m() {
      return new B();
    }
  }
}

Surchargez cet opérateur !

La surcharge d'opérateurs n'est pas particulièrement un point fort de Java, mais il la supporte dans une moindre mesure pour l'opérateur '+'. Nous pouvons l'utiliser à la fois pour l'addition mathématique et la concaténation de chaînes de caractères selon le contexte.

int val = 1 + 2;
String txt = "1" + "2";

Cela devient compliqué dès qu'une valeur numérique se mélange à une chaîne. Mais la règle est simple : une addition mathématique sera effectuée jusqu'à ce qu'une chaîne soit rencontrée dans les opérandes. Dès qu'une chaîne est trouvée, les deux opérandes sont converties en chaînes (si c'est nécessaire) et une concaténation est effectuée. L'exemple suivant illustre les différentes combinaisons.

System.out.println(1 + 2); //Performs addition and prints 3

System.out.println("1" + "2"); //Performs concatenation and prints 12
System.out.println(1 + 2 + 3 + "4" + 5); //Performs addition until "4" is found and then concatenation, prints 645

System.out.println("1" + "2" + "3" + 4 + 5); //Performs concatenation and prints 12345

Formatage de Date Retors

Cette astuce est liée à la manière dont l'implémentation DateFormat fonctionne, comment son usage peut être déroutant et comment les problèmes peuvent parfois se dissimuler jusqu'à l'arrivée du code en production.

La méthode parse de DateFormat analyse une String et produit une Date. L'analyse est faite selon le masque de date défini. D'après la JavaDoc, la méthode déclenche une ParseException dès que "le début de la chaîne spécifiée ne peut être analysé". Cette définition est très vague et autorise des interprétations multiples. La plupart des développeurs supposent que si la chaîne passée en paramètre ne correspond pas au format défini, alors une ParseException est déclenchée. Cela n'est pas toujours le cas.

On devrait toujours faire très attention avec le SimpleDateFormat. En voyant le code suivant, la plupart des développeurs penseraient qu'une ParseException sera déclenchée.

String date = "16-07-2009";

SimpleDateFormat sdf = new SimpleDateFormat("ddmmyyyy");
try {
  Date d = sdf.parse(date);
  System.out.println(DateFormat.getDateInstance(DateFormat.MEDIUM,
  new Locale("US")).format(d));
} catch (ParseException pe) {
  System.out.println("Exception: " + pe.getMessage());
}

Exécuté, le code produit la sortie suivante : "Jan 16, 0007". De manière surprenante, il n'y a pas de plainte sur le fait que la chaîne ne corresponde pas au format attendu - l'implémentation se contente de continuer, essayant de son mieux d'analyser le texte. Notez qu'il y a deux comportements peu évidents. D'abord, le masque pour le mois est MM tandis que mm est utilisé pour les minutes, et cela explique pourquoi le mois est initialisé à Janvier. Ensuite, la méthode parse de DecimalFormat va formater le texte jusqu'à ce qu'un caractère non géré soit trouvé, retournant le nombre analysé jusqu'à ce point. Ainsi, "7-20" va se traduire par l'année 7. Cette divergence pourrait être facilement identifiable, mais cela devient plus compliqué si "yyyymmdd" est utilisé, car la sortie serait alors "Jan 7, 0016". "16-0" est analysé jusqu'au premier caractère non analysable, se traduisant par une année de 16. Ensuite "-0" n'impacte plus le résultat puisqu'il est interprété comme "0 minutes". Enfin, "7-" se traduirait par le 7ème jour.

A propos de l'Auteur

Paulo Moreira est un consultant développeur indépendant Portugais, travaillant actuellement dans le secteur de la finance au Luxembourg. Diplômé de l'Université de Minho avec un Master de Science en Informatique et Ingénierie des Systèmes, il travaille depuis 2001 avec Java côté serveur dans les secteurs des télécoms, de la vente en ligne, du logiciel et de la finance.

Evaluer cet article

Pertinence
Style

Contenu Éducatif

BT