Recherche…


Introduction

Plusieurs abus de langage de programmation Java peuvent conduire à un programme pour générer des résultats incorrects malgré la compilation correcte. Ce sujet a pour objectif principal d’énumérer les écueils les plus courants de leurs causes et de proposer la manière correcte d’éviter de tomber dans de tels problèmes.

Remarques

Cette rubrique concerne des aspects spécifiques de la syntaxe du langage Java qui sont susceptibles de générer des erreurs ou qui ne doivent pas être utilisés de certaines manières.

Pitfall - Ignorer la visibilité de la méthode

Même les développeurs Java expérimentés ont tendance à penser que Java ne possède que trois modificateurs de protection. La langue a en fait quatre! Le niveau de visibilité du paquet privé (aka par défaut) est souvent oublié.

Vous devriez faire attention à quelles méthodes vous rendre public. Les méthodes publiques d'une application sont l'API visible de l'application. Cela devrait être aussi petit et compact que possible, surtout si vous écrivez une bibliothèque réutilisable (voir aussi le principe SOLID ). Il est important de considérer de la même manière la visibilité de toutes les méthodes et d’utiliser uniquement l’accès privé protégé ou groupé, le cas échéant.

Lorsque vous déclarez des méthodes qui doivent être privées comme publiques, vous exposez les détails d'implémentation internes de la classe.

Un corollaire de ceci est que vous ne unité tester les méthodes publiques de votre classe - en fait , vous ne pouvez tester les méthodes publiques. C'est une mauvaise pratique d'augmenter la visibilité des méthodes privées simplement pour pouvoir exécuter des tests unitaires contre ces méthodes. Tester des méthodes publiques appelant les méthodes avec une visibilité plus restrictive devrait suffire à tester une API complète. Vous ne devez jamais développer votre API avec davantage de méthodes publiques uniquement pour autoriser les tests unitaires.

Pitfall - Manquer un "break" dans un cas de "switch"

Ces problèmes de Java peuvent être très embarrassants et restent parfois inexplorés jusqu’à la production. Un comportement irréversible dans les instructions de commutation est souvent utile. Cependant, l'absence d'un mot-clé «pause» lorsqu'un tel comportement n'est pas souhaité peut entraîner des résultats désastreux. Si vous avez oublié de mettre un «break» dans «case 0» dans l'exemple de code ci-dessous, le programme écrira «Zero» suivi de «One», car le flux de contrôle à l'intérieur il atteint une «pause». Par exemple:

public static void switchCasePrimer() {
        int caseIndex = 0;
        switch (caseIndex) {
            case 0:
                System.out.println("Zero");
            case 1:
                System.out.println("One");
                break;
            case 2:
                System.out.println("Two");
                break;
            default:
                System.out.println("Default");
        }
}

Dans la plupart des cas, la solution la plus propre consisterait à utiliser des interfaces et à déplacer le code avec un comportement spécifique dans des implémentations distinctes ( composition sur héritage )

Si une instruction switch est inévitable, il est recommandé de documenter les retombées "attendues" si elles se produisent. De cette façon, vous montrez aux autres développeurs que vous êtes au courant de la rupture manquante et que cela est un comportement attendu.

