JavaRush /Blog Java /Random-FR /Comparaison d'objets : pratique
articles
Niveau 15

Comparaison d'objets : pratique

Publié dans le groupe Random-FR
Ceci est le deuxième des articles consacrés à la comparaison d'objets. Le premier d'entre eux a discuté des bases théoriques de la comparaison : comment elle est effectuée, pourquoi et où elle est utilisée. Dans cet article nous parlerons directement de la comparaison de nombres, d'objets, de cas particuliers, de subtilités et de points non évidents. Plus précisément, voici de quoi nous parlerons :
Comparaison d'objets : pratique - 1
  • Comparaison de chaînes : ' ==' etequals
  • MéthodeString.intern
  • Comparaison de vraies primitives
  • +0.0Et-0.0
  • SignificationNaN
  • Java 5.0. Génération de méthodes et comparaison via ' =='
  • Java 5.0. Autoboxing/Unboxing : ' ==', ' >=' et ' <=' pour les wrappers d'objets.
  • Java 5.0. comparaison des éléments enum (type enum)
Alors, commençons!

Comparaison de chaînes : ' ==' etequals

Ah, ces lignes... L'un des types les plus couramment utilisés, ce qui pose beaucoup de problèmes. En principe, il existe un article séparé à leur sujet . Et ici, j'aborderai les problèmes de comparaison. Bien entendu, les chaînes peuvent être comparées en utilisant equals. De plus, ils DOIVENT être comparés via equals. Cependant, il y a des subtilités qui valent la peine d’être connues. Tout d’abord, les chaînes identiques sont en réalité un seul objet. Cela peut être facilement vérifié en exécutant le code suivant :

String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
Le résultat sera le même" . Ce qui signifie que les références de chaîne sont égales. Cela se fait au niveau du compilateur, évidemment pour économiser de la mémoire. Le compilateur crée UNE instance de la chaîne et attribue str1une str2référence à cette instance. Cependant, cela ne s'applique qu'aux chaînes déclarées comme littéraux dans le code. Si vous composez une chaîne à partir de morceaux, le lien vers celle-ci sera différent. Confirmation - cet exemple :

String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
Le résultat ne sera « pas le même » . Vous pouvez également créer un nouvel objet à l'aide du constructeur de copie :

String str1 = "string";
String str2 = new String("string");
System.out.println(str1==str2 ? "the same" : "not the same");
Le résultat ne sera également « pas le même » . Ainsi, les chaînes peuvent parfois être comparées via une comparaison de références. Mais il vaut mieux ne pas s'appuyer sur cela. Je voudrais aborder une méthode très intéressante qui permet d'obtenir la représentation dite canonique d'une chaîne - String.intern. Parlons-en plus en détail.

Méthode String.intern

Commençons par le fait que la classe Stringprend en charge un pool de chaînes. Tous les littéraux de chaîne définis dans les classes, et pas seulement eux, sont ajoutés à ce pool. Ainsi, la méthode internpermet d'obtenir de ce pool une chaîne qui est égale à celle existante (celle sur laquelle la méthode est appelée intern) du point de vue de equals. Si une telle ligne n'existe pas dans le pool, alors celle existante y est placée et un lien vers celle-ci est renvoyé. Ainsi, même si les références à deux chaînes égales sont différentes (comme dans les deux exemples ci-dessus), alors les appels à ces chaînes internrenverront une référence au même objet :

String str1 = "string";
String str2 = new String("string");
System.out.println(str1.intern()==str2.intern() ? "the same" : "not the same");
Le résultat de l'exécution de ce morceau de code sera "le même" . Je ne peux pas dire exactement pourquoi cela a été fait de cette façon. La méthode internest native, et pour être honnête, je ne veux pas entrer dans les profondeurs du code C. Très probablement, cela est fait pour optimiser la consommation de mémoire et les performances. Dans tous les cas, il vaut la peine de connaître cette fonctionnalité d’implémentation. Passons à la partie suivante.

Comparaison de vraies primitives

Pour commencer, je veux poser une question. Très simple. Quelle est la somme suivante – 0,3f + 0,4f ? Pourquoi? 0,7f ? Allons vérifier:

float f1 = 0.7f;
float f2 = 0.3f + 0.4f;
System.out.println("f1==f2: "+(f1==f2));
Par conséquent? Comme? Moi aussi. Pour ceux qui n'ont pas terminé ce fragment, je dirai que le résultat sera...

f1==f2: false
Pourquoi cela se produit-il ?.. Effectuons un autre test :

float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("f1="+(double)f1);
System.out.println("f2="+(double)f2);
System.out.println("f3="+(double)f3);
System.out.println("f4="+(double)f4);
Notez la conversion en double. Ceci est fait afin d'afficher plus de décimales. Résultat:

f1=0.30000001192092896
f2=0.4000000059604645
f3=0.7000000476837158
f4=0.699999988079071
À proprement parler, le résultat est prévisible. La représentation de la partie fractionnaire s'effectue à l'aide d'une série finie 2-n, et il n'est donc pas nécessaire de parler de la représentation exacte d'un nombre arbitrairement choisi. Comme le montre l'exemple, la précision de la représentation floatest de 7 décimales. À proprement parler, la représentation float alloue 24 bits à la mantisse. Ainsi, le nombre absolu minimum qui peut être représenté en utilisant float (sans tenir compte du degré, car on parle de précision) est 2-24≈6*10-8. C'est à cette étape que vont réellement les valeurs de la représentation float. Et comme il y a quantification, il y a aussi une erreur. D’où la conclusion : les nombres d’une représentation floatne peuvent être comparés qu’avec une certaine précision. Je recommanderais de les arrondir à la 6ème décimale (10-6), ou, de préférence, de vérifier la valeur absolue de la différence entre eux :

float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("|f3-f4|<1e-6: "+( Math.abs(f3-f4) < 1e-6 ));
Dans ce cas, le résultat est encourageant :

|f3-f4|<1e-6: true
Bien sûr, l'image est exactement la même avec le type double. La seule différence est que 53 bits sont alloués pour la mantisse, la précision de la représentation est donc de 2-53≈10-16. Oui, la valeur de quantification est beaucoup plus petite, mais elle est là. Et cela peut faire une blague cruelle. D'ailleurs, dans la bibliothèque de tests JUnit , dans les méthodes de comparaison de nombres réels, la précision est explicitement spécifiée. Ceux. la méthode de comparaison contient trois paramètres : le nombre, ce à quoi il doit être égal et la précision de la comparaison. À propos, je voudrais mentionner les subtilités liées à l'écriture des nombres dans un format scientifique, indiquant le degré. Question. Comment écrire 10-6 ? La pratique montre que plus de 80 % répondent – ​​10e-6. Pendant ce temps, la bonne réponse est 1e-6 ! Et 10e-6 fait 10-5 ! Nous avons marché sur ce râteau dans l'un des projets, de manière tout à fait inattendue. Ils ont cherché l'erreur pendant très longtemps, ont regardé les constantes 20 fois et personne n'a eu l'ombre d'un doute sur leur exactitude, jusqu'au jour où, en grande partie par accident, la constante 10e-3 a été imprimée et ils ont trouvé deux chiffres après la virgule au lieu des trois attendus. Soyez donc prudent ! Allons-nous en.

+0,0 et -0,0

Dans la représentation des nombres réels, le bit de poids fort est signé. Que se passe-t-il si tous les autres bits sont à 0 ? Contrairement aux entiers, où dans une telle situation le résultat est un nombre négatif situé à la limite inférieure de la plage de représentation, un nombre réel dont seul le bit le plus significatif est défini sur 1 signifie également 0, uniquement avec un signe moins. Ainsi, nous avons deux zéros - +0,0 et -0,0. Une question logique se pose : ces nombres doivent-ils être considérés comme égaux ? La machine virtuelle pense exactement de cette façon. Cependant, ce sont deux nombres différents , car à la suite d'opérations avec eux, des valeurs différentes sont obtenues :

float f1 = 0.0f/1.0f;
float f2 = 0.0f/-1.0f;
System.out.println("f1="+f1);
System.out.println("f2="+f2);
System.out.println("f1==f2: "+(f1==f2));
float f3 = 1.0f / f1;
float f4 = 1.0f / f2;
System.out.println("f3="+f3);
System.out.println("f4="+f4);
... et le résultat :

f1=0.0
f2=-0.0
f1==f2: true
f3=Infinity
f4=-Infinity
Ainsi, dans certains cas, il est logique de traiter +0,0 et -0,0 comme deux nombres différents. Et si nous avons deux objets, dans l'un desquels le champ est +0,0 et dans l'autre -0,0, ces objets peuvent également être considérés comme inégaux. La question se pose : comment comprendre que les chiffres sont inégaux si leur comparaison directe avec une machine virtuelle le donne true? La réponse est la suivante. Même si la machine virtuelle considère ces nombres comme égaux, leurs représentations restent différentes. La seule chose à faire est donc de comparer les points de vue. Et pour l'obtenir, il existe des méthodes int Float.floatToIntBits(float)et long Double.doubleToLongBits(double), qui renvoient une représentation binaire sous la forme intet longrespectivement (suite de l'exemple précédent) :

int i1 = Float.floatToIntBits(f1);
int i2 = Float.floatToIntBits(f2);
System.out.println("i1 (+0.0):"+ Integer.toBinaryString(i1));
System.out.println("i2 (-0.0):"+ Integer.toBinaryString(i2));
System.out.println("i1==i2: "+(i1 == i2));
Le résultat sera

i1 (+0.0):0
i2 (-0.0):10000000000000000000000000000000
i1==i2: false
Ainsi, si vous avez +0,0 et -0,0 sont des nombres différents, alors vous devez comparer les variables réelles via leur représentation binaire. Il semble que nous ayons réglé +0,0 et -0,0. -0,0 n’est cependant pas la seule surprise. Il existe aussi une chose telle que...

Valeur NaN

NaNreprésente Not-a-Number. Cette valeur apparaît à la suite d'opérations mathématiques incorrectes, par exemple diviser 0,0 par 0,0, l'infini par l'infini, etc. La particularité de cette valeur est qu’elle n’est pas égale à elle-même. Ceux.:

float x = 0.0f/0.0f;
System.out.println("x="+x);
System.out.println("x==x: "+(x==x));
...résultera...

x=NaN
x==x: false
Comment cela peut-il se produire lors de la comparaison d’objets ? Si le champ de l'objet est égal à NaN, alors la comparaison donnera false, c'est-à-dire les objets sont assurément considérés comme inégaux. Bien que, logiquement, nous souhaitions peut-être exactement le contraire. Vous pouvez obtenir le résultat souhaité en utilisant la méthode Float.isNaN(float). Il retourne truesi l'argument est NaN. Dans ce cas, je ne me baserais pas sur la comparaison des représentations binaires, car ce n’est pas standardisé. Cela suffit peut-être pour les primitifs. Passons maintenant aux subtilités apparues en Java depuis la version 5.0. Et le premier point que je voudrais aborder est

Java 5.0. Génération de méthodes et comparaison via ' =='

Il existe un modèle de conception appelé méthode de production.. Parfois, son utilisation est bien plus rentable que le recours à un constructeur. Laisse moi te donner un exemple. Je pense que je connais bien le shell de l'objet Boolean. Cette classe est immuable et ne peut contenir que deux valeurs. Autrement dit, pour tous les besoins, seuls deux exemplaires suffisent. Et si vous les créez à l'avance puis les renvoyez simplement, ce sera beaucoup plus rapide que d'utiliser un constructeur. Il existe une telle méthode Boolean: valueOf(boolean). Il est apparu dans la version 1.4. Des méthodes de production similaires ont été introduites dans la version 5.0 dans les classes Byte, Character, et . Lorsque ces classes sont chargées, des tableaux de leurs instances sont créés correspondant à certaines plages de valeurs primitives. Ces plages sont les suivantes : ShortIntegerLong
Comparaison d'objets : pratique - 2
Cela signifie que lors de l'utilisation de la méthode, valueOf(...)si l'argument se situe dans la plage spécifiée, le même objet sera toujours renvoyé. Peut-être que cela donne une certaine augmentation de vitesse. Mais en même temps, des problèmes surgissent d’une telle nature qu’il peut être assez difficile d’aller au fond des choses. En savoir plus à ce sujet. En théorie, la méthode de production valueOfa été ajoutée aux classes Floatet Double. Leur description indique que si vous n'avez pas besoin d'une nouvelle copie, il est préférable d'utiliser cette méthode, car cela peut donner une augmentation de la vitesse, etc. et ainsi de suite. Cependant, dans l'implémentation actuelle (Java 5.0), une nouvelle instance est créée dans cette méthode, c'est-à-dire Son utilisation n’est pas garantie pour augmenter la vitesse. De plus, il m'est difficile d'imaginer comment cette méthode peut être accélérée, car du fait de la continuité des valeurs, un cache ne peut y être organisé. Sauf pour les entiers. Je veux dire, sans la partie fractionnaire.

Java 5.0. Autoboxing/Unboxing : ' ==', ' >=' et ' <=' pour les wrappers d'objets.

Je soupçonne que les méthodes de production et le cache d'instance ont été ajoutés aux wrappers des primitives entières afin d'optimiser les opérations autoboxing/unboxing. Laissez-moi vous rappeler ce que c'est. Si un objet doit être impliqué dans une opération, mais qu'une primitive est impliquée, alors cette primitive est automatiquement enveloppée dans un wrapper d'objet. Ce autoboxing. Et vice versa - si une primitive doit être impliquée dans l'opération, vous pouvez alors y substituer un shell d'objet et la valeur en sera automatiquement développée. Ce unboxing. Naturellement, pour une telle commodité, vous devez payer. Les opérations de conversion automatique ralentissent quelque peu l'application. Cependant, cela n’a rien à voir avec le sujet actuel, alors laissons de côté cette question. Tout va bien tant qu'il s'agit d'opérations clairement liées aux primitives ou aux shells. Qu'adviendra-t-il de l' ==opération « » ? Disons que nous avons deux objets Integeravec la même valeur à l'intérieur. Comment vont-ils se comparer ?

Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1==i2: "+(i1==i2));
Résultat:
i1==i2: false

Кто бы сомневался... Сравниваются они How an objectы. А если так:Integer i1 = 1;
Integer i2 = 1;
System.out.println("i1==i2: "+(i1==i2));
Résultat:

i1==i2: true
Maintenant, c'est plus intéressant ! Si autoboxing-e, les mêmes objets sont renvoyés ! C’est là que réside le piège. Une fois que nous découvrirons que les mêmes objets sont renvoyés, nous commencerons à expérimenter pour voir si c'est toujours le cas. Et combien de valeurs allons-nous vérifier ? Un? Dix? Cent? Très probablement, nous nous limiterons à une centaine dans chaque direction autour de zéro. Et nous obtenons l’égalité partout. Il semblerait que tout va bien. Cependant, regardez un peu en arrière, ici . Avez-vous deviné quel est le problème ? Oui, les instances de shells d'objets pendant l'autoboxing sont créées à l'aide de méthodes de production. Ceci est bien illustré par le test suivant :

public class AutoboxingTest {

    private static final int numbers[] = new int[]{-129,-128,127,128};

    public static void main(String[] args) {
        for (int number : numbers) {
            Integer i1 = number;
            Integer i2 = number;
            System.out.println("number=" + number + ": " + (i1 == i2));
        }
    }
}
Le résultat sera comme ceci :

number=-129: false
number=-128: true
number=127: true
number=128: false
Pour les valeurs comprises dans la plage de mise en cache , des objets identiques sont renvoyés, pour celles en dehors, des objets différents sont renvoyés. Et par conséquent, si quelque part dans l'application, les shells sont comparés au lieu des primitives, il y a une chance d'obtenir l'erreur la plus terrible : une erreur flottante. Car le code sera très probablement également testé sur une plage limitée de valeurs dans laquelle cette erreur n'apparaîtra pas. Mais dans le travail réel, il apparaîtra ou disparaîtra, selon les résultats de certains calculs. Il est plus facile de devenir fou que de trouver une telle erreur. Par conséquent, je vous conseillerais d’éviter autant que possible l’autoboxing. Et ce n'est pas ça. Rappelons les mathématiques, pas plus loin que la 5e année. Laissez les inégalités A>=Bet А<=B. Que peut-on dire de la relation Aet B? Il n’y a qu’une chose : ils sont égaux. Êtes-vous d'accord? Je pense que oui. Lançons le test :

Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1>=i2: "+(i1>=i2));
System.out.println("i1<=i2: "+(i1<=i2));
System.out.println("i1==i2: "+(i1==i2));
Résultat:

i1>=i2: true
i1<=i2: true
i1==i2: false
Et c’est la chose la plus étrange pour moi. Je ne comprends pas du tout pourquoi cette fonctionnalité a été introduite dans le langage si elle introduit de telles contradictions. En général, je le répète encore une fois - s'il est possible de s'en passer autoboxing/unboxing, cela vaut la peine d'utiliser cette opportunité au maximum. Le dernier sujet que je voudrais aborder est... Java 5.0. comparaison des éléments d'énumération (type enum) Comme vous le savez, depuis la version 5.0, Java a introduit un type tel que enum - énumération. Ses instances contiennent par défaut le nom et le numéro de séquence dans la déclaration d'instance dans la classe. En conséquence, lorsque l’ordre des annonces change, les chiffres changent. Cependant, comme je l'ai dit dans l'article « La sérialisation telle qu'elle est » , cela ne pose pas de problèmes. Tous les éléments d'énumération existent en une seule copie, ceci est contrôlé au niveau de la machine virtuelle. Ils peuvent donc être comparés directement, à l’aide de liens. * * * C'est peut-être tout pour aujourd'hui concernant l'aspect pratique de la mise en œuvre de la comparaison d'objets. Peut-être qu'il me manque quelque chose. Comme toujours, j'attends vos commentaires avec impatience ! Pour l'instant, permettez-moi de prendre congé. Merci à tous pour votre attention ! Lien vers la source : Comparer des objets : pratique
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION