JavaRush /Java-Blog /Random-DE /Vergleich von Objekten: Praxis
articles
Level 15

Vergleich von Objekten: Praxis

Veröffentlicht in der Gruppe Random-DE
Dies ist der zweite Artikel, der sich dem Vergleich von Objekten widmet. Der erste von ihnen diskutierte die theoretischen Grundlagen des Vergleichs – wie er durchgeführt wird, warum und wo er verwendet wird. In diesem Artikel werden wir direkt über den Vergleich von Zahlen, Objekten, Sonderfällen, Feinheiten und nicht offensichtlichen Punkten sprechen. Genauer gesagt, hier ist, worüber wir sprechen werden:
Objektvergleich: Praxis - 1
  • String-Vergleich: ' ==' undequals
  • MethodeString.intern
  • Vergleich realer Primitiven
  • +0.0Und-0.0
  • BedeutungNaN
  • Java 5.0. Generierung von Methoden und Vergleich über ' =='
  • Java 5.0. Autoboxing/Unboxing: ' ==', ' >=' und ' <=' für Objekt-Wrapper.
  • Java 5.0. Vergleich von Enum-Elementen (Typ enum)
Also lasst uns anfangen!

String-Vergleich: ' ==' undequals

Ah, diese Zeilen... Einer der am häufigsten verwendeten Typen, der viele Probleme verursacht. Grundsätzlich gibt es dazu einen eigenen Artikel . Und hier werde ich auf Vergleichsfragen eingehen. Natürlich können Strings mit verglichen werden equals. Darüber hinaus MÜSSEN sie über verglichen werden equals. Es gibt jedoch Feinheiten, die es zu wissen gilt. Erstens sind identische Zeichenfolgen tatsächlich ein einzelnes Objekt. Dies kann leicht überprüft werden, indem Sie den folgenden Code ausführen:
String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
Das Ergebnis wird „das Gleiche“ sein . Das bedeutet, dass die String-Referenzen gleich sind. Dies geschieht auf Compiler-Ebene, offensichtlich um Speicherplatz zu sparen. Der Compiler erstellt EINE Instanz der Zeichenfolge und weist dieser Instanz str1eine Referenz zu. str2Dies gilt jedoch nur für Zeichenfolgen, die im Code als Literale deklariert sind. Wenn Sie eine Zeichenfolge aus Teilen zusammenstellen, ist der Link dazu anders. Bestätigung – dieses Beispiel:
String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
Das Ergebnis wird „nicht dasselbe“ sein . Sie können auch mit dem Kopierkonstruktor ein neues Objekt erstellen:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1==str2 ? "the same" : "not the same");
Das Ergebnis wird auch „nicht dasselbe“ sein . Daher können Zeichenfolgen manchmal durch Referenzvergleich verglichen werden. Aber darauf sollte man sich besser nicht verlassen. Ich möchte auf eine sehr interessante Methode eingehen, mit der Sie die sogenannte kanonische Darstellung einer Zeichenfolge erhalten können – String.intern. Lassen Sie uns ausführlicher darüber sprechen.

String.intern-Methode

Beginnen wir mit der Tatsache, dass die Klasse Stringeinen String-Pool unterstützt. Alle in Klassen definierten String-Literale und nicht nur diese werden diesem Pool hinzugefügt. Die Methode ermöglicht es Ihnen also, internaus diesem Pool eine Zeichenfolge abzurufen, die internaus Sicht von der vorhandenen Zeichenfolge (der Zeichenfolge, auf der die Methode aufgerufen wird) entspricht equals. Wenn eine solche Zeile im Pool nicht vorhanden ist, wird die vorhandene Zeile dort platziert und ein Link darauf zurückgegeben. Selbst wenn also die Verweise auf zwei gleiche Zeichenfolgen unterschiedlich sind (wie in den beiden Beispielen oben), geben Aufrufe dieser Zeichenfolgen interneine Referenz auf dasselbe Objekt zurück:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1.intern()==str2.intern() ? "the same" : "not the same");
Das Ergebnis der Ausführung dieses Codeteils ist „das Gleiche“ . Ich kann nicht genau sagen, warum das so gemacht wurde. Die Methode internist nativ und um ehrlich zu sein, möchte ich mich nicht in die Wildnis des C-Codes begeben. Dies geschieht höchstwahrscheinlich, um den Speicherverbrauch und die Leistung zu optimieren. In jedem Fall lohnt es sich, über diese Implementierungsfunktion Bescheid zu wissen. Kommen wir zum nächsten Teil.

