2014-11-20 4 views
19

Ich habe Code mit einer Methodenreferenz, die zur Laufzeit kompiliert und fehlschlägt.LambdaConversionException mit Generics: JVM-Bug?

Die Ausnahme ist so:

Caused by: java.lang.invoke.LambdaConversionException: Invalid receiver type class redacted.BasicEntity; not a subtype of implementation type interface redacted.HasImagesEntity 
    at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:233) 
    at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303) 
    at java.lang.invoke.CallSite.makeSite(CallSite.java:289) 

Die Klasse ist in etwa so:

class ImageController<E extends BasicEntity & HasImagesEntity> { 
    void doTheThing(E entity) { 
     Set<String> filenames = entity.getImages().keySet().stream() 
      .map(entity::filename) 
      .collect(Collectors.toSet()); 
    } 
} 

die Ausnahme ausgelöst wird versucht Einheit :: Dateinamen zu lösen. filename() wird in HasImagesEntity deklariert. Soweit ich das beurteilen kann, erhalte ich die Ausnahme, weil das Löschen von E BasicEntity ist und die JVM nicht andere Grenzen auf E berücksichtigt. Wenn ich die Methodenreferenz als triviales Lambda umschreibe, alles ist gut. Es scheint mir wirklich faul, dass ein Konstrukt wie erwartet funktioniert und sein semantisches Äquivalent explodiert. Könnte das möglicherweise in der Spezifikation sein? Ich bemühe mich sehr, einen Weg zu finden, um im Compiler oder in der Laufzeit kein Problem zu sein, und habe mir nichts einfallen lassen.

+2

Wenn Sie Einheit :: Dateiname schreiben, ich glaube, Sie an den Dateinamen Methode der Instanz, deren beziehen Variablenname Einheit, aber sicher Sie sind also die Dateinamen Methode der durch den Strom bereitgestellt Instanzen zugreifen? – Luciano

+1

@Luciano Ich glaube, 'filename' akzeptiert alles, was in' getImages() 'ist und gibt eine' String' z. 'img -> Einheit.Dateiname (img)'. OP könnte klären. – Radiodef

+1

@Radiodef es sagt "filename() ist auf HasImagesEntity deklariert" (scheint keine Parameter zu nehmen) – Luciano

Antwort

19

Hier ist ein vereinfachtes Beispiel, das das Problem reproduziert und nur Core Java-Klassen verwendet:

public static void main(String[] argv) { 
    System.out.println(dummy("foo")); 
} 
static <T extends Serializable&CharSequence> int dummy(T value) { 
    return Optional.ofNullable(value).map(CharSequence::length).orElse(0); 
} 

Ihre Annahme ist die JRE-spezifische Implementierung der Zielmethode als MethodHandle erhält korrekt, die keine Informationen über Generika hat Arten. Daher ist das einzige, was es sieht, dass die Rohtypen nicht übereinstimmen.

Wie bei vielen generischen Konstrukten ist auf Bytecodeebene eine Typumwandlung erforderlich, die nicht im Quellcode angezeigt wird. Da LambdaMetafactory explizit eine direkte Methodenkennung erfordert, kann eine Methodenreferenz, die einen solchen Typcast kapselt, nicht als MethodHandle an das Werk übergeben werden.

Es gibt zwei Möglichkeiten, damit umzugehen.

Erste Lösung wäre die LambdaMetafactory zu ändern, um die MethodHandle zu vertrauen, wenn der Empfängertyp ein interface ist und die erforderliche Art von selbst in der erzeugten Lambda-Klasse warf einfügen, anstatt sie abzulehnen. Immerhin ist es für Parameter und Rückgabetypen bereits ähnlich.

Alternativ könnte der Compiler eine synthetische Hilfsmethode erstellen, die den Typcast und den Methodenaufruf kapselt, genau so, als ob Sie einen Lambda-Ausdruck geschrieben hätten. Dies ist keine einzigartige Situation. Wenn Sie eine Methodenreferenz zu einem varargs-Verfahren oder eine Array-Erstellung wie z. String[]::new, können sie nicht als direkte Methode behandelt ausgedrückt werden und enden in synthetischen Hilfsmethoden.

In beiden Fällen können wir das aktuelle Verhalten als Fehler betrachten. Aber offensichtlich müssen sich Compiler- und JRE-Entwickler darüber einigen, auf welche Weise sie behandelt werden sollten, bevor wir sagen können, auf welcher Seite der Fehler liegt.

14

Ich habe gerade dieses Problem in JDK9 und JDK8u45 behoben. Siehe this bug. Die Änderung wird eine Weile dauern, um in geförderte Builds zu gelangen. Dan hat mich gerade auf diese StackOverflow-Frage hingewiesen, also füge ich diese Notiz hinzu. Wenn Sie Fehler finden, senden Sie sie bitte.

Ich adressiert dies, indem der Compiler eine Brücke erstellt, wie es der Ansatz für viele Fälle von komplexen Methodenreferenzen ist. Wir untersuchen auch Spezifikationen Implikationen.

+0

Ich verwende JDK 1.8.0_40-b25 64-Bit-Server unter Windows und ich reproduzieren das Problem. – Nathan

+1

@Nathan: Bestätigt; in 'jdk1.8.0_40' ist es immer noch da. Der verknüpfte Fehlereintrag benennt 'jdk1.8.0_45b01' als (neues?) Ziel. – Holger

+3

Noch scheint es mit 1.8.0_51, [this] (http://stackoverflow.com/q/31711967/1093528) Frage scheint genau das gleiche Problem zu sein ... – fge

1

Ich fand einen Workaround für diesen Austausch der Reihenfolge der Generika. Verwenden Sie zum Beispiel class A<T extends B & C>, in dem Sie auf eine B-Methode zugreifen müssen, oder verwenden Sie class A<T extends C & B>, wenn Sie auf eine C-Methode zugreifen müssen. Wenn Sie Zugriff auf Methoden aus beiden Klassen benötigen, funktioniert das natürlich nicht. Ich fand das nützlich, wenn eine der Schnittstellen eine Markierungsschnittstelle wie Serializable war.

Wie im JDK zu beheben, die einzige Information, die ich finden konnte, waren einige Bugs auf Openjdk Bug Tracker, die in Version 9 als gelöst markiert sind, die eher wenig hilfreich ist.

+0

Das funktioniert natürlich nur, wenn sowohl "B" als auch "C" Schnittstellen sind. – Holger

8

Dieser Fehler ist nicht vollständig behoben. Ich bin gerade in eine LambdaConversionException in 1.8.0_72 gelaufen und habe gesehen, dass es offene Fehlerberichte in Oracles Fehlerverfolgungssystem gibt: link1, link2.

(Edit: Die verknüpften Bugs berichtet in JDK 9 b93 geschlossen werden)

Als einfache Abhilfe, die ich vermeiden Methode behandelt. Anstatt also

.map(entity::filename) 

ich

.map(entity -> entity.filename()) 

Hier ist der Code ist, das Problem auf Debian 3.11.8-1 x86_64 zur Wiedergabe.

import java.awt.Component; 
import java.util.Collection; 
import java.util.Collections; 

public class MethodHandleTest { 
    public static void main(String... args) { 
     new MethodHandleTest().run(); 
    } 

    private void run() { 
     ComponentWithSomeMethod myComp = new ComponentWithSomeMethod(); 
     new Caller<ComponentWithSomeMethod>().callSomeMethod(Collections.singletonList(myComp)); 
    } 

    private interface HasSomeMethod { 
     void someMethod(); 
    } 

    static class ComponentWithSomeMethod extends Component implements HasSomeMethod { 
     @Override 
     public void someMethod() { 
      System.out.println("Some method"); 
     } 
    } 

    class Caller<T extends Component & HasSomeMethod> { 
     public void callSomeMethod(Collection<T> components) { 
      components.forEach(HasSomeMethod::someMethod); // <-- crashes 
//   components.forEach(comp -> comp.someMethod());  <-- works fine 

     } 
    } 
}