equals und instanceOf pattern matching

mihe7

Top Contributor
Seit Java 16 gibt es das instanceOf pattern matching, das ich bislang einfach nur als Vereinfachung gesehen habe. Jetzt bin ich allerdings über diverse Inhalte gestoßen, von denen ich einigermaßen begeistert bin und die ich Euch nicht vorenthalten möchte.



Vorher aber kurz eine Erklärung, für diejenigen, die nicht wissen, worum es beim instanceOf pattern matching geht. Manchmal steht man vor dem Problem, dass man ein Objekt bekommt und prüfen muss, ob das Objekt eine Instanz eines bestimmten Typs ist, um anschließend mit den Methoden dieses Typs weiterarbeiten zu können.

Bis Java 15 waren hierfür drei Schritte notwendig.
  1. Prüfen, ob das gegebene Objekt eine Instanz des Typs ist.
  2. Falls ja, das Objekt auf den betreffenden Typ casten und
  3. einer mit diesem Typ deklarierten, lokalen Variablen zuweisen.
In Code:
Java:
if (obj instanceOf Typ) {
    Typ instanz = (Typ) obj;
    // mach was mit instanz
}

Siet Java 16 kann man nun kurz schreiben:
Java:
if (obj instanceof Typ instanz) {
    // mach was mit instanz
}




Zurück zum eigentlichen Thema. Die equals-Methode wird üblicherweise ähnlich überschrieben, wie im folgenden Beispiel gezeigt:
Java:
public boolean equals(Object obj) {
    if (obj == this) return true;
    if (obj == null) return false;
    if (!(obj instanceof Typ)) return false;
    Typ other = (Typ) obj;
    return this.x == other.x
            && this.y == other.y;
}

Mit pattern matching kann man nun z. B. schreiben:
Java:
public boolean equals(Object obj) {
    if (this == obj) return true;
    return (obj instanceof Typ other)
            && this.x == other.x
            && this.y == other.y;
}

In dieser Art hat das Brian Goetz auch für das JDK committed. Klar, das ist kürzer und einfacher zu lesen. Der Knaller aber ist, dass der Spaß auch noch schneller ist (wobei das auch davon abhängt, womit man vergleicht):
Code:
Benchmark       (mode)   Mode  Cnt       Score       Error  Units
Main.benchmark     old  thrpt   30  241058,051 ±  4640,652  ops/s
Main.benchmark     new  thrpt   30  629972,551 ± 11241,051  ops/s
(Edit / Hinweis: hier hat sich leider ein Fehler eingeschlichen - siehe #8 für Details bezüglich der Geschwindigkeit, die ungefähr gleich ist bei beiden Varianten)

Im JEP zum instanceOf pattern matching wird die equals-Methode gar in dieser Weise implementiert:
Java:
public boolean equals(Object obj) {
    return (obj instanceof Typ other)
            && this.x == other.x
            && this.y == other.y;
}

Das gefällt mir! Aber warum wird hier sogar völlig auf die Referenzvergleiche verzichtet? José Paumard hat darauf eine Antwort: predictive branching.

Die CPU macht Vorhersagen, welcher Zweig durchlaufen werden wird und kann so die Pipeline besser füllen. Stellt sich heraus, dass die Vorhersage falsch war (der Referenzvergleich muss ja in jedem Fall ausgeführt werden), muss zurückgesetzt werden - was im Verhältnis massig Zeit kostet. In Summe macht dies den Versuch der Optimierung über den Referenzvergleich oft zunichte.
 
Zuletzt bearbeitet von einem Moderator:
Im JEP zum instanceOf pattern matching wird die equals-Methode gar in dieser Weise implementiert:
Java:
public boolean equals(Object obj) {
    return (obj instanceof Typ other)
            && this.x == other.x 
            && this.y == other.y;
}

Das gefällt mir! Aber warum wird hier sogar völlig auf die Referenzvergleiche verzichtet? José Paumard hat darauf eine Antwort: predictive branching.

Die CPU macht Vorhersagen, welcher Zweig durchlaufen werden wird und kann so die Pipeline besser füllen. Stellt sich heraus, dass die Vorhersage falsch war (der Referenzvergleich muss ja in jedem Fall ausgeführt werden), muss zurückgesetzt werden - was im Verhältnis massig Zeit kostet. In Summe macht dies den Versuch der Optimierung über den Referenzvergleich oft zunichte.
Das find ich interessant - und es ist ein Klasse Beispiel, warum Mikro-Optimierungen selten sinnvoll sind. Das der Code ohne Referenzvergleich schneller ist, ist nicht offensichtlich - zeigt aber, dass bei den ganzen Mechanismen die passieren, bis wirklich Code auf der CPU ausgeführt wird, bereits so viel "Magie" drin ist, dass man mit Mikro-Optimierungen leicht das Gegenteil erreicht. Bzw. man solche Sachen wirklich genau messen muss. (Und selbst dann ist nicht klar, ob das bei der nächsten CPU-Generation, nächsten Java-Version etc. immer noch gilt)
 
Interessant ist tatsächlich der Unterschied zwischen:
Java:
public boolean equals(Object obj) {
if (!(obj instanceof A)) return false;
    A other = (A) obj;
    return this.x == other.x
            && this.y == other.y;
}
mit:
Code:
 0: aload_1
 1: instanceof    #7
 4: ifne          9
 7: iconst_0
 8: ireturn
 9: aload_1
10: checkcast     #7
13: astore_2
14: aload_0
15: getfield      #9
18: aload_2
19: getfield      #9
22: if_icmpne     40
25: aload_0
26: getfield      #13
29: aload_2
30: getfield      #13
33: if_icmpne     40
36: iconst_1
37: goto          41
40: iconst_0
41: ireturn
und:
Java:
public boolean equals(Object obj) {
  return (obj instanceof B other)
            && this.x == other.x
            && this.y == other.y;
}
mit:
Code:
 0: aload_1
 1: instanceof    #7
 4: ifeq          38 <---- Unterschied hier!
 7: aload_1
 8: checkcast     #7
11: astore_2
12: aload_0
13: getfield      #9
16: aload_2
17: getfield      #9
20: if_icmpne     38
23: aload_0
24: getfield      #13
27: aload_2
28: getfield      #13
31: if_icmpne     38
34: iconst_1
35: goto          39
38: iconst_0
39: ireturn
Macht "scheinbar" exakt dasselbe, also selbe Anzahl von instanceof Aufrufen und checkcast Aufrufen.
Nur, der Vergleich beim instanceof ist umgedreht. Im A-Fall (traditionell) wird im "true"-Fall ein klein bisschen weiter gesprungen und im "false"-Fall nicht gesprungen.
Im B-Fall (instanceof pattern matching) wird im "true"-Fall nicht gesprungen, und im "false"-Fall sehr weit gesprungen.
Beide Fälle haben (zumindest im JVM Bytecode) exakt gleich viele Vergleiche, instanceof und checkcasts.

Prinzipiell würde ich sagen, ist das "instanceof Pattern Matching" nur syntaktischer Zucker, der aber zu leicht anderen bedingten Sprüngen im generierten JVM Bytecode führt. Also, "HotSpot" wird beim Laden der .class Datei bzw. der Methodendefinition nicht mehr wissen, ob das nun aus einem "instanceof Pattern Matching" kam, oder dieser JVM Bytecode anderweitig generiert wurde. Eine Verbesserung scheint es aber ja dennoch zu sein. 🙂
 
Zuletzt bearbeitet:
Na, wenn du schon mit so einem generell interessanten Thema kommst, dann war das doch das Naheliegendste, oder nicht? 😀
Eigentlich schon, aber das hatte ich jetzt gar nicht auf dem Schirm. Ich habe JMH bemüht und wenn ich mir den Bytecode ansehe, dann frage ich mich allerdings auch, wo der Unterschied herkommt.

Für den Fall, dass es jemanden interessiert:

 
Dann wäre ja dein JMH Setup eigentlich der interessante Part. 🙂
Bei mir kam nicht wirklich ein Unterschied zwischen den beiden Versionen heraus.
 
Erstmal Dank an @httpdigest, der den Spaß nochmal überprüft hat. Dann bitte ich um Asche auf mein Haupt: da sitzt man den halben Tag vor dem Code und übersieht die ganze Zeit einen besch... Copy & Paste Fehler (bei der Implementierung mit pattern matching per instanceOf auf die andere Klasse getestet, womit der eigentlich Vergleich nie ausgeführt wurde).

Jetzt sehen die Zahlen gleich ganz anders aus, z. B.
Code:
Benchmark       (mode)   Mode  Cnt       Score       Error  Units
Main.benchmark     old  thrpt   30  221621,799 ± 5688,223  ops/s
Main.benchmark     new  thrpt   30  224580,516 ± 4933,007  ops/s
Also praktisch kein Unterschied.

Der Knaller aber ist, dass der Spaß auch noch schneller ist (wobei das auch davon abhängt, womit man vergleicht):
Das ist somit schlichtweg falsch. @KonradN kannst Du in den Beitrag einen entsprechenden Hinweis aufnehmen, ggf. mit Link auf diesen Kommentar?
 
Kleine Randbemerkung: Man sollte sich gut überlegen, ob man instanceof nutzen oder Class-Objekte vergleichen möchte. Mit Class-Objekten hat man kein Problem der Symmetrie. "Leider" kann man bei other.getClass() == Me.class aber dann nicht mehr die schönen Pattern-Variablen verwenden.
 
Das ist natürlich richtig, wobei die Symmetrie sowieso hinüber ist, wenn z. B. Proxies im Spiel sind. Da funktionieren die Vergleiche der Class-Objekte natürlich auch nicht mehr.
 

Zurück
Oben