JavaRush /Blog Java /Random-FR /FindBugs vous aide à mieux apprendre Java
articles
Niveau 15

FindBugs vous aide à mieux apprendre Java

Publié dans le groupe Random-FR
Les analyseurs de code statiques sont populaires car ils aident à détecter les erreurs dues à la négligence. Mais ce qui est bien plus intéressant, c’est qu’ils permettent de corriger des erreurs dues à l’ignorance. Même si tout est écrit dans la documentation officielle du langage, ce n’est pas un fait que tous les programmeurs l’ont lu attentivement. Et les programmeurs peuvent comprendre : vous en aurez marre de lire toute la documentation. À cet égard, un analyseur statique est comme un ami expérimenté qui s'assoit à côté de vous et vous regarde écrire du code. Il vous dit non seulement : « C'est là que vous avez fait une erreur en copiant-collant », mais il vous dit aussi : « Non, vous ne pouvez pas écrire comme ça, regardez vous-même la documentation. Un tel ami est plus utile que la documentation elle-même, car il ne suggère que les choses que vous rencontrez réellement dans votre travail et garde le silence sur celles qui ne vous seront jamais utiles. Dans cet article, je parlerai de certaines des subtilités Java que j'ai apprises en utilisant l'analyseur statique FindBugs. Peut-être que certaines choses seront inattendues pour vous aussi. Il est important que tous les exemples ne soient pas spéculatifs, mais soient basés sur du code réel.

Opérateur ternaire ? :