Vergleich realer Primitiven

Zunächst möchte ich eine Frage stellen. Sehr einfach. Wie lautet die folgende Summe – 0,3f + 0,4f? Warum? 0,7f? Lass uns das Prüfen:
float f1 = 0.7f;
float f2 = 0.3f + 0.4f;
System.out.println("f1==f2: "+(f1==f2));
Infolge? Gefällt? Mir auch. Für diejenigen, die dieses Fragment nicht fertiggestellt haben, möchte ich sagen, dass das Ergebnis ... sein wird.
f1==f2: false
Warum passiert das? Führen wir einen weiteren Test durch:
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);
Beachten Sie die Konvertierung in double. Dies geschieht, um mehr Dezimalstellen auszugeben. Ergebnis:
f1=0.30000001192092896
f2=0.4000000059604645
f3=0.7000000476837158
f4=0.699999988079071
Streng genommen ist das Ergebnis vorhersehbar. Die Darstellung des Bruchteils erfolgt über eine endliche Reihe 2-n, sodass über die exakte Darstellung einer willkürlich gewählten Zahl nicht gesprochen werden muss. Wie aus dem Beispiel ersichtlich ist, floatbeträgt die Darstellungsgenauigkeit 7 Nachkommastellen. Genau genommen float weist die Darstellung der Mantisse 24 Bit zu. Somit beträgt die minimale absolute Zahl, die mit float (ohne Berücksichtigung des Grades, da es sich um Genauigkeit handelt) dargestellt werden kann, 2-24≈6*10-8. Mit diesem Schritt gehen die Werte in die Darstellung tatsächlich über float. Und da es eine Quantisierung gibt, liegt auch ein Fehler vor. Daher die Schlussfolgerung: Zahlen in einer Darstellung floatkönnen nur mit einer gewissen Genauigkeit verglichen werden. Ich würde empfehlen, sie auf die 6. Dezimalstelle (10-6) zu runden oder, vorzugsweise, den absoluten Wert der Differenz zwischen ihnen zu überprüfen:
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 ));
In diesem Fall ist das Ergebnis ermutigend:
|f3-f4|<1e-6: true
Natürlich ist das Bild beim Typ genau das gleiche double. Der einzige Unterschied besteht darin, dass der Mantisse 53 Bits zugewiesen sind, daher beträgt die Darstellungsgenauigkeit 2-53≈10-16. Ja, der Quantisierungswert ist viel kleiner, aber er ist vorhanden. Und es kann einen grausamen Scherz spielen. Übrigens wird in der JUnit- Testbibliothek in den Methoden zum Vergleich reeller Zahlen die Genauigkeit explizit angegeben. Diese. Die Vergleichsmethode enthält drei Parameter: die Zahl, den Wert, dem sie entsprechen soll, und die Genauigkeit des Vergleichs. Übrigens möchte ich die Feinheiten erwähnen, die mit dem Schreiben von Zahlen in wissenschaftlicher Form und der Angabe des Abschlusses verbunden sind. Frage. Wie schreibe ich 10-6? Die Praxis zeigt, dass mehr als 80 % mit 10e-6 antworten. Mittlerweile ist die richtige Antwort 1e-6! Und 10e-6 ist 10-5! Wir sind bei einem der Projekte ganz unerwartet auf diesen Rechen getreten. Sie suchten sehr lange nach dem Fehler, schauten sich die Konstanten 20 Mal an. Und niemand hatte den geringsten Zweifel an ihrer Richtigkeit, bis eines Tages, größtenteils zufällig, die Konstante 10e-3 gedruckt wurde und sie zwei fanden Stellen nach dem Komma anstelle der erwarteten drei. Seien Sie daher vorsichtig! Lass uns weitermachen.