switch(caseIndex) {
    [...]
    case 2:
        System.out.println("Two");
        // fallthrough
    default:
        System.out.println("Default");

Pitfall - Les points-virgules mal placés et les accolades manquantes

C'est une erreur qui crée une véritable confusion pour les débutants de Java, du moins la première fois qu'ils le font. Au lieu d'écrire ceci:

if (feeling == HAPPY)
    System.out.println("Smile");
else
    System.out.println("Frown");

ils écrivent accidentellement ceci:

if (feeling == HAPPY);
    System.out.println("Smile");
else
    System.out.println("Frown");

et sont perplexes lorsque le compilateur Java leur dit que le else est mal placé. Le compilateur Java avec interpréter ce qui suit comme suit:

if (feeling == HAPPY)
    /*empty statement*/ ;
System.out.println("Smile");   // This is unconditional
else                           // This is misplaced.  A statement cannot
                               // start with 'else'
System.out.println("Frown");

Dans d'autres cas, il n'y aura pas d'erreurs de compilation, mais le code ne fera pas ce que le programmeur a l'intention de faire. Par exemple:

for (int i = 0; i < 5; i++);
    System.out.println("Hello");

n'imprime que "Hello" une fois. Encore une fois, le faux point-virgule signifie que le corps de la boucle for est une instruction vide. Cela signifie que l'appel println suivant est inconditionnel.

Une autre variante:

for (int i = 0; i < 5; i++);
    System.out.println("The number is " + i);

Cela donnera une erreur "Impossible de trouver le symbole" pour i . La présence du point-virgule erroné signifie que l'appel println tente d'utiliser i dehors de son champ d'application.

Dans ces exemples, il existe une solution simple: supprimez simplement le demi-point erroné. Cependant, il y a des leçons plus profondes à tirer de ces exemples:

  1. Le point-virgule en Java n'est pas un "bruit syntaxique". La présence ou l'absence d'un point-virgule peut changer le sens de votre programme. Ne vous contentez pas de les ajouter à la fin de chaque ligne.

  2. Ne faites pas confiance à l'indentation de votre code. En langage Java, le compilateur ignore les espaces blancs en début de ligne.

  3. Utilisez un pénétrateur automatique. Tous les IDE et de nombreux éditeurs de texte simples comprennent comment mettre correctement en retrait le code Java.

  4. C'est la leçon la plus importante. Suivez les dernières directives de style Java et placez des accolades autour des instructions "then" et "else" et de la déclaration de corps d'une boucle. L'attache ouverte ( { ) ne doit pas figurer sur une nouvelle ligne.

Si le programmeur suivait les règles de style, l'exemple if avec des points-virgules égarés ressemblerait à ceci:

if (feeling == HAPPY); {
    System.out.println("Smile");
} else {
    System.out.println("Frown");
}

Cela semble étrange à un œil expérimenté. Si vous indentez automatiquement ce code, cela ressemblera probablement à ceci:

if (feeling == HAPPY); {
                           System.out.println("Smile");
                       } else {
                           System.out.println("Frown");
                       }

qui devrait se démarquer même mal pour un débutant.

Pitfall - Quitter les accolades: les problèmes de "pendants si" et de "pendants"

La dernière version du guide de style Java Oracle exige que les instructions "then" et "else" d'une instruction if soient toujours placées entre "accolades" ou "accolades". Des règles similaires s'appliquent aux corps des différentes instructions de boucle.

if (a) {           // <- open brace
    doSomething();
    doSomeMore();
}                  // <- close brace

Ce n'est pas réellement requis par la syntaxe du langage Java. En effet, si la partie "alors" d’une déclaration if est une déclaration unique, il est légal de laisser de côté les accolades

if (a)
    doSomething();

ou même

if (a) doSomething();

Cependant, il y a des dangers à ignorer les règles de style Java et à laisser de côté les accolades. Plus précisément, vous augmentez considérablement le risque que le code avec une indentation erronée soit mal interprété.

Le problème du "dangling if":

Considérez l'exemple de code ci-dessus, réécrit sans accolades.

if (a)
   doSomething();
   doSomeMore();

Ce code semble dire que les appels à doSomething et doSomeMore se produiront tous les deux si et seulement si a est true . En fait, le code est indenté incorrectement. La spécification de langage Java que l'appel doSomeMore() est une instruction distincte suivant l'instruction if . L'indentation correcte est la suivante:

if (a)
   doSomething();
doSomeMore();

Le problème du "dangling else"

Un deuxième problème apparaît lorsque l' on ajoute le else au mélange. Prenons l'exemple suivant avec des accolades manquantes.

if (a)
   if (b)
      doX();
   else if (c)
      doY(); 
else
   doZ();

Le code ci-dessus semble dire que doZ sera appelé quand a est false . En fait, l'indentation est incorrecte encore une fois. L'indentation correcte du code est la suivante:

if (a)
   if (b)
      doX();
   else if (c)
      doY(); 
   else
      doZ();

Si le code était écrit conformément aux règles de style Java, cela ressemblerait à ceci:

if (a) {
   if (b) {
      doX();
   } else if (c) {
      doY(); 
   } else {
      doZ();
   }
}

Pour illustrer pourquoi cela est mieux, supposez que vous ayez accidentellement induit le code en erreur. Vous pourriez vous retrouver avec quelque chose comme ça:

if (a) {                         if (a) {
   if (b) {                          if (b) {
      doX();                            doX();
   } else if (c) {                   } else if (c) {
      doY();                            doY();
} else {                         } else {
   doZ();                            doZ();
}                                    }
}                                }

Mais dans les deux cas, le code mal indenté "semble erroné" aux yeux d'un programmeur Java expérimenté.

Piège - Surcharge au lieu de dépasser

Prenons l'exemple suivant:

public final class Person {
    private final String firstName;
    private final String lastName;
   
    public Person(String firstName, String lastName) {
        this.firstName = (firstName == null) ? "" : firstName;
        this.lastName = (lastName == null) ? "" : lastName;
    }

    public boolean equals(String other) {
        if (!(other instanceof Person)) {
            return false;
        }
        Person p = (Person) other;
        return firstName.equals(p.firstName) &&
                lastName.equals(p.lastName);
    }

    public int hashcode() {
        return firstName.hashCode() + 31 * lastName.hashCode();
    }
}

Ce code ne va pas se comporter comme prévu. Le problème est que les méthodes equals et hashcode pour Person ne remplacent pas les méthodes standard définies par Object .

  • La méthode equals a la mauvaise signature. Il doit être déclaré comme equals(Object) non equals(String) .
  • La méthode de hashcode n'a pas le bon nom. Ce devrait être hashCode() (notez le C majuscule).

Ces erreurs signifient que nous avons déclaré des surcharges accidentelles, et celles-ci ne seront pas utilisées si Person est utilisé dans un contexte polymorphe.

Cependant, il existe un moyen simple de gérer cela (à partir de Java 5). Utilisez la @Override annotation chaque fois que vous avez l' intention de votre méthode pour être un remplacement:

Java SE 5
public final class Person {
    ...

    @Override
    public boolean equals(String other) {
        ....
    }

    @Override
    public hashcode() {
        ....
    }
}

Lorsque nous ajoutons une @Override annotation à une déclaration de méthode, le compilateur vérifiera que la méthode ne remplace (ou mettre en œuvre) une méthode déclarée dans une superclasse ou de l' interface. Ainsi, dans l'exemple ci-dessus, le compilateur nous donnera deux erreurs de compilation, ce qui devrait suffire à nous alerter de l'erreur.

Piège - littéraux octaux

Considérez l'extrait de code suivant:

// Print the sum of the numbers 1 to 10
int count = 0;
for (int i = 1; i < 010; i++) {    // Mistake here ....
    count = count + i;
}
System.out.println("The sum of 1 to 10 is " + count);

Un débutant Java pourrait être surpris de savoir que le programme ci-dessus imprime la mauvaise réponse. Il affiche en fait la somme des chiffres 1 à 8.

La raison en est qu'un littéral entier qui commence par le chiffre zéro ('0') est interprété par le compilateur Java comme un littéral octal, et non comme un littéral décimal. Ainsi, 010 est le nombre octal 10, qui est 8 en décimal.

Piège - Déclaration de classes avec les mêmes noms que les classes standard

Parfois, les programmeurs novices en Java font l'erreur de définir une classe avec un nom identique à une classe largement utilisée. Par exemple:

package com.example;

/**
 * My string utilities
 */
public class String {
    ....
}

Ensuite, ils se demandent pourquoi ils obtiennent des erreurs inattendues. Par exemple:

package com.example;

public class Test {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

Si vous compilez puis essayez d'exécuter les classes ci-dessus, vous obtiendrez une erreur:

$ javac com/example/*.java
$ java com.example.Test
Error: Main method not found in class test.Test, please define the main method as:
   public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application

Quelqu'un qui regarde le code de la classe Test verra la déclaration de main et regardera sa signature et se demandera de quoi la commande java se plaint. Mais en fait, la commande java dit la vérité.

Lorsque nous déclarons une version de String dans le même package que Test , cette version est prioritaire sur l'importation automatique de java.lang.String . Ainsi, la signature de la méthode Test.main est en fait

void main(com.example.String[] args) 

au lieu de

void main(java.lang.String[] args)

et la java commande ne reconnaît pas que comme une méthode de point d' entrée.

Leçon: Ne définissez pas les classes qui portent le même nom que les classes existantes dans java.lang ou d'autres classes couramment utilisées dans la bibliothèque Java SE. Si vous faites cela, vous vous exposez à toutes sortes d'erreurs obscures.

Pitfall - Utiliser '==' pour tester un booléen

Parfois, un nouveau programmeur Java écrit un code comme celui-ci:

public void check(boolean ok) {
    if (ok == true) {           // Note 'ok == true'
        System.out.println("It is OK");
    }
}

Un programmeur expérimenté le trouverait maladroit et voudrait le réécrire comme suit:

public void check(boolean ok) {
    if (ok) {
       System.out.println("It is OK");
    }
}

Cependant, il y a plus de tort avec ok == true que la simple maladresse. Considérez cette variation:

public void check(boolean ok) {
    if (ok = true) {           // Oooops!
        System.out.println("It is OK");
    }
}

Ici, le programmeur a mal interprété == comme = ... et maintenant le code a un bogue subtil. L'expression x = true assigne inconditionnellement true à x et évalue ensuite true . En d'autres termes, la méthode de check affichera désormais "Il est correct", quel que soit le paramètre.

La leçon ici est de sortir de l'habitude d'utiliser == false et == true . En plus d'être verbeux, ils rendent votre codage plus susceptible aux erreurs.


Note: Une alternative possible à ok == true qui évite le piège est d'utiliser les conditions de Yoda ; c'est-à-dire mettre le littéral à gauche de l'opérateur relationnel, comme dans true == ok . Cela fonctionne, mais la plupart des programmeurs seraient probablement d'accord pour dire que les conditions de Yoda semblent étranges. Certes, ok (ou !ok ) est plus concis et plus naturel.

Pitfall - Les importations de Wildcard peuvent rendre votre code fragile

Prenons l'exemple partiel suivant:

import com.example.somelib.*;
import com.acme.otherlib.*;

public class Test {
    private Context x = new Context();   // from com.example.somelib
    ...
}

Supposons que lorsque vous avez développé le code pour la première fois avec la version 1.0 de somelib et la version 1.0 de otherlib . Ensuite, vous devrez mettre à niveau vos dépendances vers des versions ultérieures et décider d’utiliser otherlib version 2.0. Supposons également que l'une des modifications apportées à otherlib entre 1.0 et 2.0 consistait à ajouter une classe de Context .

Maintenant, lorsque vous recompilez Test , vous obtenez une erreur de compilation indiquant que le Context est une importation ambiguë.

Si vous êtes familier avec la base de code, cela représente probablement un inconvénient mineur. Sinon, vous avez du travail à faire pour résoudre ce problème, ici et potentiellement ailleurs.

Le problème ici est les importations de caractères génériques. D'une part, l'utilisation de caractères génériques peut rendre vos cours plus courts. D'autre part:

  • Des modifications compatibles vers le haut vers d'autres parties de votre base de code, vers des bibliothèques standard Java ou vers des bibliothèques tierces peuvent entraîner des erreurs de compilation.

  • La lisibilité en souffre. À moins d'utiliser un IDE, il peut être difficile de déterminer les importations de caractères génériques dans une classe nommée.

La leçon est que c'est une mauvaise idée d'utiliser des importations de caractères génériques dans un code qui doit durer longtemps. Les importations spécifiques (non génériques) ne nécessitent pas beaucoup d'efforts si vous utilisez un IDE, et l'effort en vaut la peine.

Piège: Utiliser 'assert' pour la validation des arguments ou des entrées utilisateur

Une question qui se pose parfois sur StackOverflow est de savoir s’il convient d’utiliser assert pour valider les arguments fournis à une méthode, ou même les entrées fournies par l’utilisateur.

La réponse simple est que ce n'est pas approprié.

Les meilleures alternatives incluent:

  • Lancer une exception IllegalArgumentException à l'aide du code personnalisé.
  • Utilisation des méthodes Preconditions disponibles dans la bibliothèque Google Guava.
  • Utilisation des méthodes Validate disponibles dans la bibliothèque Apache Commons Lang3.

Voici ce que la spécification de langage Java (JLS 14.10, pour Java 8) conseille à cet égard:

En règle générale, la vérification des assertions est activée lors du développement et du test du programme et désactivée pour le déploiement afin d'améliorer les performances.

Les assertions pouvant être désactivées, les programmes ne doivent pas supposer que les expressions contenues dans les assertions seront évaluées. Ainsi, ces expressions booléennes devraient généralement être exemptes d’effets secondaires. L'évaluation d'une telle expression booléenne ne devrait affecter aucun état visible une fois l'évaluation terminée. Il n'est pas illégal qu'une expression booléenne contenue dans une assertion ait un effet secondaire, mais elle est généralement inappropriée, car elle pourrait faire varier le comportement du programme selon que les assertions ont été activées ou désactivées.

À la lumière de cela, les assertions ne doivent pas être utilisées pour vérifier les arguments dans les méthodes publiques. La vérification des arguments fait généralement partie du contrat d'une méthode et ce contrat doit être respecté, que les assertions soient activées ou désactivées.

Un problème secondaire lié à l'utilisation d'assertions pour la vérification d'arguments est que des arguments erronés doivent entraîner une exception d'exécution appropriée (telle IllegalArgumentException , ArrayIndexOutOfBoundsException ou NullPointerException ). Un échec d'assertion ne déclenchera pas une exception appropriée. Encore une fois, il n’est pas illégal d’utiliser des assertions pour vérifier les arguments sur des méthodes publiques, mais cela est généralement inapproprié. Il est prévu que AssertionError ne soit jamais intercepté, mais il est possible de le faire, donc les règles pour les instructions try doivent traiter les assertions apparaissant dans un bloc try de la même manière que le traitement actuel des instructions throw.

Piège des objets nuls auto-unboxing dans les primitifs

public class Foobar {
    public static void main(String[] args) {

        // example: 
        Boolean ignore = null;
        if (ignore == false) {
            System.out.println("Do not ignore!");
        }
    }
}

Le piège est que null est comparé à false . Étant donné que nous comparons un boolean primitif à un Boolean , Java tente de déballer l' Object Boolean dans un équivalent primitif, prêt pour la comparaison. Cependant, puisque cette valeur est null , une NullPointerException est levée.

Java est incapable de comparer les types primitifs aux valeurs null , ce qui provoque une NullPointerException à l'exécution. Considérons le cas primitif de la condition false == null ; Cela générerait une erreur de compilation de incomparable types: int and <null> .



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow