Au cours de ces années, la fosse de Java sur laquelle nous avons marché

Auteur | Changyi

Source | Intergiciel Alibaba (ID : Aliware_2018)

Couverture | Oriental IC

Il y a un vieil adage en Chine qui s'appelle "Il n'y a pas plus de trois choses", ce qui signifie que si une personne fait la même erreur une, deux ou trois fois, elle peut être pardonnée, mais plus de trois fois est impardonnable. Certaines personnes ont souligné que ce "trois" est un nombre imaginaire, qui est utilisé pour se référer à plusieurs reprises, donc "les choses ne sont rien d'autre que trois" n'inclut pas "trois". Quant au paquet "trois choses" n'inclut pas "trois", il peut avoir quelque chose à voir avec le résultat net de chacun, qui appartient à la catégorie de la philosophie et n'entre pas dans le cadre de cet article.

Il en va de même pour l'écriture de code, le même code "pit", marcher dessus pour la première fois s'appelle "expérimenté", marcher dessus la deuxième fois s'appelle "impression", marcher dessus la troisième fois s'appelle "ne pas grandir votre cur", marcher dessus plus de trois fois est appelé "" sans espoir ". Dans cet article, l'auteur résume certaines fosses de code, décrit le phénomène problématique, analyse le problème et donne des méthodes pour éviter les fosses. J'espère que tout le monde rencontrera de telles fosses de code dans le codage quotidien et pourra les éviter à l'avance.

méthode de comparaison d'objets

La méthode Objects.equals fournie par JDK1.7 est très pratique pour implémenter la comparaison d'objets, évitant efficacement la fastidieuse vérification du pointeur nul.

1.1. Phénomène problématique

Avant JDK1.7, pour juger si un type de données de package entier court, entier ou entier long est égal à une constante, nous écrivions généralement :

Valeur courte courte = (courte)12345 ; System.out.println(shortValue == 12345); // vrai System.out.println(12345 == shortValue); // vrai Entier intValue = 12345 ; System.out.println(intValue == 12345); // vrai System.out.println(12345 == intValue); // vrai Valeur longue longue = 12345L ; System.out.println(longValue == 12345); // vrai System.out.println(12345 == valeurlongue); // vrai

Après JDK1.7, la méthode Objects.equals est fournie et la programmation fonctionnelle est recommandée. Les changements de code sont les suivants :

Valeur courte courte = (courte)12345 ; System.out.println(Objects.equals(shortValue, 12345)); // false System.out.println(Objects.equals(12345, shortValue)); // faux Entier intValue = 12345 ; System.out.println(Objects.equals(intValue, 12345)); // vrai System.out.println(Objects.equals(12345, intValue)); // vrai Valeur longue longue = 12345L ; System.out.println(Objects.equals(longValue, 12345)); // false System.out.println(Objects.equals(12345, longValue)); // faux

Pourquoi le remplacement de == directement par la méthode Objects.equals donne-t-il des résultats de sortie différents ?

1.2. Analyse du problème

En décompilant le premier morceau de code, on obtient l'instruction bytecode de l'instruction "System.out.println(shortValue == 12345);" comme suit :

7 getstatic java.lang.System.out : java.io.PrintStream 10 aload_111 invokevirtual java.lang.Short.shortValue : short 14 sip 1234517 if_icmpne 2420 icônest_121 aller à 2524 icônest_025 invokevirtual java.io.PrintStream.println(boolean) : void

Il s'avère que le compilateur jugera le type de données de base correspondant au type de données d'emballage et utilisera les instructions de ce type de données de base pour la comparaison (telles que sipush et if_icmpne dans les instructions de bytecode ci-dessus), ce qui équivaut au compilateur automatiquement données pour les constantes Coercition de type.

Pourquoi le compilateur ne convertit-il pas automatiquement le type de données des constantes après avoir utilisé la méthode Objects.equals ? En décompilant le deuxième morceau de code, nous obtenons les instructions de bytecode pour l'instruction "System.out.println(Objects.equals(shortValue, 12345));" comme suit :

7 getstatic java.lang.System.out : java.io.PrintStream 10 aload_111 sip 1234514 invokestatic java.lang.Integer.valueOf(int) : java.lang.Integer 17 invokestatic java.util.Objects.equals(java.lang.Object, java.lang.Object) : booléen 20 invokevirtual java.io.PrintStream.println(boolean) : void

Il s'avère que le compilateur pense que le type de données de base par défaut de la constante 12345 est int selon le sens littéral, il sera donc automatiquement converti en type de données wrapper Integer .

Dans le langage Java, le type de données par défaut pour les entiers est int et le type de données par défaut pour les décimales est double .

Analysons l'implémentation du code de la méthode Objects.equals :

public statique booléen égal(Objet a, Objet b) { return (a == b) || (a != a.equals(b)); }

Parmi eux, l'instruction "a.equals(b)" utilisera la méthode Short.equals.

L'implémentation de code de la méthode Short.equals est :

public booléen égal(Objet obj) { if (obj instanceof Short) { valeur de retour == ((Short)obj).shortValue ; } retourner faux ; }

Analyse via le code : l'instruction correspondante "System.out.println(Objects.equals(shortValue, 12345));", car les types d'objets des deux paramètres de Objects.equals sont incohérents, l'un est le type de données d'emballage Short et l'autre est le type de données d'emballage Integer, donc le résultat final de la comparaison doit être faux. De même, l'instruction "System.out.println(Objects.equals(intValue, 12345));", parce que les types d'objets des deux paramètres de Objects.equals sont identiques, ils sont tous deux de type de données encapsulé Integer et ont le même valeur, donc le résultat final de la comparaison doit être vrai.

1.3. Méthode d'évitement des fosses

1. Maintenez de bonnes habitudes de codage et évitez la conversion automatique des types de données.

Afin d'éviter la conversion automatique des types de données, une manière plus scientifique d'écrire consiste à déclarer directement des constantes comme types de données de base correspondants.

Le premier morceau de code peut être écrit comme ceci :

Valeur courte courte = (courte)12345 ; System.out.println(shortValue == (short)12345); // vrai System.out.println((short)12345 == shortValue); // vrai Entier intValue = 12345 ; System.out.println(intValue == 12345); // vrai System.out.println(12345 == intValue); // vrai Valeur longue longue = 12345L ; System.out.println(longValue == 12345L); // vrai System.out.println(12345L == valeurlongue); // vrai

Le deuxième morceau de code peut être écrit comme ceci :

Valeur courte courte = (courte)12345 ; System.out.println(Objects.equals(shortValue, (short)12345)); // vrai System.out.println(Objects.equals((short)12345, shortValue)); // vrai Entier intValue = 12345 ; System.out.println(Objects.equals(intValue, 12345)); // vrai System.out.println(Objects.equals(12345, intValue)); // vrai Valeur longue longue = 12345L ; System.out.println(Objects.equals(longValue, 12345L)); // vrai System.out.println(Objects.equals(12345L, longValue)); // vrai

2. Avec l'aide d'outils de développement ou de plug-ins, les incompatibilités de types de données peuvent être détectées plus tôt.

Dans la fenêtre de problème d'Eclipse, nous verrons une invite comme celle-ci :

Type d'argument improbable pour equals : int semble être sans rapport avec Short Type d'argument improbable pour égal : court semble être sans rapport avec int Type d'argument improbable pour equals : int semble être sans rapport avec Long Type d'argument improbable pour les égaux : Long semble être sans rapport avec int

En parcourant le plugin FindBugs, nous voyons cet avertissement :

Appel à Short.equals(Integer) dans xxx.Xxx.main(String) Appel à Integer.equals(Short) dans xxx.Xxx.main(String) Appel à Long.equals(Integer) dans xxx.Xxx.main(String) Appel à Integer.equals(Long) dans xxx.Xxx.main(String)

3. Effectuer des tests unitaires de routine, dans la mesure du possible Détecter quantitativement les problèmes au stade de la R&D.

"Ne faites pas quelque chose de petit et ne le faites pas", n'avez pas besoin de tests unitaires car les changements sont petits, souvent des bogues apparaissent dans votre code trop confiant. Comme ce genre de problème, tant qu'un test unitaire est effectué, le problème peut être trouvé complètement.

Déballage de l'expression ternaire

Les expressions ternaires sont une syntaxe fixe dans le codage Java : "expression conditionnelle ? expression1:expression2". La logique d'une expression ternaire est : "Si l'expression conditionnelle est vraie, alors exécute l'expression 1 , sinon exécute l'expression 2".

2.1. Phénomène problématique

condition booléenne = faux ; Double valeur1 = 1.0D ; Double valeur2 = 2.0D ; Double valeur3 = ; Double résultat = condition ? valeur1 * valeur2 : valeur3 ; // lève une exception de pointeur nul

Lorsque la condition d'expression conditionnelle est égale à false, en attribuant directement la valeur3 de l'objet Double au résultat de l'objet Double, il n'y a pas de problème de raison, pourquoi une exception de pointeur nul (PointerException) est-elle levée ?

2.2. Analyse du problème

En décompilant le code, on obtient l'instruction bytecode de l'instruction " Double result = condition ? value1 * value2 : value3; " comme suit :

17 iload_118 ifeq 3321 aload_222 invokevirtual java.lang.Double.doubleValue : double 25 aload_326 invokevirtual java.lang.Double.doubleValue : double 29 dmul 30 aller à 3833 charge 435 invokevirtual java.lang.Double.doubleValue : double 38 invokestatic java.lang.Double.valueOf(double) : java.lang.Double 41 un magasin 543 getstatic java.lang.System.out : java.io.PrintStream 46 en charge 5

A la ligne 33, l'objet Double value3 est chargé dans la pile des opérandes ; à la ligne 35, la méthode doubleValue de l'objet Double value3 est appelée. À ce stade, étant donné que value3 est un objet null, l'appel de la méthode doubleValue lèvera inévitablement une exception de pointeur null. Mais pourquoi convertir l'objet vide value3 en type de données sous-jacent double ?

Vérifiez les informations pertinentes pour obtenir les règles de conversion de type pour les expressions ternaires :

  • Si les deux expressions ont le même type, le type de valeur de retour est ce type ;

  • Si les deux expressions sont de types différents, mais que les types ne sont pas convertibles, le type de valeur de retour est Object ;

  • Si les types des deux expressions sont différents, mais que les types peuvent être convertis, convertissez d'abord le type de données wrapper en type de données de base, puis suivez les règles de conversion du type de données de base (byte < court (char) < entier < long < flotteur < double) à convertir, le type de valeur de retour est le type de données de base avec la priorité la plus élevée.

  • Selon l'analyse des règles, l'expression 1 (valeur1 * valeur2) renvoie le type de données de base double après calcul, et l'expression 2 (valeur3) renvoie le type de données de package Double. Selon les règles de conversion de type des expressions ternaires, le type de retour final est le type de données de base double. Par conséquent, lorsque la condition d'expression conditionnelle est égale à false, l'objet vide value3 doit être converti dans le type de données de base double, de sorte que la méthode doubleValue de value3 est appelée et une exception de pointeur null est levée.

    Les règles de conversion de type pour les expressions ternaires peuvent être vérifiées avec les cas suivants :

    condition booléenne = faux ; Double valeur1 = 1.0D ; Double valeur2 = 2.0D ; Double valeur3 = ; Valeur entière4 = ; // Le type de retour est Double, aucune exception de pointeur nul n'est levée Double résultat1 = condition ? valeur1 : valeur3 ; // Le type de retour est double, une exception de pointeur nul sera levée Double résultat2 = condition ? valeur1 : valeur4 ; // Le type de retour est double, aucune exception de pointeur nul n'est levée Double résultat3 = !condition ?valeur1 * valeur2 : valeur3; // Le type de retour est double, une exception de pointeur nul sera levée Double résultat4 = condition ? valeur1 * valeur2 : valeur3 ;

    2.3. Méthode d'évitement des fosses

    1. Essayez d'éviter d'utiliser des expressions ternaires, vous pouvez utiliser des instructions if-else à la place.

    S'il existe des calculs arithmétiques et des types de données encapsulés dans des expressions ternaires, envisagez d'utiliser des instructions if-else à la place. Réécrivez le code comme suit :

    condition booléenne = faux ; Double valeur1 = 1.0D ; Double valeur2 = 2.0D ; Double valeur3 = ; Double résultat ; si (état) { résultat = valeur1 * valeur2 ; } autre { résultat = valeur3 ; }

    2. Essayez d'utiliser des types de données de base pour éviter la conversion automatique des types de données.

    S'il existe des calculs arithmétiques et des types de données encapsulés dans des expressions ternaires, envisagez d'utiliser des instructions if-else à la place. Réécrivez le code comme suit :

    condition booléenne = faux ;

    double valeur1 = 1.0D ;

    valeur double2 = 2.0D ;

    double valeur3 = 3.0D ;

    résultat double = condition ? valeur1 * valeur2 : valeur3 ;

    3. Effectuez des tests unitaires de couverture et essayez de trouver des problèmes au stade du développement.

    De tels problèmes peuvent être découverts à l'avance tant que vous écrivez des cas de test unitaire et effectuez des tests de couverture.

    Affectation d'objet générique

    Java Generics est une nouvelle fonctionnalité introduite dans JDK1.5, et son essence est le type paramétré, c'est-à-dire que le type de données est utilisé comme paramètre.

    3.1. Phénomène problématique

    Lors d'une requête de pagination des données utilisateur, le code suivant a été écrit à cause d'une faute de frappe :

    1. PageDataVO.java :

    /** Classe VO des données de radiomessagerie */ @Getter @Setter @ToString @NoArgsConstructor @AllArgsConstructor classe publique PageDataVO < J > { /** nombre total */ totalCount long privé ; /** Feuilles de données */ Liste privée < J > liste de données ; }

    2. UserDAO.java :

    /** Interface utilisateur DAO */ @Mappeur interface publique UserDAO { /** Compter le nombre d'utilisateurs */ public Long countUser(@Param("query") UserQueryVO query); /** Interroger les informations de l'utilisateur */ Liste publique < UtilisateurDO > queryUser(@Param("query") UserQueryVO query); }

    3. UserService.java :

    /** Classe de service utilisateur */ @Service classe publique UserService { /** DAO utilisateur */ @Autowired userDAO privé userDAO ; /** Interroger les informations de l'utilisateur */ public PageDataVO < UtilisateurVO > queryUser (requête UserQueryVO) { Liste < UtilisateurDO > listeDonnées = ; Long totalCount = userDAO.countUser(requête); si (Objects.non(totalCount) totalCount.compareTo(0L) > 0) { dataList = userDAO.queryUser(requête); } renvoie un nouveau PageDataVO(totalCount, dataList); } }

    4. UserController.java :

    /** Classe de contrôleur utilisateur */ @Manette @RequestMapping("/utilisateur") public class UserController { /** Service utilisateur */ @Autowired UserService privé userService ; /** Interroger l'utilisateur */ @ResponseBody @RequestMapping(value = "/query", méthode = RequestMethod.POST) Résultat public < PageDataVO < UtilisateurVO > > queryUser(@RequestBody UserQueryVO requête) { PageDataVO < UtilisateurVO > pageData = userService.queryUser(requête); return ResultBuilder.success(pageData); } }

    Le code ci-dessus ne présente aucun problème de compilation, mais il renvoie certains champs secrets dans UserDO au frontal. Les lecteurs attentifs ont peut-être trouvé que dans l'instruction "return new PageDataVO(totalCount, dataList);" de la méthode queryUser de la classe UserService, nous mettons la List < UtilisateurDO > L'objet dataList est affecté à PageDataVO < UtilisateurVO > Liste de < UtilisateurVO > Liste de données de champ.

    La question est : pourquoi les outils de développement ne signalent-ils pas les erreurs de compilation ?

    3.2. Analyse du problème

    Pour des raisons historiques, les types paramétrés et les types primitifs doivent être compatibles. Prenons ArrayList comme exemple pour voir comment il est compatible.

    Précédemment écrit :

    liste ArrayList = nouvelle ArrayList ;

    La manière actuelle d'écrire :

    Liste des tableaux < Chaîne de caractères > liste = nouvelle ArrayList < Chaîne de caractères > ;

    Compte tenu de la compatibilité avec le code précédent, les situations suivantes se produiront inévitablement lors du passage de valeurs entre différentes références d'objet :

    // premier cas ArrayList list1 = nouvelle ArrayList < Chaîne de caractères > ; // deuxième cas Liste des tableaux < Chaîne de caractères > liste2 = nouvelle ArrayList ;

    Par conséquent, le compilateur Java est compatible avec les deux types ci-dessus, et il n'y aura pas d'erreurs de compilation, mais des avertissements de compilation apparaîtront. Cependant, mes outils de développement ne donnent vraiment aucun avertissement lors de la compilation.

    Analysons les problèmes que nous avons rencontrés, en fait, deux situations se sont heurtées en même temps :

    1. Mettez la liste < UtilisateurDO > L'objet est assigné à List , qui atteint le premier cas ;

    2. Affectez l'objet PageDataVO à PageDataVO < UtilisateurVO > , frappe le deuxième cas.

    L'effet final est : nous mettons magiquement la liste < UtilisateurDO > L'objet est affecté à la liste < UtilisateurVO > .

    La racine du problème est que nous n'avons pas exigé de vérification de type obligatoire lorsque nous avons initialisé l'objet PageDataVO.

    3.3. Méthode d'évitement des fosses

    1. Lors de l'initialisation d'un objet générique, il est recommandé d'utiliser la syntaxe diamant.

    Dans le "Alibaba Java Development Manual", il existe une telle règle recommandée :

    [Recommandé] Lors de la définition des génériques d'ensemble, dans JDK7 et supérieur, utilisez la syntaxe diamant ou omettez-la entièrement. Description : Le générique du losange, à savoir le diamant, est utilisé directement < > pour faire référence au type spécifié précédemment. Exemple positif :

    // < > chemin du diamant HashMap < Chaîne, Chaîne > userCache = nouveau HashMap < > (16); // omission totale Liste des tableaux < Utilisateur > utilisateurs = nouvelle ArrayList(10);

    En fait, il est déconseillé de tout omettre lors de l'initialisation d'un objet générique. Cela évite la vérification de type, qui provoque le problème ci-dessus.

    Lors de l'initialisation d'un objet générique, il est recommandé d'utiliser la syntaxe en losange dont le code est le suivant :

    renvoyer une nouvelle PageDataVO < > (totalCount, dataList);

    Maintenant, dans la fenêtre de problème d'Eclipse, nous verrons une erreur comme celle-ci :

    Impossible de déduire les arguments de type pour PageDataVO < >

    Donc, nous savons que nous avons oublié de mettre la liste < UtilisateurDO > Objet converti en liste < UtilisateurVO > objet.

    2. Lors de l'exécution de tests unitaires, il est nécessaire de comparer le contenu des données.

    En ce qui concerne les tests unitaires, la santé est un indicateur, mais des données correctes sont plus importantes.

    Copie de propriété générique

    La méthode BeanUtils.copyProperties de Spring est une méthode d'outil de copie de propriété très utile.

    4.1. Phénomène problématique

    Selon la spécification de développement de la base de données, la table de la base de données doit contenir trois champs : id, gmt_create et gmt_modified. Parmi eux, le champ id peut être de type int ou long selon la quantité de données.

    Alors, extrayez ces trois champs et définissez une classe de base BaseDO :

    /** Classe DO de base */

    @Getter

    @Setter

    @ToString

    classe publique BaseDO < J > {

    identifiant T privé ;

    Date privée gmtCreate ;

    date privée gmtModified ;

    }

    Pour la table des utilisateurs, une classe UserDO est définie :

    /** Classe utilisateur DO */

    @Getter

    @Setter

    @ToString

    la classe publique UserDO étend BaseDO < Long > {

    nom de chaîne privé ;

    description de la chaîne privée ;

    }

    Pour l'interface de requête, une classe UserVO est définie :

    /** Classe VO utilisateur */ @Getter @Setter @ToString classe statique publique UserVO { ID long privé ; nom de chaîne privé ; description de la chaîne privée ; }

    Pour implémenter l'interface de service utilisateur de requête, le code d'implémentation est le suivant :

    /** Classe de service utilisateur */ @Service classe publique UserService { /** DAO utilisateur */ @Autowired userDAO privé userDAO ; /** Interroger l'utilisateur */ Liste publique < UtilisateurVO > queryUser (requête UserQueryVO) { // Interroger les informations de l'utilisateur Liste < UtilisateurDO > userDOList = userDAO.queryUser(requête); si (CollectionUtils.isEmpty) { return Collections.emptyList ; } // liste des utilisateurs convertis Liste < UtilisateurVO > userVOList = nouvelle ArrayList < > (userDOList.size); for (UserDO userDO : userDOList) { UserVO userVO = nouveau UserVO ; BeanUtils.copyProperties(userDO, userVO); userVOList.add(userVO); } // renvoie la liste des utilisateurs renvoie userVOList ; } }

    Grâce aux tests, nous trouverons un problème - en appelant l'interface de service utilisateur de requête, la valeur de l'ID utilisateur n'est pas renvoyée.

    4.2. Analyse du problème

    Logiquement, les champs id de la classe UserDO et de la classe UserVO sont tous les deux de type Long, et il n'y a aucun type qui ne peut pas être converti, ils devraient donc pouvoir attribuer des valeurs normalement. Essayez l'affectation manuelle, le code est le suivant :

    for (UserDO userDO : userDOList) { UserVO userVO = nouveau UserVO ; userVO.setId(userDO.getId); userVO.setName(userDO.getName); userVO.setDescription(userDO.getDescription); userVOList.add(userVO); }

    Après le test, le code ci-dessus renvoie des résultats normaux et la valeur de l'ID utilisateur est renvoyée avec succès.

    Il s'agit donc de la méthode utilitaire BeanUtils.copyProperties. Exécutez en mode débogage, entrez la méthode de l'outil BeanUtils.copyProperties et obtenez les données suivantes :

    Il s'avère que le type de retour de la méthode getId de la classe UserDO n'est pas un type Long, mais est restauré en type Object par des génériques. La méthode suivante de l'outil ClassUtils.isAssignable détermine si le type Object peut être affecté au type Long. Bien entendu, elle renverra false et la propriété ne pourra pas être copiée.

    Pourquoi l'auteur n'a-t-il pas envisagé "d'obtenir d'abord la valeur de l'attribut, puis de juger si une valeur peut lui être attribuée" ? Le code suggéré est le suivant :

    Valeur de l'objet = readMethod.invoke(source);

    if (Objects.non(value) ClassUtils.isAssignable(writeMethod.getParameterTypes, value.getClass)) {

    ... // code lié à l'affectation

    }

    4.3. Méthode d'évitement des fosses

    1. Ne faites pas aveuglément confiance aux boîtes à outils tierces, toute boîte à outils peut avoir des problèmes.

    En Java, il existe de nombreux toolkits tiers, tels que : commons-lang3 d'Apache, commons-collections, guava de Google... Ce sont tous des toolkits tiers très utiles. Cependant, ne faites pas aveuglément confiance aux boîtes à outils tierces, toute boîte à outils peut avoir des problèmes.

    2. S'il y a peu d'attributs à copier, vous pouvez encoder manuellement les attributs à copier.

    Le principal avantage de l'utilisation de BeanUtils.copyProperties pour refléter les propriétés de copie est d'économiser la quantité de code, mais le principal inconvénient est que les performances du programme sont dégradées. Par conséquent, s'il y a peu d'attributs à copier, vous pouvez coder manuellement les attributs à copier.

    3. Des tests unitaires doivent être effectués et le contenu des données doit être comparé.

    Après avoir écrit le code, assurez-vous d'effectuer des tests unitaires et de comparer le contenu des données. Ne le prenez pas pour acquis : la boîte à outils est mature, le code est simple et il n'y a aucun risque de problèmes.

    Définir la réorganisation des objets

    Dans le langage Java, la structure de données Set peut être utilisée pour réorganiser les objets. Les classes Set communes incluent HashSet , LinkedHashSet, etc.

    5.1. Phénomène problématique

    A écrit une classe d'assistance de ville pour lire les données de la ville à partir d'un fichier CSV :

    /** Auxiliaire urbain */ @Slf4j classe publique CityHelper { /** Tester la méthode principale */ public static void main(String args) { Le recueil < Ville > cityCollection = readCities2("cities.csv"); log.info(JSON.toJSONString(cityCollection)); } /** lire la ville */ Collection statique publique < Ville > readCities(String fileName) { essayez (flux FileInputStream = new FileInputStream(fileName); lecteur InputStreamReader = new InputStreamReader(stream, "GBK"); Analyseur CSVParser = nouveau CSVParser (lecteur, CSVFormat.DEFAULT.withHeader)) { Régler < Ville > citySet = nouveau HashSet < > (1024); Itérateur < Enregistrement CSVR > itérateur = analyseur.itérateur ; tandis que (iterator.hasNext) { citySet.add(parseCity(iterator.next)); } renvoie citySet ; } capture (IOException e) { log.warn("Lire toutes les exceptions de la ville", e); } return Collections.emptyList ; } /** Analyser la ville */ ville statique privée parseCity (enregistrement CSVRecord) { Ville ville = nouvelle ville ; ville.setCode(record.get(0)); ville.setName(record.get(1)); ville de retour ; } /** classe urbaine */ @Getter @Setter @ToString classe statique privée Ville { /** indicatif de la ville */ code de chaîne privé ; /** Nom de Ville */ nom de chaîne privé ; } }

    La structure de données HashSet est utilisée dans le code, afin d'éviter la duplication des données de ville et de réorganiser de force les données de ville lues.

    Lorsque le contenu du fichier d'entrée est le suivant :

    nom de code 010, Pékin 020, Canton 010, Pékin

    Le résultat JSON analysé est le suivant :

    Cependant, la ville "Pékin" n'a pas été reclassée.

    5.2. Analyse du problème

    Lors de l'ajout d'un objet à une collection Set, la collection calcule d'abord le hashCode de l'objet à ajouter et obtient un emplacement pour stocker l'objet actuel en fonction de cette valeur. S'il n'y a pas d'objet à cette position, alors la collection Set pense que l'objet n'existe pas dans la collection et l'ajoute directement. S'il y a un objet à cette position, alors comparez l'objet à ajouter à la collection avec l'objet à cette position par la méthode equals : si la méthode equals renvoie false, alors la collection pense que l'objet n'existe pas dans la collection , et place l'objet L'objet est placé après cet objet ; si la méthode equals renvoie true, l'objet est considéré comme existant déjà dans la collection et l'objet ne sera pas ajouté à la collection. Par conséquent, la méthode hashCode et la méthode equals sont utilisées pour déterminer si deux éléments sont répétés dans la table de hachage. La méthode hashCode détermine où les données sont stockées dans la table, et la méthode equals détermine si les mêmes données existent dans la table.

    En analysant le problème ci-dessus, étant donné que la méthode hashCode et la méthode equals de la classe City ne sont pas remplacées, la méthode hashCode et la méthode equals de la classe Object seront utilisées. Il est implémenté comme suit :

    public natif int hashCode ; public booléen égal(Objet obj) { retour (this == obj); }

    On peut voir que la méthode hashCode de la classe Object est une méthode locale et renvoie l'adresse de l'objet ; la méthode equals de la classe Object compare uniquement si les objets sont égaux. Par conséquent, pour deux données Beijing identiques, étant donné que différents objets City sont initialisés lors de l'analyse, la méthode hashCode et la méthode equals ont des valeurs différentes, qui doivent être considérées comme des objets différents par Set, il n'y a donc pas de tri.

    Ensuite, on réécrit la méthode hashCode et la méthode equals de la classe City, le code est le suivant :

    /** classe urbaine */ @Getter @Setter @ToString classe statique privée Ville { /** indicatif de la ville */ code de chaîne privé ; /** Nom de Ville */ nom de chaîne privé ; /** Vérifie l'égalité */ @Passer outre public booléen égal(Objet obj) { si (obj == ceci) { retourner vrai ; } si (Objets.est(obj)) { retourner faux ; } si (obj.getClass != this.getClass) { retourner faux ; } return Objects.equals(this.code, ((City)obj).code); } /** Code de hachage */ @Passer outre public int hashCode { return Objects.hashCode(this.code); } }

    Le programme de test est à nouveau pris en charge et les résultats JSON analysés sont les suivants :

    Le résultat est correct, et la ville "Pékin" a été reclassée.

    5.3. Méthode d'évitement des fosses

    1. Lorsqu'il est déterminé que les données sont uniques, List peut être utilisé à la place de Set.

    Lorsqu'il est déterminé que les données de ville analysées sont uniques, il n'est pas nécessaire d'effectuer des opérations de réorganisation et List peut être utilisé pour les stocker directement.

    Liste < Ville > citySet = nouvelle ArrayList < > (1024); Itérateur < Enregistrement CSVR > itérateur = analyseur.itérateur ; tandis que (iterator.hasNext) { citySet.add(parseCity(iterator.next)); } renvoie citySet ;

    2. Lorsqu'il est déterminé que les données ne sont pas uniques, Map peut être utilisé à la place de Set.

    Lorsqu'il est déterminé que les données de ville analysées ne sont pas uniques, le nom de la ville doit être installé pour les opérations de tri et de réorganisation, qui peuvent être stockées directement à l'aide de Map. Pourquoi n'est-il pas recommandé d'implémenter la méthode hashCode de la classe City, puis d'utiliser le HashSet pour implémenter la pondération ? Tout d'abord, ne voulez pas mettre la logique métier dans la classe DO du modèle ; deuxièmement, placez le champ de poids de ligne dans le code, ce qui est pratique pour la lecture, la compréhension et la maintenance du code.

    Carte < Chaîne, Ville > cityMap = nouveau HashMap < > (1024); Itérateur < Enregistrement CSVR > itérateur = analyseur.itérateur ; tandis que (iterator.hasNext) { Ville city = parseCity(iterator.next); cityMap.put(ville.getCode, ville); } renvoie cityMap.values ;

    3. Suivez les spécifications du langage Java et réécrivez la méthode hashCode et la méthode equals.

    Les classes personnalisées qui ne remplacent pas les méthodes hashCode et equals ne doivent pas être utilisées dans Set.

    proxy de méthode publique

    La classe proxy générée par le proxy SpringCGLIB est une classe proxy héritée, qui implémente le proxy en remplaçant les méthodes non finales dans la classe proxy. Par conséquent, la classe proxy par SpringCGLIB ne peut pas être une classe finale, et la méthode proxy ne peut pas être une méthode finale, qui est restreinte par le mécanisme d'héritage.

    6.1. Phénomène problématique

    Voici un exemple simple, seul le super utilisateur a le pouvoir de supprimer l'entreprise, et toutes les fonctions de service sont interceptées par AOP pour gérer les exceptions. L'exemple de code est le suivant :

    1. UserService.java :

    /** Classe de service utilisateur */ @Service classe publique UserService { /** racine */ superutilisateur utilisateur privé ; /** Définir un super utilisateur */ public void setSuperUser(Utilisateur superUtilisateur) { this.superUser = superutilisateur ; } /** Récupère le superutilisateur */ Utilisateur final public getSuperUser { retourne this.superUser ; } }

    2. CompanyService.java :

    /** Classe de service de l'entreprise */ @Service classe publique CompanyService { /** Société DAO */ @Autowired société privéeDAO companyDAO ; /** Service utilisateur */ @Autowired UserService privé userService ; /** supprimer l'entreprise */ public void deleteCompany(Long companyId, Long operatorId) { // définit le super utilisateur userService.setSuperUser(nouvel utilisateur(0L, "admin", "Super utilisateur")); // Authentification du superutilisateur if (!Objects.equals(operatorId, userService.getSuperUser.getId)) { throw new ExampleException("Seuls les super-utilisateurs peuvent supprimer des entreprises"); } // supprimer les informations de l'entreprise companyDAO.delete(companyId, operatorId); } }

    3. AopConfiguration.java :

    /** Classe de configuration AOP */ @Slf4j @Aspect @Configuration classe publique AopConfiguration { /** méthode wrap */ @Around("execution(*org.changyi.springboot.service..*.*(..))") Objet public autour (ProceedingJoinPoint joinPoint) { essayer { log.info("Commencer à appeler la méthode de service..."); return joinPoint.proceed ; } attraper (e jetable) { log.error(e.getMessage, e); jeter new ExampleException(e.getMessage, e); } } }

    Lorsque nous appelons la méthode deleteCompany de CompanyService, une exception de pointeur nul (PointerException) est levée, car le super utilisateur obtenu en appelant la méthode getSuperUser de la classe UserService est . Cependant, dans la méthode deleteCompany de la classe CompanyService, nous forçons un super utilisateur à être spécifié à chaque fois via la méthode setSuperUser de la classe UserService. Logiquement, le super utilisateur obtenu via la méthode getSuperUser de la classe UserService ne devrait pas être . En fait, ce problème est également causé par le proxy AOP.

    6.2. Analyse du problème

    Lors de l'utilisation de la classe proxy SpringCGLIB, Spring crée une classe proxy nommée UserService$$EnhancerBySpringCGLIB$$???????? . La décompilation de cette classe proxy donne le code principal suivant :

    public class UserService$$EnhancerBySpringCGLIB$$a2c3b345 étend UserService implémente SpringProxy, Advised, Factory { ...... public final void setSuperUser(User var1) { MethodInterceptor var10000 = this.CGLIB$CALLBACK_0 ; si (var10000 == ) { CGLIB$BIND_CALLBACKS(ceci); var10000 = this.CGLIB$CALLBACK_0 ; } si (var10000 != ) { var10000.intercept(this, CGLIB$setSuperUser$0$Method, new Object{var1}, CGLIB$setSuperUser$0$Proxy); } autre { super.setSuperUser(var1); } } ...... }

    On peut voir que cette classe proxy hérite de la classe UserService, proxy de la méthode setSuperUser, mais pas proxy de la méthode getSuperUser. Ainsi, lorsque nous appelons la méthode setSuperUser, nous définissons la valeur du champ superUser de l'instance d'objet d'origine ; et lorsque nous appelons la méthode getSuperUser, nous obtenons la valeur du champ superUser de l'instance d'objet proxy. Si les modificateurs finaux de ces deux méthodes sont interchangés, il y a aussi le problème d'obtenir un comportement de superutilisateur.

    6.3. Méthode d'évitement des fosses

    1. Suivez strictement la spécification du proxy CGLIB et n'ajoutez pas de modificateurs finaux aux classes et aux méthodes en cours de proxy.

    Suivez strictement la spécification de proxy CGLIB. N'ajoutez pas de modificateurs finaux aux classes et méthodes proxy, afin d'éviter que les instances d'objet d'exploitation de proxy dynamique (instances d'objet d'origine et instances d'objet proxy) ne soient différentes, ce qui entraînerait une incohérence des données ou des problèmes de pointeur nul .

    2. Réduisez la portée des classes proxy CGLIB et ne soyez pas proxy si les classes qui peuvent être proxy ne sont pas utilisées.

    La réduction de la portée des classes proxy CGLIB peut économiser la surcharge de mémoire et améliorer l'efficacité des appels de fonction.

    proxy de champ public

    J'ai marché sur une fosse lorsque fastjson a été contraint de passer à la version 1.2.60. Afin de développer rapidement, l'auteur a défini dans ParseConfig :

    classe publique ParseConfig {

    public final SymbolTable symbolTable = new SymbolTable(4096);

    ......

    }

    A hérité de cette classe dans notre projet, et en même temps a été mandaté dynamiquement par AOP, donc une ligne de code a provoqué un "cas de sang".

    7.1. Phénomène problématique

    Utilisez toujours l'exemple du chapitre précédent, mais supprimez les méthodes get et set et définissez un champ public. L'exemple de code est le suivant :

    1. UserService.java :

    /** Classe de service utilisateur */ @Service classe publique UserService { /** racine */ public final User superUser = new User(0L, "admin", "superuser"); ...... }

    2. CompanyService.java :

    /** Classe de service de l'entreprise */ @Service classe publique CompanyService { /** Société DAO */ @Autowired société privéeDAO companyDAO ; /** Service utilisateur */ @Autowired UserService privé userService ; /** supprimer l'entreprise */ public void deleteCompany(Long companyId, Long operatorId) { // Authentification du superutilisateur if (!Objects.equals(operatorId, userService.superUser.getId)) { throw new ExampleException("Seuls les super-utilisateurs peuvent supprimer des entreprises"); } // supprimer les informations de l'entreprise companyDAO.delete(companyId, operatorId); } }

    3. AopConfiguration.java :

    Identique au chapitre précédent AopConfiguration.java .

    Lorsque nous appelons la méthode deleteCompany de CompanyService, une exception de pointeur null (PointerException) est levée. Après le débogage et l'impression, on constate que la variable superUser de UserService est . Si AopConfiguration est supprimé, il n'y aura pas d'exception de pointeur null, indiquant que le problème est causé par le proxy AOP.

    7.2. Analyse du problème

    Lors de l'utilisation de la classe proxy SpringCGLIB, Spring crée une classe proxy nommée UserService$$EnhancerBySpringCGLIB$$???????? . Cette classe proxy étend la classe UserService et remplace toutes les méthodes publiques non finales de la classe UserService. Cependant, cette classe proxy n'appelle pas la méthode de la super classe de base ; à la place, elle crée un membre userService qui pointe vers l'instance d'origine de l'objet de la classe UserService. Maintenant, il y a deux instances d'objet en mémoire : l'une est l'instance d'origine de l'objet UserService et l'autre est une instance d'objet proxy qui pointe vers UserService. Cette classe proxy est juste un proxy factice qui étend la classe UserService et a les mêmes champs que UserService , mais il ne les initialise jamais et ne les utilise jamais. Par conséquent, une fois qu'une variable de membre public est obtenue via cette instance d'objet de classe proxy, une valeur par défaut est renvoyée.

    7.3. Méthode d'évitement des fosses

    1. Lorsque le champ est déterminé comme immuable, il peut être défini comme une constante statique publique.

    Lorsque le champ est déterminé comme immuable, il peut être défini comme une constante statique publique et accessible avec le nom de la classe + le nom du champ. Le nom de classe + nom de champ accède aux constantes statiques publiques, quel que soit le proxy dynamique de l'instance de classe.

    /** Classe de service utilisateur */ @Service classe publique UserService { /** racine */ public static final User SUPER_USER = new User(0L, "admin", "Super User"); ...... } /** utiliser le code */ if (!Objects.equals(operatorId, UserService.SUPER_USER.getId)) { throw new ExampleException("Seuls les super-utilisateurs peuvent supprimer des entreprises"); }

    2. Lorsqu'il est déterminé que le champ est immuable, il peut être défini comme une variable de membre privé.

    Lorsqu'il est déterminé que le champ est immuable, il peut être défini comme une variable membre privée et fournir une méthode publique pour obtenir la valeur de la variable. Lorsque l'instance de classe est mandatée dynamiquement, la méthode proxy appelle la méthode proxy, renvoyant ainsi la valeur de la variable membre de la classe proxy.

    /** Classe de service utilisateur */ @Service classe publique UserService { /** racine */ Utilisateur privé superutilisateur = nouvel utilisateur (0L, "admin", "superutilisateur"); /** Récupère le superutilisateur */ Utilisateur public getSuperUser { retourne this.superUser ; } ...... } /** utiliser le code */ if (!Objects.equals(operatorId, userService.getSuperUser.getId)) { throw new ExampleException("Seuls les super-utilisateurs peuvent supprimer des entreprises"); }

    3. Suivez la norme de codage JavaBean et ne définissez pas de variables de membre publiques.

    Suivez les conventions de codage JavaBean et ne définissez pas de variables de membre publiques. La spécification JavaBean est la suivante :

    (1) La classe JavaBean doit être une classe publique et définir son attribut d'accès sur public, par exemple : public class User{...} (2) La classe JavaBean doit avoir un constructeur vide : la classe doit avoir un constructeur public sans paramètres (3) Une classe JavaBean ne doit pas avoir de variables d'instance publiques, toutes les variables de classe sont privées, telles que : identifiant Integer privé ; (4) Les propriétés doivent être accessibles via un ensemble de méthodes getter/setter.

    Les êtres humains bénéficient de la pensée "analogique", et c'est la sagesse humaine de tirer des conclusions d'un cas à l'autre. Chaque fois qu'ils rencontrent de nouvelles choses, les gens utilisent souvent des choses connues similaires comme référence, ce qui peut accélérer la cognition de nouvelles choses. Et les êtres humains sont soumis à une pensée "fixe", parce que les choses connues ne peuvent pas représenter de nouvelles choses, et les gens sont enclins à former des concepts préconçus, qui conduisent finalement à une mauvaise appréciation des nouvelles choses.

    À propos de l'auteur : Chen Changyi, dont le nom de fleur est Changyi, est un expert de la technologie cartographique AutoNavi. Il a rejoint Alibaba en 2018 et travaille sur la collecte de données cartographiques.

    Xunfei Intelligent Voice Pioneer : lorsque l'interaction homme-ordinateur est aussi naturelle que la communication humaine, la véritable ère de l'intelligence viendra !

    Les entreprises ont besoin d'un ensemble de méthodologies de la Silicon Valley pour construire leur propre centre de données (il y a des avantages à la fin de l'article !)

    De Nginx à Pandownload, comment les programmeurs peuvent-ils éviter la programmation axée sur la prison ?

    Pouvez-vous découvrir des algorithmes avec uniquement des opérations mathématiques du secondaire ? Quelle est la puissance de l'open source AutoML-Zero de Google ?

    Architecture microservices sous architecture Spring Cloud : Microservices départementaux (Dept)

    De Spring Cloud à Service Mesh, comment évolue le système de gouvernance de l'architecture des microservices ?

    les gens! Recruter la connaissance de l'entrevue MySQL doit maîtriser les huit points
    Précédent
    L'open source ne peut que se faire des amis?
    Prochain
    Serverless houleuse, pourquoi Ali, Microsoft, AWS ont adopté OAM open source?
    Google aussi « serrer la ceinture » pour vivre une
    5G infrastructures: comment faire des centaines de millions d'utilisateurs pour soutenir de façon transparente IPv6?
    Vraiment parler | « Terreux CP » Ce qui est plus fort? S'il vous plaît retrouver un jour Hu et Zhang Yunlong
    Planting Grass Ji | Maquillage des lèvres riche, veuillez également Kendall et Hyun Ya Innocent
    acteur Beat Street | est costume de couleur crème glacée Zhuxing Jie, donc je commence à regarder en avant l'été
    Arc-en-fart | Liu Shi a été en train de pleurer en direct aux États-Unis? Jetez un oeil à quel point elle portait un costume blanc
    Arc-en-péter | Yang Mi Qui a dit que l'amour sait? Vous voyez, elle a fait une star d'un acteur
    Le pull de Lin Yun, les jambes de baguettes de Yang Mi et les stars européennes et américaines qui ne peuvent pas se permettre de vivre, "Poster Street Shot"
    États-Unis pour vous enseigner | avoir un mot à dire, Liu Yifei enfin voir des photos de progrès
    Pop-corn | sélection des plus belles étoiles de Corée du Sud est sorti, fils Depp un beau petit ah ah ah ah
    Vraiment parlant | regardé « notre groupe », se souvient du moment où le « chanteur de bande » qui