+0,0 und -0,0

Bei der Darstellung reeller Zahlen ist das höchstwertige Bit vorzeichenbehaftet. Was passiert, wenn alle anderen Bits 0 sind? Im Gegensatz zu ganzen Zahlen, bei denen in einer solchen Situation das Ergebnis eine negative Zahl ist, die an der unteren Grenze des Darstellungsbereichs liegt, bedeutet eine reelle Zahl, bei der nur das höchstwertige Bit auf 1 gesetzt ist, auch 0, nur mit einem Minuszeichen. Somit haben wir zwei Nullen – +0,0 und –0,0. Es stellt sich die logische Frage: Sollten diese Zahlen als gleich betrachtet werden? Die virtuelle Maschine denkt genau so. Dies sind jedoch zwei verschiedene Zahlen, da durch Operationen mit ihnen unterschiedliche Werte erhalten werden:
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);
... und das Ergebnis:
f1=0.0
f2=-0.0
f1==f2: true
f3=Infinity
f4=-Infinity
Daher ist es in manchen Fällen sinnvoll, +0,0 und -0,0 als zwei unterschiedliche Zahlen zu behandeln. Und wenn wir zwei Objekte haben, bei denen das Feld +0,0 und das andere -0,0 ist, können diese Objekte auch als ungleich betrachtet werden. Es stellt sich die Frage: Wie kann man verstehen, dass die Zahlen ungleich sind, wenn der direkte Vergleich mit einer virtuellen Maschine ergibt true? Die Antwort ist diese. Auch wenn die virtuelle Maschine diese Zahlen als gleich ansieht, sind ihre Darstellungen dennoch unterschiedlich. Daher bleibt nur ein Vergleich der Ansichten. Und um es zu bekommen, gibt es Methoden int Float.floatToIntBits(float)und long Double.doubleToLongBits(double), die eine Bitdarstellung in der Form intbzw. zurückgeben long(Fortsetzung des vorherigen Beispiels):
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));
Das Ergebnis wird sein
i1 (+0.0):0
i2 (-0.0):10000000000000000000000000000000
i1==i2: false
Wenn also +0,0 und -0,0 unterschiedliche Zahlen sind, sollten Sie reale Variablen anhand ihrer Bitdarstellung vergleichen. Wir scheinen +0,0 und -0,0 geklärt zu haben. -0,0 ist jedoch nicht die einzige Überraschung. Es gibt auch so etwas wie...

NaN-Wert

NaNsteht für Not-a-Number. Dieser Wert entsteht als Ergebnis falscher mathematischer Operationen, beispielsweise der Division von 0,0 durch 0,0, Unendlich durch Unendlich usw. Die Besonderheit dieses Wertes besteht darin, dass er nicht sich selbst entspricht. Diese.:
float x = 0.0f/0.0f;
System.out.println("x="+x);
System.out.println("x==x: "+(x==x));
...wird dazu führen...
x=NaN
x==x: false
Wie kann das beim Vergleich von Objekten ausfallen? Wenn das Feld des Objekts gleich ist NaN, dann wird der Vergleich ergeben false, d.h. Objekte gelten garantiert als ungleich. Obwohl wir logischerweise vielleicht genau das Gegenteil wollen. Mit der Methode können Sie das gewünschte Ergebnis erzielen Float.isNaN(float). Es wird zurückgegeben true, wenn das Argument lautet NaN. In diesem Fall würde ich mich nicht auf den Vergleich von Bitdarstellungen verlassen, weil es ist nicht standardisiert. Vielleicht reicht das mit den Primitiven. Kommen wir nun zu den Feinheiten, die in Java seit Version 5.0 aufgetaucht sind. Und der erste Punkt, den ich ansprechen möchte, ist