Il semblerait qu’il n’y ait rien de plus simple que l’opérateur ternaire, mais il comporte ses pièges. Je pensais qu’il n’y avait pas de différence fondamentale entre les conceptions Type var = condition ? valTrue : valFalse; et Type var; if(condition) var = valTrue; else var = valFalse; il s’est avéré qu’il y avait ici une subtilité. Puisque l’opérateur ternaire peut faire partie d’une expression complexe, son résultat doit être un type concret déterminé au moment de la compilation. Par conséquent, disons, avec une condition vraie sous la forme if, le compilateur amène valTrue directement au type Type, et sous la forme d'un opérateur ternaire, il mène d'abord aux types communs valTrue et valFalse (malgré le fait que valFalse ne soit pas évalué), puis le résultat conduit au type Type. Les règles de conversion ne sont pas entièrement triviales si l'expression implique des types primitifs et des wrappers dessus (Integer, Double, etc.). Toutes les règles sont décrites en détail dans JLS 15.25. Regardons quelques exemples. Number n = flag ? new Integer(1) : new Double(2.0); Qu'arrivera-t-il à n si l'indicateur est défini ? Un objet Double avec une valeur de 1,0. Le compilateur trouve nos tentatives maladroites pour créer un objet amusantes. Étant donné que les deuxième et troisième arguments enveloppent différents types primitifs, le compilateur les déballe et génère un type plus précis (dans ce cas, double). Et après avoir exécuté l'opérateur ternaire pour l'affectation, la boxe est à nouveau effectuée. En substance, le code est équivalent à ceci : Number n; if( flag ) n = Double.valueOf((double) ( new Integer(1).intValue() )); else n = Double.valueOf(new Double(2.0).doubleValue()); du point de vue du compilateur, le code ne pose aucun problème et se compile parfaitement. Mais FindBugs donne un avertissement :
BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR : la valeur primitive est déballée et forcée pour l'opérateur ternaire dans TestTernary.main(String[]) Une valeur primitive encapsulée est déballée et convertie en un autre type primitif dans le cadre de l'évaluation d'un opérateur ternaire conditionnel (l'opérateur b? e1 : e2 ). La sémantique de Java exige que si e1 et e2 sont des valeurs numériques encapsulées, les valeurs sont déballées et converties/contraintes à leur type commun (par exemple, si e1 est de type Integer et e2 est de type Float, alors e1 est déballé, converti en valeur à virgule flottante et encadré. Voir JLS Section 15.25. Bien sûr, FindBugs prévient également qu'Integer.valueOf(1) est plus efficace que new Integer(1), mais tout le monde le sait déjà.
Ou cet exemple : Integer n = flag ? 1 : null; l'auteur veut mettre null dans n si l'indicateur n'est pas défini. Pensez-vous que cela va fonctionner? Oui. Mais compliquons les choses : Integer n = flag1 ? 1 : flag2 ? 2 : null; il semblerait qu'il n'y ait pas beaucoup de différence. Cependant, maintenant, si les deux indicateurs sont clairs, cette ligne renvoie une NullPointerException. Les options pour l'opérateur ternaire droit sont int et null, donc le type de résultat est Integer. Les options pour celle de gauche sont int et Integer, donc selon les règles Java, le résultat est int. Pour ce faire, vous devez effectuer un déballage en appelant intValue, qui lève une exception. Le code est équivalent à ceci : Integer n; if( flag1 ) n = Integer.valueOf(1); else { if( flag2 ) n = Integer.valueOf(Integer.valueOf(2).intValue()); else n = Integer.valueOf(((Integer)null).intValue()); } Ici FindBugs produit deux messages, qui suffisent à suspecter une erreur :
BX_UNBOXING_IMMEDIATELY_REBOXED : la valeur encadrée est déballée puis immédiatement reboxée dans TestTernary.main(String[]) NP_NULL_ON_SOME_PATH : déréférencement possible du pointeur nul de null dans TestTernary.main(String[]) Il existe une branche d'instruction qui, si elle est exécutée, garantit qu'un La valeur null sera déréférencée, ce qui générerait une NullPointerException lors de l'exécution du code.
Bon, un dernier exemple sur ce sujet : double[] vals = new double[] {1.0, 2.0, 3.0}; double getVal(int idx) { return (idx < 0 || idx >= vals.length) ? null : vals[idx]; } Il n'est pas surprenant que ce code ne fonctionne pas : comment une fonction renvoyant un type primitif peut-elle renvoyer null ? Étonnamment, il compile sans problème. Eh bien, vous comprenez déjà pourquoi il compile.

Format de date

Pour formater les dates et les heures en Java, il est recommandé d'utiliser des classes qui implémentent l'interface DateFormat. Par exemple, cela ressemble à ceci : public String getDate() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } Souvent, une classe utilise le même format encore et encore. Beaucoup de gens auront l'idée de l'optimisation : pourquoi créer un objet de format à chaque fois quand on peut utiliser une instance commune ? private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public String getDate() { return format.format(new Date()); } C’est tellement beau et cool, mais malheureusement ça ne marche pas. Plus précisément, cela fonctionne, mais tombe parfois en panne. Le fait est que la documentation de DateFormat dit :
Les formats de date ne sont pas synchronisés. Il est recommandé de créer des instances de format distinctes pour chaque thread. Si plusieurs threads accèdent simultanément à un format, celui-ci doit être synchronisé en externe.
Et cela est vrai si vous regardez l’implémentation interne de SimpleDateFormat. Lors de l'exécution de la méthode format(), l'objet écrit dans les champs de la classe, donc l'utilisation simultanée de SimpleDateFormat à partir de deux threads conduira à un résultat incorrect avec une certaine probabilité. Voici ce que FindBugs écrit à ce sujet :
STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE : Appel à la méthode statique java.text.DateFormat dans TestDate.getDate() Comme l'indique JavaDoc, les DateFormats sont intrinsèquement dangereux pour une utilisation multithread. Le détecteur a trouvé un appel à une instance de DateFormat qui a été obtenue via un champ statique. Cela semble suspect. Pour plus d'informations à ce sujet, consultez Sun Bug #6231579 et Sun Bug #6178997.

Les pièges de BigDecimal

Ayant appris que la classe BigDecimal permet de stocker des nombres fractionnaires de précision arbitraire, et voyant qu'elle possède un constructeur pour double, certains décideront que tout est clair et vous pouvez procéder comme ceci : System.out.println(new BigDecimal( 1.1)); Personne ne l'interdit vraiment, mais le résultat peut sembler inattendu : 1.100000000000000088817841970012523233890533447265625. Cela se produit parce que le double primitif est stocké au format IEEE754, dans lequel il est impossible de représenter 1,1 de manière parfaitement précise (dans le système de nombres binaires, une fraction périodique infinie est obtenue). Par conséquent, la valeur la plus proche de 1,1 y est stockée. Au contraire, le constructeur BigDecimal(double) fonctionne exactement : il convertit parfaitement un nombre donné dans IEEE754 sous forme décimale (une fraction binaire finale est toujours représentable comme une décimale finale). Si vous souhaitez représenter exactement 1,1 sous forme de BigDecimal, vous pouvez écrire soit new BigDecimal("1.1"), soit BigDecimal.valueOf(1.1). Si vous n'affichez pas le numéro tout de suite, mais effectuez certaines opérations avec, vous ne comprendrez peut-être pas d'où vient l'erreur. FindBugs émet un avertissement DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE, qui donne le même conseil. Voici autre chose : BigDecimal d1 = new BigDecimal("1.1"); BigDecimal d2 = new BigDecimal("1.10"); System.out.println(d1.equals(d2)); en fait, d1 et d2 représentent le même nombre, mais égal renvoie false car il compare non seulement la valeur des nombres, mais aussi l'ordre actuel (le nombre de décimales). Ceci est écrit dans la documentation, mais peu de personnes liront la documentation d'une méthode aussi familière. Un tel problème ne surviendra peut-être pas immédiatement. FindBugs lui-même, malheureusement, ne prévient pas à ce sujet, mais il existe une extension populaire pour cela - fb-contrib, qui prend en compte ce bug :
MDM_BIGDECIMAL_EQUALS equals() étant appelé pour comparer deux nombres java.math.BigDecimal. C'est normalement une erreur, car deux objets BigDecimal ne sont égaux que s'ils sont égaux en valeur et en échelle, de sorte que 2,0 n'est pas égal à 2,00. Pour comparer les objets BigDecimal pour l’égalité mathématique, utilisez plutôt compareTo().

Sauts de ligne et printf

Souvent, les programmeurs qui passent à Java après C sont heureux de découvrir PrintStream.printf (ainsi que PrintWriter.printf , etc.). Super, je sais que, tout comme en C, vous n’avez pas besoin d’apprendre quoi que ce soit de nouveau. Il existe effectivement des différences. L'un d'eux réside dans les traductions en ligne. Le langage C est divisé en flux texte et binaire. La sortie du caractère '\n' dans un flux de texte par quelque moyen que ce soit sera automatiquement convertie en une nouvelle ligne dépendante du système ("\r\n" sous Windows). Il n’existe pas une telle séparation en Java : la séquence correcte de caractères doit être transmise au flux de sortie. Cela se fait automatiquement, par exemple, par les méthodes de la famille PrintStream.println. Mais lors de l'utilisation de printf, passer '\n' dans la chaîne de format est simplement '\n', et non une nouvelle ligne dépendante du système. Par exemple, écrivons le code suivant : System.out.printf("%s\n", "str#1"); System.out.println("str#2"); Après avoir redirigé le résultat vers un fichier, nous verrons : FindBugs vous aide à mieux apprendre Java - 1 Ainsi, vous pouvez obtenir une étrange combinaison de sauts de ligne dans un thread, ce qui semble bâclé et peut épater certains analyseurs. L'erreur peut passer longtemps inaperçue, surtout si vous travaillez principalement sur des systèmes Unix. Pour insérer une nouvelle ligne valide à l'aide de printf, un caractère de formatage spécial "%n" est utilisé. Voici ce que FindBugs écrit à ce sujet :
VA_FORMAT_STRING_USES_NEWLINE : la chaîne de format doit utiliser %n plutôt que \n dans TestNewline.main(String[]) Cette chaîne de format inclut un caractère de nouvelle ligne (\n). Dans les chaînes de format, il est généralement préférable d'utiliser %n, qui produira le séparateur de ligne spécifique à la plate-forme.
Peut-être que pour certains lecteurs, tout ce qui précède était connu depuis longtemps. Mais je suis presque sûr que pour eux il y aura un avertissement intéressant de l'analyseur statique, qui leur révélera de nouvelles fonctionnalités du langage de programmation utilisé.
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION