Objekt durch var ersetzen: Was könnte schief gehen?

Ich bin kürzlich auf eine Situation gestoßen, in der das Ersetzen von Object durch var in einem Java 10-Programm zur Laufzeit eine Ausnahme auslöst. Ich interessierte mich dafür, auf wie viele verschiedene Arten dieser Effekt erzielt werden kann, und wandte mich an die Community:



Es stellte sich heraus, dass Sie den Effekt auf verschiedene Arten erzielen können. Obwohl alle etwas kompliziert sind, ist es interessant, sich am Beispiel einer solchen Aufgabe an die verschiedenen Feinheiten der Sprache zu erinnern. Mal sehen, welche Methoden gefunden wurden.


Mitglieder


Unter den Befragten waren viele berühmte und nicht sehr Menschen. Dies ist Sergey Bsideup Egorov, Pivotal-Mitarbeiter, Redner, einer der Schöpfer von Testcontainern. Dies ist Victor Polishchuk , berühmt für Berichte über das blutige Unternehmen. Auch Nikita Artyushov von Google bemerkt; Dmitry Mikhailov und Maccimo . Besonders gefreut hat mich aber die Ankunft von Wouter Coekaerts . Er ist bekannt für seinen Artikel im letzten Jahr , in dem er durch das Java-Typensystem ging und erzählte, wie hoffnungslos es kaputt war. Einige dieser Artikel haben jbaruch und ich sogar in der vierten Version von Java Puzzlers verwendet .


Aufgabe und Lösungen


Das Wesentliche unserer Aufgabe ist also: Es gibt ein Java-Programm, in dem eine Variable der Form Object x = ... java.lang.Object ist (ehrlicher Standard java.lang.Object , keine Typersetzungen). Das Programm kompiliert, führt und druckt so etwas wie "Ok". Wir ersetzen Object durch var und erfordern eine automatische Typinferenz. Danach wird das Programm weiter kompiliert, stürzt jedoch beim Start mit einer Ausnahme ab.


Lösungen können grob in zwei Gruppen unterteilt werden. Im ersten Fall wird die Variable nach dem Ersetzen durch var primitiv (dh sie war ursprünglich Autoboxing). Der zweite Typ bleibt Objekt, aber spezifischer als Object . Hier können Sie eine interessante Untergruppe hervorheben, die Generika verwendet.


Boxen


Wie kann man ein Objekt von einem Grundelement unterscheiden? Es gibt viele verschiedene Möglichkeiten. Am einfachsten ist es, die Identität zu überprüfen. Diese Lösung wurde von Nikita vorgeschlagen:


 Object x = 1000; if (x == new Integer(1000)) throw new Error(); System.out.println("Ok"); 

Wenn x ein Objekt ist, kann es unter Bezugnahme auf das neue Objekt new Integer(1000) sicherlich nicht gleich sein. Und wenn es sich um ein Grundelement handelt, entfaltet sich nach den Regeln der Sprache die new Integer(1000) sofort auch zu einem Grundelement, und Zahlen werden als Grundelemente verglichen.


Ein anderer Weg sind überladene Methoden. Sie können Ihre eigenen schreiben, aber Sergey hat eine elegantere Option gefunden: Verwenden Sie die Standardbibliothek. Die List.remove Methode ist List.remove , überladen und kann entweder ein Element nach Index entfernen, wenn ein Grundelement übergeben wird, oder ein Element nach Wert, wenn ein Objekt übergeben wird. Dies hat wiederholt zu Fehlern in realen Programmen geführt, wenn Sie List<Integer> . Für unsere Aufgabe könnte die Lösung folgendermaßen aussehen:


 Object x = 1000; List<?> list = new ArrayList<>(); list.remove(x); System.out.println("Ok"); 

Jetzt versuchen wir, das nicht vorhandene Element 1000 aus der leeren Liste zu entfernen. Dies ist nur eine nutzlose Aktion. Wenn wir Object durch var ersetzen, rufen wir eine andere Methode auf, mit der das Element mit dem Index 1000 entfernt wird. Dies führt bereits zu IndexOutOfBoundsException .


