Probieren Sie den verbesserten instanceof-Operator in Java 14 aus

Nicht weit entfernt ist die neue, 14. Version von Java, was bedeutet, dass es Zeit ist zu sehen, welche neuen Syntaxfunktionen diese Version von Java enthalten wird. Eine dieser syntaktischen Möglichkeiten ist der Mustervergleich des Typs , der unter Verwendung der verbesserten (erweiterten) instanceof Operators implementiert wird.

Heute möchte ich mit diesem neuen Operator herumspielen und die Funktionen seiner Arbeit detaillierter betrachten. Da die Mustererkennung nach Typ noch nicht im Haupt-JDK-Repository vorhanden ist, musste ich das Repository des Amber-Projekts herunterladen, das neue Java-Syntaxkonstrukte entwickelt, und das JDK aus diesem Repository kompilieren .

Als erstes überprüfen wir die Java-Version, um sicherzustellen, dass wir wirklich JDK 14 verwenden:

 > java -version openjdk version "14-internal" 2020-03-17 OpenJDK Runtime Environment (build 14-internal+0-adhoc.osboxes.amber-amber) OpenJDK 64-Bit Server VM (build 14-internal+0-adhoc.osboxes.amber-amber, mixed mode, sharing) 

Alles ist richtig.

Jetzt schreiben wir ein kleines Stück Code mit der "alten" instanceof Operators und führen es aus:

 public class A { public static void main(String[] args) { new A().f("Hello, world!"); } public void f(Object obj) { if (obj instanceof String) { String str = (String) obj; System.out.println(str.toLowerCase()); } } } 

 > java A.java hello, world! 

Es funktioniert Dies ist eine Standardtypprüfung, gefolgt von einer Besetzung. Wir schreiben jeden Tag ähnliche Konstruktionen, egal welche Java-Version wir verwenden, mindestens 1.0, mindestens 13.
Aber jetzt haben wir Java 14 in unseren Händen und schreiben den Code mit der verbesserten instanceof Operators neu (ich werde in Zukunft darauf verzichten, Codezeilen zu wiederholen):

 if (obj instanceof String str) { System.out.println(str.toLowerCase()); } 

 > java --enable-preview --source 14 A.java hello, world! 

Großartig. Der Code ist sauberer, kürzer, sicherer und lesbarer. Es gab drei Wiederholungen des Wortes String, eine wurde. Beachten Sie, dass wir nicht vergessen haben, die Argumente --enable-preview --source 14 , as anzugeben Der neue Operator ist eine Vorschaufunktion . Darüber hinaus hat ein aufmerksamer Leser wahrscheinlich bemerkt, dass wir die A.java-Quelldatei direkt ohne Kompilierung ausgeführt haben. Diese Funktion erschien in Java 11.

Versuchen wir, etwas komplexeres zu schreiben und fügen eine zweite Bedingung hinzu, die die gerade deklarierte Variable verwendet:

 if (obj instanceof String str && str.length() > 5) { System.out.println(str.toLowerCase()); } 

Es kompiliert und funktioniert. Aber was ist, wenn Sie die Bedingungen tauschen?

 if (str.length() > 5 && obj instanceof String str) { System.out.println(str.toLowerCase()); } 

 A.java:7: error: cannot find symbol if (str.length() > 5 && obj instanceof String str) { ^ 

Kompilierungsfehler. Was zu erwarten ist: Die Variable str wurde noch nicht deklariert, kann also nicht verwendet werden.

Was ist übrigens mit der Veränderlichkeit? Ist die Variable final oder nicht? Wir versuchen:

 if (obj instanceof String str) { str = "World, hello!"; System.out.println(str.toLowerCase()); } 

 A.java:8: error: pattern binding str may not be assigned str = "World, hello!"; ^ 

Ja, die letzte Variable. Dies bedeutet, dass das Wort „Variable“ hier nicht ganz richtig ist. Und der Compiler verwendet den Spezialbegriff "Musterbindung". Daher schlage ich von nun an vor, nicht "variabel", sondern "Musterbindung" zu sagen (das Wort "Bindung" ist leider nicht sehr gut ins Russische übersetzt).

Mit Veränderlichkeit und Terminologie aussortiert. Lass uns weiter experimentieren. Was ist, wenn es uns gelingt, den Compiler zu "brechen"?

Was ist, wenn Sie eine Variable und ein Muster mit demselben Namen benennen?

 if (obj instanceof String obj) { System.out.println(obj.toLowerCase()); } 

 A.java:7: error: variable obj is already defined in method f(Object) if (obj instanceof String obj) { ^ 

Ist logisch. Das Überlappen einer Variablen aus dem äußeren Bereich funktioniert nicht. Dies ist äquivalent dazu, als ob wir die Variable obj zweites Mal im selben Bereich obj .

Und wenn ja:

 if (obj instanceof String str && obj instanceof String str) { System.out.println(str.toLowerCase()); } 

 A.java:7: error: illegal attempt to redefine an existing match binding if (obj instanceof String str && obj instanceof String str) { ^ 

Der Compiler ist solide wie Beton.

Was können Sie noch versuchen? Lassen Sie uns mit den Bereichen herumspielen. Wenn Bindung in der if Verzweigung definiert ist, wird sie in der else Verzweigung definiert, wenn die Bedingung invertiert ist?

 if (!(obj instanceof String str)) { System.out.println("not a string"); } else { System.out.println(str.toLowerCase()); } 

Es hat funktioniert Der Compiler ist nicht nur zuverlässig, sondern auch intelligent.

Und wenn ja

 if (obj instanceof String str && true) { System.out.println(str.toLowerCase()); } 

Es hat wieder funktioniert. Der Compiler versteht richtig, dass die Bedingung auf eine einfache obj instanceof String str .

Ist es wirklich nicht möglich, den Compiler zu "brechen"?

Möglicherweise so?

 if (obj instanceof String str || false) { System.out.println(str.toLowerCase()); } 

 A.java:8: error: cannot find symbol System.out.println(str.toLowerCase()); ^ 

Ja! Das sieht schon nach einem Bug aus. Schließlich sind alle drei Bedingungen absolut gleichwertig:

  • obj instanceof String str
  • obj instanceof String str && true
  • obj instanceof String str || false

Flow-Scoping-Regeln sind dagegen eher nicht trivial , und vielleicht sollte ein solcher Fall wirklich nicht funktionieren. Aber wenn Sie rein aus menschlicher Sicht schauen, dann denke ich, dass dies ein Fehler ist.

Aber komm, lass uns etwas anderes versuchen. Funktioniert das:

 if (!(obj instanceof String str)) { throw new RuntimeException(); } System.out.println(str.toLowerCase()); 

Kompiliert. Das ist gut, da dieser Code dem Folgenden entspricht:
 if (!(obj instanceof String str)) { throw new RuntimeException(); } else { System.out.println(str.toLowerCase()); } 

Und da beide Optionen gleichwertig sind, erwartet der Programmierer, dass sie auf die gleiche Weise funktionieren.

Was ist mit überlappenden Feldern?

 public class A { private String str; public void f(Object obj) { if (obj instanceof String str) { System.out.println(str.toLowerCase()); } else { System.out.println(str.toLowerCase()); } } } 

Der Compiler hat nicht geschworen. Dies ist logisch, da lokale Variablen immer Felder überlappen können. Anscheinend haben sie auch beschlossen, keine Ausnahmen für Musterbindungen zu machen. Andererseits ist ein solcher Code ziemlich zerbrechlich. Ein unvorsichtiger Schritt, und Sie werden möglicherweise nicht bemerken, wie Ihr if Zweig gebrochen ist:

 private boolean isOK() { return false; } public void f(Object obj) { if (obj instanceof String str || isOK()) { System.out.println(str.toLowerCase()); } else { System.out.println(str.toLowerCase()); } } 

Beide Zweige verwenden jetzt das Feld str , das ein unaufmerksamer Programmierer möglicherweise nicht erwartet. Um solche Fehler so früh wie möglich zu erkennen, verwenden Sie die Überprüfungen in der IDE und unterschiedliche Syntaxhervorhebungen für Felder und Variablen. Ich empfehle außerdem, dass Sie this Qualifikationsmerkmal immer für Felder verwenden. Dies erhöht die Zuverlässigkeit.

Was ist noch interessant? Wie die "alte" instanceof , stimmt die neue nie mit null überein. Dies bedeutet, dass Sie sich immer darauf verlassen können, dass Musterbinder niemals null :

 if (obj instanceof String str) { System.out.println(str.toLowerCase()); //    NullPointerException } 

Übrigens können Sie mit dieser Eigenschaft solche Ketten verkürzen:

 if (a != null) { B b = a.getB(); if (b != null) { C c = b.getC(); if (c != null) { System.out.println(c.getSize()); } } } 

Wenn Sie instanceof , kann der obige Code folgendermaßen umgeschrieben werden:

 if (a != null && a.getB() instanceof B b && b.getC() instanceof C c) { System.out.println(c.getSize()); } 

Schreiben Sie in die Kommentare, was Sie über diesen Stil denken. Würden Sie diese Redewendung verwenden?

Was ist mit Generika?

 import java.util.List; public class A { public static void main(String[] args) { new A().f(List.of(1, 2, 3)); } public void f(Object obj) { if (obj instanceof List<Integer> list) { System.out.println(list.size()); } } } 

 > java --enable-preview --source 14 A.java Note: A.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. 3 

Sehr interessant. Wenn die "alte" instanceof nur " instanceof List oder " instanceof List<?> " Unterstützt, funktioniert die neue mit einem bestimmten Typ. Wir warten darauf, dass die erste Person in eine solche Falle gerät:

 if (obj instanceof List<Integer> list) { System.out.println("Int list of size " + list.size()); } else if (obj instanceof List<String> list) { System.out.println("String list of size " + list.size()); } 

Warum funktioniert das nicht?
Antwort: Mangel an reifizierten Generika in Java.

IMHO, das ist ein ziemlich ernstes Problem. Andererseits weiß ich nicht, wie ich das beheben soll. Es sieht so aus, als müssten Sie sich wieder auf Inspektionen in der IDE verlassen.

Schlussfolgerungen


Im Allgemeinen funktioniert der neue Pattern-Matching-Typ sehr gut. Mit der verbesserten instanceof Operators können Sie nicht nur eine Typprüfung durchführen, sondern auch fertige Bindemittel dieses Typs deklarieren, sodass kein manuelles Gießen erforderlich ist. Dies bedeutet, dass der Code weniger Rauschen enthält und der Leser nützliche Logik leichter erkennen kann. Beispielsweise können die meisten equals() -Implementierungen in einer Zeile geschrieben werden:

 public class Point { private final int x, y; … @Override public int hashCode() { return Objects.hash(x, y); } @Override public boolean equals(Object obj) { return obj instanceof Point p && px == this.x && py == this.y; } } 

Der obige Code kann noch kürzer geschrieben werden. Wie?
Verwenden von Einträgen , die auch in Java 14 enthalten sein werden. Wir werden beim nächsten Mal darüber sprechen.

Andererseits werfen einige kontroverse Punkte kleine Fragen auf:

  • Nicht vollständig transparente Bereichsregeln (Beispiel mit instanceof || false ).
  • Überlappende Felder.
  • instanceof und Generika.

Dies sind jedoch eher unbedeutende Anschuldigungen als ernsthafte Behauptungen. Alles in allem sind die enormen Vorteile der neuen instanceof Operators definitiv eine zusätzliche Sprache wert. Und wenn es immer noch den Vorschau-Status verlässt und zu einer stabilen Syntax wird, ist es eine gute Motivation, Java 8 endgültig der neuen Java-Version zu überlassen.

PS Ich habe einen Kanal in Telegram, in dem ich über Java-Nachrichten schreibe. Ich fordere Sie auf, es zu abonnieren.

Source: https://habr.com/ru/post/de477654/


All Articles