Java 5.0. Generierung von Methoden und Vergleich über ' =='

Es gibt ein Muster im Design, das als Herstellungsmethode bezeichnet wird. Manchmal ist seine Verwendung viel rentabler als die Verwendung eines Konstruktors. Lassen Sie mich Ihnen ein Beispiel geben. Ich glaube, ich kenne die Objekthülle gut Boolean. Diese Klasse ist unveränderlich und kann nur zwei Werte enthalten. Das heißt, für jeden Bedarf reichen nur zwei Kopien aus. Und wenn Sie sie im Voraus erstellen und sie dann einfach zurückgeben, ist dies viel schneller als die Verwendung eines Konstruktors. Es gibt eine solche Methode Boolean: valueOf(boolean). Es erschien in Version 1.4. Ähnliche Produktionsmethoden wurden in Version 5.0 in den Klassen Byte, Character, und eingeführt . Wenn diese Klassen geladen werden, werden Arrays ihrer Instanzen erstellt, die bestimmten Bereichen primitiver Werte entsprechen. Diese Bereiche sind wie folgt: ShortIntegerLong
Objektvergleich: Praxis - 2
Dies bedeutet, dass bei Verwendung der Methode valueOf(...)immer dasselbe Objekt zurückgegeben wird, wenn das Argument in den angegebenen Bereich fällt. Möglicherweise erhöht dies die Geschwindigkeit. Doch gleichzeitig entstehen Probleme, deren Beschaffenheit es recht schwierig macht, ihnen auf den Grund zu gehen. Lesen Sie mehr darüber. Theoretisch wurde die Produktionsmethode sowohl zu den Klassen als auch hinzugefügt . In ihrer Beschreibung heißt es, dass es besser ist, diese Methode zu verwenden, wenn Sie keine neue Kopie benötigen, denn es kann zu einer Erhöhung der Geschwindigkeit usw. führen. usw. Allerdings wird in der aktuellen (Java 5.0) Implementierung bei dieser Methode eine neue Instanz erstellt, d.h. Es ist nicht garantiert, dass die Verwendung zu einer Geschwindigkeitssteigerung führt. Darüber hinaus kann ich mir nur schwer vorstellen, wie diese Methode beschleunigt werden kann, da aufgrund der Wertekontinuität dort kein Cache organisiert werden kann. Außer ganze Zahlen. Ich meine, ohne den Bruchteil.valueOfFloatDouble

Java 5.0. Autoboxing/Unboxing: ' ==', ' >=' und ' <=' für Objekt-Wrapper.

Ich vermute, dass die Produktionsmethoden und der Instanzcache zu Wrappern für ganzzahlige Grundelemente hinzugefügt wurden, um den Betrieb zu optimieren autoboxing/unboxing. Ich möchte Sie daran erinnern, was es ist. Wenn ein Objekt an einer Operation beteiligt sein muss, aber ein Grundelement beteiligt ist, wird dieses Grundelement automatisch in einen Objekt-Wrapper eingeschlossen. Das autoboxing. Und umgekehrt – wenn ein Grundelement an der Operation beteiligt sein muss, können Sie dort eine Objekthülle ersetzen und der Wert wird automatisch daraus erweitert. Das unboxing. Natürlich muss man für diesen Komfort bezahlen. Automatische Konvertierungsvorgänge verlangsamen die Anwendung etwas. Dies ist jedoch für das aktuelle Thema nicht relevant, daher lassen wir es bei dieser Frage. Alles ist in Ordnung, solange es sich um Operationen handelt, die eindeutig mit Primitiven oder Shells zusammenhängen. Was passiert mit der ==Operation „ “? Nehmen wir an, wir haben zwei Objekte Integermit demselben Wert darin. Wie werden sie sich vergleichen?
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1==i2: "+(i1==i2));
Ergebnis:
i1==i2: false