Die dritte Methode ist der Typkonvertierungsoperator. Wir können ein anderes Grundelement erfolgreich in einen Grundelementtyp konvertieren, aber ein Objekt wird nur konvertiert, wenn es einen Wrapper über denselben Typ gibt, in den wir konvertieren werden (dann wird Anboxing stattfinden). Eigentlich brauchen wir den gegenteiligen Effekt: Eine Ausnahme ist im Fall eines Grundelements und nicht im Fall eines Objekts, aber mit Try-Catch ist dies leicht zu erreichen, was Viktor verwendet hat:


 Object x = 40; try { throw new Error("Oops :" + (char)x); } catch (ClassCastException e) { System.out.println("Ok"); } 

Hier ist ClassCastException das erwartete Verhalten, dann wird das Programm normal beendet. Aber nach der Verwendung von var verschwindet diese Ausnahme und wir werfen etwas anderes. Ich frage mich, ob dies vom echten Code des blutigen Unternehmens inspiriert ist.


Eine andere Option zur Typkonvertierung wurde von Wouter vorgeschlagen. Sie können die seltsame Logik des Operators verwenden ?: . Der Code liefert zwar nur unterschiedliche Ergebnisse, daher müssen Sie ihn irgendwie ändern, damit es eine Ausnahme gibt. So scheint es mir ganz elegant:


 Object x = 1.0; System.out.println(String.valueOf(false ? x : 100000000000L).substring(12) + "Ok"); 

Der Unterschied zwischen dieser Methode besteht darin, dass wir den Wert von x direkt verwenden, sondern dass der Typ x den Typ des Ausdrucks false ? x : 100000000000L false ? x : 100000000000L . Wenn x ein Object , dann ist der Typ des gesamten Ausdrucks Object , und dann haben wir nur Boxing. String.valueOf() gibt String.valueOf() Zeichenfolge von 100000000000 , für die substring(12) eine leere Zeichenfolge ist. Wenn Sie var , wird der Typ x double und daher der Typ false ? x : 100000000000L false ? x : 100000000000L ebenfalls double , 1.0E11 100000000000L wird zu 1.0E11 , wo es viel weniger als 12 Zeichen sind. Das Aufrufen von substring führt also zu einer StringIndexOutOfBoundsException .


Schließlich nutzen wir die Tatsache, dass eine Variable nach der Erstellung tatsächlich geändert werden kann. Und in der Objektvariablen können Sie im Gegensatz zum Grundelement null setzen. Das Einfügen von null in eine Variable ist einfach, es gibt viele Möglichkeiten. Aber auch hier verfolgte Wouter einen kreativen Ansatz mit der lächerlichen Integer.getInteger Methode:


 Object x = 1; x = Integer.getInteger("moo"); System.out.println("Ok"); 

Nicht jeder weiß, dass diese Methode eine Systemeigenschaft namens moo liest und, falls vorhanden, versucht, sie in eine Zahl umzuwandeln, andernfalls wird null . Wenn keine Eigenschaft vorhanden ist, weisen wir dem Objekt stillschweigend null zu, fallen jedoch von NullPointerException wenn versucht wird, es einem Grundelement zuzuweisen (dort erfolgt ein automatisches Anboxing). Es hätte natürlich einfacher sein können. Grobe Version x = null; Es wird nicht gecrawlt - es wird nicht kompiliert, aber der Compiler wird es jetzt verschlucken:


 Object x = 1; x = (Integer)null; System.out.println("Ok"); 

Objekttyp


Angenommen, Sie können nicht mehr mit Grundelementen spielen. Was können Sie noch denken?


Nun, erstens die einfachste von Dmitry vorgeschlagene Methode zur Überladung von Methoden:


 public static void main(String[] args) { Object x = "Ok"; sayWhat(x); } static void sayWhat(Object x) { System.out.println(x); } static void sayWhat(String x) { throw new Error(); } 

Die Verknüpfung überladener Methoden in Java erfolgt statisch in der Kompilierungsphase. Die sayWhat sayWhat(Object) -Methode wird sayWhat(Object) Wenn wir jedoch automatisch auf den Typ x schließen, wird der String sayWhat(String) , und daher wird die spezifischere sayWhat(String) -Methode verknüpft.


Eine andere Möglichkeit, einen mehrdeutigen Aufruf in Java durchzuführen, ist die Verwendung variabler Argumente (varargs). Wouter erinnerte sich noch einmal daran:


 Object x = new Object[] {}; Arrays.asList(x).get(0); System.out.println("Ok"); 

