Überschreiben von Methoden anhand des Rückgabewerts #338

Open
opened 2024-05-26 18:16:00 +00:00 by i21023 · 8 comments
Collaborator

Hier geht es um ein Problem zu #294, das mir gerade aufgefallen ist. Der Fix für diesen Bug ist wahrscheinlich nicht optimal implementiert.

Problem 1

Die Substitution der primitiven Typen scheint unabhängig von Referenztypen immer vorgenommen zu werden. Hier müsste man vielleicht überprüfen, ob der primitive Typ zum Wrappertyp passt.

Beispiel:

import java.util.List;
import java.lang.Integer;
import java.lang.String;
import java.lang.Object;
import java.util.List;

public class Foo{
    public hashCode(){
        return List.of(42);
    }
}

Dieser Code sollte vermutlich eine Exception werfen, da als Rückgabewert der Funktion offensichtlich java.lang.List inferiert wird. Die hashCode Methode von Object gibt aber ein int zurück und die Methode sollte nicht anhand des Rückgabewerts überladen werden können.
Stattdessen kompiliert der Code folgendermaßen:

 public int hashCode();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #12                 // int 42
         2: invokestatic  #18                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: invokestatic  #24                 // InterfaceMethod java/util/List.of:(Ljava/lang/Object;)Ljava/util/List;
         8: ireturn
    Signature: #11                          // ()I
      JavaTXSignature: length = 0x2 (unknown attribute)
       00 1F

Er substituiert wegen der Lösung von #294 int für List, gibt aber weiterhin ein Listenobjekt zurück. Das wirft zur Laufzeit eine Exception.

Problem 2

Generell scheint die Substitution nicht nur bei primitiven Typen zu passieren.

Beispiel

import java.util.List;
import java.lang.Integer;
import java.lang.String;
import java.lang.Object;
import java.util.List;

public class Foo{
    public Integer toString(){
        return 42;
    }
}

In diesem Fall gibt die toString() Methode von Object ein String Object zurück. Diesen Code sollte der Typcheck wahrscheinlich direkt ablehnen. Stattdessen wird auch hier einfach String für Integer substituiert, obwohl Integer zurückgegeben wird.

  public java.lang.String toString();
    descriptor: ()Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #12                 // int 42
         2: invokestatic  #18                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: areturn
    Signature: #11                          // ()Ljava/lang/String;
      JavaTXSignature: length = 0x2 (unknown attribute)
       00 19

Problem 3

Allgemein ist es aktuell möglich, Methoden anhand des Rückgabewerts zu überladen.

Beispiel:

import java.util.List;
import java.lang.Integer;
import java.lang.String;
import java.lang.Object;
import java.util.List;

public class Foo{
    foo(){
        return "42";
    }

    foo(){
        return 42;
    }
}

Dieser Code sollte wahrscheinlich nicht kompilieren.

Es ist auch möglich die genau gleiche Methode mehrmals zu schreiben. Im Bytecode steht genau die gleiche Methode dann einfach zwei mal.
z.B.

import java.lang.String;
import java.lang.Integer;

public class Foo{
    foo(){
        return 42;
    }

    foo(){
        return 42;
    }
}

Vielleicht wäre es tatsächlich eine bessere Lösung für dieses Problems Bridge Methods zu verwenden. Das wurde ja auch in Bad Honnef von einem Teilnehmer vorgeschlagen.

Also dass der Compiler zusätzlichen Bytecode generiert, der den Methodenaufruf mit dem primitiven Typen in der Signatur auf die Methode mit der Wrapperklasse in der Signatur weiterleitet.

z.B.

public class Foo{
    java.lang.Integer hashCode(){
       return ...
   }

   //Vom Compiler automatisch generierter Code (Bridge Methode)
   int hashCode(){
       return this.hashCode(); //Im Bytecode müsste für diesen Aufruf ja eh der Rückgabewert angegeben werden, sodass der Aufruf eindeutig sein sollte 
    }
}
Hier geht es um ein Problem zu #294, das mir gerade aufgefallen ist. Der Fix für diesen Bug ist wahrscheinlich nicht optimal implementiert. ### Problem 1 Die Substitution der primitiven Typen scheint unabhängig von Referenztypen immer vorgenommen zu werden. Hier müsste man vielleicht überprüfen, ob der primitive Typ zum Wrappertyp passt. Beispiel: ```java import java.util.List; import java.lang.Integer; import java.lang.String; import java.lang.Object; import java.util.List; public class Foo{ public hashCode(){ return List.of(42); } } ``` Dieser Code sollte vermutlich eine Exception werfen, da als Rückgabewert der Funktion offensichtlich java.lang.List inferiert wird. Die hashCode Methode von Object gibt aber ein int zurück und die Methode sollte nicht anhand des Rückgabewerts überladen werden können. Stattdessen kompiliert der Code folgendermaßen: ``` public int hashCode(); descriptor: ()I flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: ldc #12 // int 42 2: invokestatic #18 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: invokestatic #24 // InterfaceMethod java/util/List.of:(Ljava/lang/Object;)Ljava/util/List; 8: ireturn Signature: #11 // ()I JavaTXSignature: length = 0x2 (unknown attribute) 00 1F ``` Er substituiert wegen der Lösung von #294 int für List, gibt aber weiterhin ein Listenobjekt zurück. Das wirft zur Laufzeit eine Exception. ### Problem 2 Generell scheint die Substitution nicht nur bei primitiven Typen zu passieren. Beispiel ```java import java.util.List; import java.lang.Integer; import java.lang.String; import java.lang.Object; import java.util.List; public class Foo{ public Integer toString(){ return 42; } } ``` In diesem Fall gibt die `toString()` Methode von Object ein String Object zurück. Diesen Code sollte der Typcheck wahrscheinlich direkt ablehnen. Stattdessen wird auch hier einfach String für Integer substituiert, obwohl Integer zurückgegeben wird. ``` public java.lang.String toString(); descriptor: ()Ljava/lang/String; flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: ldc #12 // int 42 2: invokestatic #18 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: areturn Signature: #11 // ()Ljava/lang/String; JavaTXSignature: length = 0x2 (unknown attribute) 00 19 ``` ### Problem 3 Allgemein ist es aktuell möglich, Methoden anhand des Rückgabewerts zu überladen. Beispiel: ```java import java.util.List; import java.lang.Integer; import java.lang.String; import java.lang.Object; import java.util.List; public class Foo{ foo(){ return "42"; } foo(){ return 42; } } ``` Dieser Code sollte wahrscheinlich nicht kompilieren. Es ist auch möglich die genau gleiche Methode mehrmals zu schreiben. Im Bytecode steht genau die gleiche Methode dann einfach zwei mal. z.B. ```java import java.lang.String; import java.lang.Integer; public class Foo{ foo(){ return 42; } foo(){ return 42; } } ``` --- Vielleicht wäre es tatsächlich eine bessere Lösung für dieses Problems [Bridge Methods](https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html) zu verwenden. Das wurde ja auch in Bad Honnef von einem Teilnehmer vorgeschlagen. Also dass der Compiler zusätzlichen Bytecode generiert, der den Methodenaufruf mit dem primitiven Typen in der Signatur auf die Methode mit der Wrapperklasse in der Signatur weiterleitet. z.B. ```java public class Foo{ java.lang.Integer hashCode(){ return ... } //Vom Compiler automatisch generierter Code (Bridge Methode) int hashCode(){ return this.hashCode(); //Im Bytecode müsste für diesen Aufruf ja eh der Rückgabewert angegeben werden, sodass der Aufruf eindeutig sein sollte } } ```
dholle was assigned by i21023 2024-05-26 18:16:14 +00:00
Owner

Ich glaube das mit den Bridge Methods ist keine gute Idee. Was passiert, wenn man versucht die Klasse von normalem Java-Code aufzurufen?

Ich glaube das mit den Bridge Methods ist keine gute Idee. Was passiert, wenn man versucht die Klasse von normalem Java-Code aufzurufen?
Author
Collaborator

Meiner Meinung nach sollte nicht viel passieren. Es wird halt eine von beiden Methoden aufgerufen, wahrscheinlich die erste im Bytecode. Ist im dem Fall ja eigentlich auch egal welche aufgerufen wird, oder?

https://github.com/vijayjogi/shshankar1/blob/master/Java%20Generics%20and%20Collections.pdf
Wenn du dir hier mal Abschnitt 3.8 anschaust, macht Java ja prinzipiell das Selbe nur für ein anderes Problem

Meiner Meinung nach sollte nicht viel passieren. Es wird halt eine von beiden Methoden aufgerufen, wahrscheinlich die erste im Bytecode. Ist im dem Fall ja eigentlich auch egal welche aufgerufen wird, oder? https://github.com/vijayjogi/shshankar1/blob/master/Java%20Generics%20and%20Collections.pdf Wenn du dir hier mal Abschnitt 3.8 anschaust, macht Java ja prinzipiell das Selbe nur für ein anderes Problem
Owner