Кто бы сомневался... Сравниваются они Wie ein Objektы. А если так:Integer i1 = 1;
Integer i2 = 1;
System.out.println("i1==i2: "+(i1==i2));
Ergebnis:
i1==i2: true
Das ist jetzt interessanter! Wenn autoboxing-e werden die gleichen Objekte zurückgegeben! Hier liegt die Falle. Sobald wir feststellen, dass dieselben Objekte zurückgegeben werden, beginnen wir mit dem Experimentieren, um herauszufinden, ob dies immer der Fall ist. Und wie viele Werte werden wir überprüfen? Eins? Zehn? Einhundert? Höchstwahrscheinlich werden wir uns auf hundert in jede Richtung um Null beschränken. Und wir bekommen überall Gleichheit. Es scheint, dass alles in Ordnung ist. Schauen Sie hier jedoch etwas zurück . Haben Sie erraten, wo der Haken liegt? Ja, Instanzen von Objekthüllen werden beim Autoboxing mithilfe von Produktionsmethoden erstellt. Dies wird durch den folgenden Test gut veranschaulicht:
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));
        }
    }
}
Das Ergebnis wird so aussehen:
number=-129: false
number=-128: true
number=127: true
number=128: false
Für Werte, die innerhalb des Caching-Bereichs liegen , werden identische Objekte zurückgegeben, für Werte außerhalb dieses Bereichs werden unterschiedliche Objekte zurückgegeben. Wenn daher irgendwo in der Anwendung Shells anstelle von Grundelementen verglichen werden, besteht die Möglichkeit, dass der schrecklichste Fehler auftritt: ein schwebender Fehler. Denn höchstwahrscheinlich wird der Code auch auf einen begrenzten Wertebereich getestet, in dem dieser Fehler nicht auftritt. Aber in der realen Arbeit wird es je nach den Ergebnissen einiger Berechnungen entweder erscheinen oder verschwinden. Es ist einfacher, verrückt zu werden, als einen solchen Fehler zu finden. Daher würde ich Ihnen raten, Autoboxing nach Möglichkeit zu vermeiden. Und das ist es nicht. Erinnern wir uns an Mathematik, nicht weiter als bis zur 5. Klasse. Lassen Sie die Ungleichungen A>=Bund А<=B. AWas lässt sich über die Beziehung und sagen B? Es gibt nur eines: Sie sind gleich. Sind Sie einverstanden? Ich denke ja. Lassen Sie uns den Test durchführen:
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));
Ergebnis:
i1>=i2: true
i1<=i2: true
i1==i2: false
Und das ist für mich das Seltsamste. Ich verstehe überhaupt nicht, warum diese Funktion in die Sprache eingeführt wurde, wenn sie solche Widersprüche mit sich bringt. Im Allgemeinen wiederhole ich es noch einmal: Wenn es möglich ist, darauf zu verzichten autoboxing/unboxing, dann lohnt es sich, diese Gelegenheit in vollem Umfang zu nutzen. Das letzte Thema, das ich ansprechen möchte, ist... Java 5.0. Vergleich von Aufzählungselementen (Aufzählungstyp) Wie Sie wissen, hat Java seit Version 5.0 einen Typ wie Aufzählung eingeführt - Aufzählung. Seine Instanzen enthalten standardmäßig den Namen und die Sequenznummer in der Instanzdeklaration in der Klasse. Wenn sich die Ankündigungsreihenfolge ändert, ändern sich dementsprechend auch die Zahlen. Wie ich jedoch im Artikel „Serialisierung wie sie ist“ gesagt habe , verursacht dies keine Probleme. Alle Aufzählungselemente liegen in einer einzigen Kopie vor, dies wird auf der Ebene der virtuellen Maschine gesteuert. Daher können sie über Links direkt verglichen werden. * * * Vielleicht ist das für heute alles über die praktische Seite der Implementierung des Objektvergleichs. Vielleicht übersehe ich etwas. Ich freue mich wie immer auf eure Kommentare! Lassen Sie mich vorerst verabschieden. Vielen Dank für Ihre Aufmerksamkeit! Link zur Quelle: Objekte vergleichen: Übung
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION