Default methods are not correctly backported
pietrodev opened this issue · 8 comments
As stated in the guide:
Default methods are backported by copying the default methods to a companion class (interface name + "$") as static methods, replacing the default methods in the interface with abstract methods, and by adding the necessary method implementations to all classes which implement that interface.
If I have this interface:
public interface TestI {
default void test() {}
}
and this class:
public class TestC implements TestI {}
as far as I understand the output of retrolambda will be:
- An interface with an abstract method test
- A new class with a static method (empty in the example)
- The class TestC with the implemented test method
So, If then I have a new class:
public class Test2 {
static {
new TestC().test();
}
}
This should work. Am I right?
I'm asking this because it doesn't.
Test2.java:3: error: cannot find symbol
new TestC().test();
^
symbol: method test()
location: class TestC
If I decompile I found:
public abstract interface TestI {
public abstract void test();
}
public class TestI$ {
public static void test(TestI paramTestI) {}
}
and:
public class TestC implements TestI {}
I don't think the last one is right. I'm doing something wrong or I didn't understand the section:
by adding the necessary method implementations to all classes which implement that interface.
If I use javap I obtain
public class TestC implements TestI {
public TestC();
public void test();
}
So the method is there but as the decompiler cannot find it neither javac does.
I tried both using compatibility with bytecode 50 and 51 using the latest version.
java -Dretrolambda.defaultMethods=true -Dretrolambda.bytecodeVersion=50 -Dretrolambda.inputDir=. -Dretrolambda.classpath=. -jar retrolambda.jar
You can reproduce it by using the same source code
Of course it would be nice if my TestC has all the implementation so that I can use it in other java7 project.
If it can help, I can add the following:
Using IntelliJ as IDE, when I type "new TestC()." it autocomplete with test() method, it founds it. So if I type "new TestC().test()" it doesn't throw any error.
But when I then compile (maven or ant) it throws the same error as above.
This will instead work:
public class Test2 {
static {
TestI t = new TestC();
t.test();
}
}
I also tried to run it inside main() and it works, while this doen't:
public class Test2 {
static {
TestC t = new TestC();
t.test();
}
}
(I just changed the type of t)
So the problem is the compilation phase.
I'm unable to reproduce this. Here is the output from javap:
$ javap -p -v end-to-end-tests/target/classes/TestI.class
Classfile /C:/DEVEL/Retrolambda/retrolambda/end-to-end-tests/target/classes/TestI.class
Last modified 7.6.2015; size 112 bytes
MD5 checksum 41dda4160c62269e56d7aba1673b0e63
Compiled from "TestI.java"
public interface TestI
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
#1 = Utf8 TestI
#2 = Class #1 // TestI
#3 = Utf8 java/lang/Object
#4 = Class #3 // java/lang/Object
#5 = Utf8 TestI.java
#6 = Utf8 test
#7 = Utf8 ()V
#8 = Utf8 SourceFile
{
public abstract void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "TestI.java"
$ javap -p -v end-to-end-tests/target/classes/TestI\$.class
Classfile /C:/DEVEL/Retrolambda/retrolambda/end-to-end-tests/target/classes/TestI$.class
Last modified 7.6.2015; size 232 bytes
MD5 checksum 3f3d7421bf979cb865cf6ae303ef6586
Compiled from "TestI.java"
public class TestI$
minor version: 0
major version: 51
flags: ACC_PUBLIC
Constant pool:
#1 = Utf8 TestI$
#2 = Class #1 // TestI$
#3 = Utf8 java/lang/Object
#4 = Class #3 // java/lang/Object
#5 = Utf8 TestI.java
#6 = Utf8 test
#7 = Utf8 (LTestI;)V
#8 = Utf8 this
#9 = Utf8 LTestI;
#10 = Utf8 Code
#11 = Utf8 LocalVariableTable
#12 = Utf8 LineNumberTable
#13 = Utf8 SourceFile
{
public static void test(TestI);
descriptor: (LTestI;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this LTestI;
LineNumberTable:
line 2: 0
}
SourceFile: "TestI.java"
$ javap -p -v end-to-end-tests/target/classes/TestC.class
Classfile /C:/DEVEL/Retrolambda/retrolambda/end-to-end-tests/target/classes/TestC.class
Last modified 7.6.2015; size 326 bytes
MD5 checksum 9c1281b74a7f95fa46102e959ea50a7b
Compiled from "TestC.java"
public class TestC implements TestI
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Utf8 TestC
#2 = Class #1 // TestC
#3 = Utf8 java/lang/Object
#4 = Class #3 // java/lang/Object
#5 = Utf8 TestI
#6 = Class #5 // TestI
#7 = Utf8 TestC.java
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = NameAndType #8:#9 // "<init>":()V
#11 = Methodref #4.#10 // java/lang/Object."<init>":()V
#12 = Utf8 this
#13 = Utf8 LTestC;
#14 = Utf8 test
#15 = Utf8 TestI$
#16 = Class #15 // TestI$
#17 = Utf8 (LTestI;)V
#18 = NameAndType #14:#17 // test:(LTestI;)V
#19 = Methodref #16.#18 // TestI$.test:(LTestI;)V
#20 = Utf8 Code
#21 = Utf8 LocalVariableTable
#22 = Utf8 LineNumberTable
#23 = Utf8 SourceFile
{
public TestC();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #11 // Method java/lang/Object."<init>":()V
4: return
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LTestC;
LineNumberTable:
line 1: 0
public void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokestatic #19 // Method TestI$.test:(LTestI;)V
4: return
}
SourceFile: "TestC.java"
$ javap -p -v end-to-end-tests/target/classes/Test2.class
Classfile /C:/DEVEL/Retrolambda/retrolambda/end-to-end-tests/target/classes/Test2.class
Last modified 7.6.2015; size 337 bytes
MD5 checksum e04c6d30bd057a3141d0b4f79144218f
Compiled from "Test2.java"
public class Test2
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Utf8 Test2
#2 = Class #1 // Test2
#3 = Utf8 java/lang/Object
#4 = Class #3 // java/lang/Object
#5 = Utf8 Test2.java
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = NameAndType #6:#7 // "<init>":()V
#9 = Methodref #4.#8 // java/lang/Object."<init>":()V
#10 = Utf8 this
#11 = Utf8 LTest2;
#12 = Utf8 <clinit>
#13 = Utf8 TestC
#14 = Class #13 // TestC
#15 = Methodref #14.#8 // TestC."<init>":()V
#16 = Utf8 test
#17 = NameAndType #16:#7 // test:()V
#18 = Methodref #14.#17 // TestC.test:()V
#19 = Utf8 Code
#20 = Utf8 LocalVariableTable
#21 = Utf8 LineNumberTable
#22 = Utf8 SourceFile
{
public Test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #9 // Method java/lang/Object."<init>":()V
4: return
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LTest2;
LineNumberTable:
line 1: 0
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #14 // class TestC
3: dup
4: invokespecial #15 // Method TestC."<init>":()V
7: invokevirtual #18 // Method TestC.test:()V
10: return
LineNumberTable:
line 3: 0
line 4: 10
}
SourceFile: "Test2.java"
Based on your report of Test2.java:3: error: cannot find symbol
, it seems to me that you are calling the Java compiler after running Retrolambda. The way that Retrolambda backports default methods is incompatible with incremental compiling. You must always make a clean build (e.g. mvn clean verify
) or you will end up with weird compile errors.
I presume that this is the case here, so I'm closing this issue.
Hi, thanks for your time. I'm going to try to explain better what I want to achieve and what I understood for what the retrolambda incremental compiling limitation is.
First, I'd like to do the following, which I supposed I could do:
- I have a library project written in Java 8 with interfaces with default methods
- I compile this project with retrolambda and obtain a Java 7 compatible jar
- I want to use this library in another Java 7 project
All went well until I invoke, as the opened issue, a method on an object declared as itself when this method is declared as default in interface and it is not overridden. Maybe I was wrong but I supposed that the backport task would put the missing implementations in all the classes without the method. But it does not work.
But, it does work whenever:
- I declare the object as the interface (it finds the method)
- The default method is overridden (it obviously finds the method)
So the method is there.
For incremental compiling limitation I understood that, if I want to create or modify a class that implements the backported interface this does not work because it cannot backport the default method again in the new class. This sounds right and I can live with that. You already backported missing methods so I understand you cannot backport them twice. What made me open the issue is that the compiler output changes depending on how I declare the object (interface or itself)
I do not know the implementation details but is it possible to have the backported classes work if I just use it without adding any functionality (e.g., new overriding classes)?
I'd like the backported concrete classes to have the implementation of the default methods so I can invoke them when using the project as library of another Java 7 project. Is it possible?
Finally, now I'm using a simple workaround that is to implement the default methods in the concrete classes (point 2 of exceptions). Doing so it works.
Am I wrong or does it make any sense to you?
Thanks
Ok, so it's some issue to do with using the methods from a different module. I'll look into it. There are a bunch of corner cases in the Java language there.
It might have to do with Retrolambda marking the method in TestC as synthetic. I'll need to check the language spec to see whether the Java compiler disallows such calls...
This is fixed in Retrolambda 2.0.4