Ich muss das mal ausprobieren. Bin mir tatsächlich nicht sicher was da passiert.

Ich muss das mal ausprobieren. Bin mir tatsächlich nicht sicher was da passiert.
Owner

Also ich habe es jetzt ausprobiert und es entsteht folgender Fehler wenn man versucht die Methode hashCode aufzurufen:

Java.java:4: Fehler: Referenz zu hashCode ist mehrdeutig
        return test.hashCode();
                   ^
  Sowohl Methode hashCode() in Test als auch Methode hashCode() in Test stimmen überein
1 Fehler

Das überschreiben mit einer Methode mit anderer Signatur scheint also was anderes zu sein, in dem Fall existiert aber auch nur eine Methode innerhalb der Klasse.

Also ich habe es jetzt ausprobiert und es entsteht folgender Fehler wenn man versucht die Methode hashCode aufzurufen: ``` Java.java:4: Fehler: Referenz zu hashCode ist mehrdeutig return test.hashCode(); ^ Sowohl Methode hashCode() in Test als auch Methode hashCode() in Test stimmen überein 1 Fehler ``` Das überschreiben mit einer Methode mit anderer Signatur scheint also was anderes zu sein, in dem Fall existiert aber auch nur eine Methode innerhalb der Klasse.
Author
Collaborator

Ich hab es auch mal ausprobiert. Es scheint wichtig zu sein, dass man für die Bridge Methode die Flags ACC_BRIDGE und ACC_SYNTHETIC setzt. Dann geht es aber wie erwartet.

Ich lasse diesen Bytecode generieren:

public class Foo {
    public Foo() {
    }

    public Integer hashCode() {
        return 42;
    }

   public int hashCode(){
       //Hier sollte man natürlich die andere Methode aufrufen, ich hab das aber mal so gemacht dass man sieht welche Methode aufgerufen wird
       return 24;
   }
}

Und aufrufen kann ich es dann wie erwartet:

class Call{
    public static void main(String[] args) {
        Foo foo = new Foo();
        System.out.println(foo.hashCode());
        //Output: 42

        Object obj = new Foo();
        System.out.println(obj.hashCode());
        //Output: 24
    }
}

Ich hab das ganze Beispielprojekt mal in Anhang gepackt

Ich hab es auch mal ausprobiert. Es scheint wichtig zu sein, dass man für die Bridge Methode die Flags ACC_BRIDGE und ACC_SYNTHETIC setzt. Dann geht es aber wie erwartet. Ich lasse diesen Bytecode generieren: ```java public class Foo { public Foo() { } public Integer hashCode() { return 42; } public int hashCode(){ //Hier sollte man natürlich die andere Methode aufrufen, ich hab das aber mal so gemacht dass man sieht welche Methode aufgerufen wird return 24; } } ``` Und aufrufen kann ich es dann wie erwartet: ```java class Call{ public static void main(String[] args) { Foo foo = new Foo(); System.out.println(foo.hashCode()); //Output: 42 Object obj = new Foo(); System.out.println(obj.hashCode()); //Output: 24 } } ``` Ich hab das ganze Beispielprojekt mal in Anhang gepackt
Owner

Interessant, hab noch nie was von den Flags gehört aber du hast recht, jetzt funktioniert es. Was sagst du dazu @pl ?

Interessant, hab noch nie was von den Flags gehört aber du hast recht, jetzt funktioniert es. Was sagst du dazu @pl ?
Owner

Hallo Julian, ich habe mir das eben von Daniel erklären lassen. Leider kann ich nicht ganz nachvollziehen welchen Vorteil Du in den Bridgemethoden siehst. Letzlich passiert doch genau das gleiche, oder welchen Vorteil siehst Du?

Hallo Julian, ich habe mir das eben von Daniel erklären lassen. Leider kann ich nicht ganz nachvollziehen welchen Vorteil Du in den Bridgemethoden siehst. Letzlich passiert doch genau das gleiche, oder welchen Vorteil siehst Du?
Author
Collaborator

Ja, das stimmt schon. Das Ergebnis ist dasselbe. Auf mich wirkt es weniger anfällig für Bugs, als die Typen im Bytecode zu substituieren. Aber keine Ahnung, ob das wirklich ein Vorteil ist

Ja, das stimmt schon. Das Ergebnis ist dasselbe. Auf mich wirkt es weniger anfällig für Bugs, als die Typen im Bytecode zu substituieren. Aber keine Ahnung, ob das wirklich ein Vorteil ist
Sign in to join this conversation.
No Milestone
No project
No Assignees
3 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: JavaTX/JavaCompilerCore#338
No description provided.