Wenn der Variablentyp Object , hält der Compiler es für ein Variablenargument und umschließt das Array mit einem anderen Array eines Elements, sodass get() erfolgreich erfüllt wird. Wenn Sie var , wird der Typ Object[] angezeigt, und es erfolgt keine zusätzliche Umhüllung. Auf diese Weise erhalten wir eine leere Liste und der Aufruf von get() schlägt fehl.


Maccimo entschied sich für Hardcore: Er beschloss, println über die MethodHandle-API aufzurufen:


 Object x = "Ok"; MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual( PrintStream.class, "println", MethodType.methodType(void.class, Object.class)); mh.invokeExact(System.out, x); 

Die invokeExact Methode und mehrere andere Methoden aus dem java.lang.invoke haben die sogenannte "polymorphe Signatur". Obwohl es als die übliche vararg invokeExact(Object... args) Methode invokeExact(Object... args) deklariert ist, tritt es beim Standard-Array-Packen nicht auf. Stattdessen wird im Bytecode eine Signatur generiert, die den tatsächlich übergebenen Argumenttypen entspricht. Die invokeExact Methode wurde für den superschnellen Aufruf von Methodenhandles entwickelt, sodass keine Standardargumenttransformationen wie Casting oder Boxing ausgeführt werden. Es wird erwartet, dass der Typ der Handle-Methode genau mit der Aufrufsignatur übereinstimmt. Dies wird zur Laufzeit überprüft, und da im Fall von var Übereinstimmung unterbrochen ist, erhalten wir eine WrongMethodTypeException .


Generika


Natürlich kann die Parametrisierung von Typen jeder Aufgabe in Java ein Funkeln verleihen. Dmitry brachte eine Lösung mit, die dem Code ähnelte, auf den ich ursprünglich gestoßen war. Dmitrys Entscheidung ist ausführlich, also werde ich zeigen:


 public static void main(String[] args) { Object x = foo(new StringBuilder()); System.out.println(x); } static <T> T foo(T x) { return (T)"Ok"; } 

Typ T wird als StringBuilder ausgegeben, aber in diesem Code muss der Compiler keine Typprüfung am Dial-Peer in den Bytecode einfügen. Es reicht ihm, dass StringBuilder Object zugewiesen werden kann, was bedeutet, dass alles in Ordnung ist. Niemand ist dagegen, dass die Methode mit dem Rückgabewert StringBuilder die Zeichenfolge tatsächlich zurückgegeben hat, wenn Sie das Ergebnis ohnehin einer Variablen vom Typ Object zugewiesen haben. Der Compiler warnt ehrlich, dass Sie eine ungeprüfte Besetzung haben, was bedeutet, dass er seine Hände wäscht. Wenn Sie x durch var ersetzen var Typ x jedoch auch als StringBuilder angezeigt. Ohne Typprüfung ist dies nicht mehr möglich, da das Zuweisen von etwas anderem zur StringBuilder Typvariablen wertlos ist. Infolgedessen stürzt var Programm nach dem Wechsel zu var sicher mit einer ClassCastException .


Wouter schlug eine Variante dieser Lösung mit Standardmethoden vor:


 Object o = ((List<String>)(List)List.of(1)).get(0); System.out.println("Ok"); 

Zum Schluss noch eine Option von Wouter:


 Object x = ""; TreeSet<?> set = Stream.of(x) .collect(toCollection(() -> new TreeSet<>((a, b) -> 0))); if (set.contains(1)) { System.out.println("Ok"); } 

Hier wird der Stream-Typ je nach Verwendung von var oder Object entweder als Stream<Object> oder als Stream<String> angezeigt. Dementsprechend werden der TreeSet Typ und der Komparator-Lambda-Typ angezeigt. Im Fall von var müssen Zeichenfolgen zum Lambda kommen. Wenn also eine Lambda-Laufzeitdarstellung generiert wird, wird automatisch eine ClassCastException eingefügt, die eine ClassCastException wenn versucht wird, eine Einheit in eine Zeichenfolge ClassCastException .


Im Allgemeinen war das Ergebnis sehr langweilig. Wenn Sie grundlegend andere Methoden finden können, um var zu brechen, schreiben Sie in die Kommentare.

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


All Articles