When a constructor doesn't have a explicit call of the super class constructor (or this()
) then the compiler inserts super()
.
What would happen if this call was removed from the class file (after compilation)?
When a constructor doesn't have a explicit call of the super class constructor (or this()
) then the compiler inserts super()
.
What would happen if this call was removed from the class file (after compilation)?
I tried it myself.
class Test
{
public Test()
{
System.out.println("Hello World");
}
public static void main(String[] args)
{
new Test()
}
}
I compiled it and removed invokespecial java/lang/Object/<init>()V
from the constructor with a class file editor.
It seems like the JVM refuses to load the class:
Exception in thread "main" java.lang.VerifyError: Operand stack overflow
Exception Details:
Location:
Test.<init>()V @4: ldc
Reason:
Exceeded max stack size.
Current Frame:
bci: @4
flags: { flagThisUninit }
locals: { uninitializedThis }
stack: { uninitializedThis, 'java/io/PrintStream' }
Bytecode:
0000000: 2ab2 0002 1203 b600 04b1
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Unknown Source)
at java.lang.Class.getMethod0(Unknown Source)
at java.lang.Class.getMethod(Unknown Source)
at sun.launcher.LauncherHelper.validateMainClass(Unknown Source)
at sun.launcher.LauncherHelper.checkAndLoadMain(Unknown Source)
I still don't know if that is a defined behaviour.
EDIT
According to Raedwald I also have to alter the stack manipulation.
So I also removed aload_0
which was before the super constructor call.
Now I get the following exception:
Exception in thread "main" java.lang.VerifyError: Constructor must call super()
or this() before return
Exception Details:
Location:
org/exolin/geno/Test.<init>()V @8: return
Reason:
Error exists in the bytecode
Bytecode:
0000000: b200 0212 03b6 0004 b1
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Unknown Source)
at java.lang.Class.getMethod0(Unknown Source)
at java.lang.Class.getMethod(Unknown Source)
at sun.launcher.LauncherHelper.validateMainClass(Unknown Source)
at sun.launcher.LauncherHelper.checkAndLoadMain(Unknown Source)
This made me curious so I reordered the constructor instructions to:
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "Message"
invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
aload_0
invokespecial java/lang/Object/<init>()V
return
Which worked!
There is a distinction between what is required at the Java language level and what is required at the bytecode level, which is much laxer.
At the Java language level, you must have a constructor call as the first statement, but the compiler will insert one implicitly if you leave it out.
At the bytecode level, the only requirement is that there is exactly one constructor call along every code path that returns normally, plus there are restrictions on what you can do with the this
value prior to initialization.
In particular, this means that
The first option actually appears in Javac generated code, because in Java, you can pass an expression as argument to the constructor call, and the code to calculate that expression obviously has to happen before the constructor is called. But the rest of those options are impossible to accomplish in Java.
Note that if you violate these restrictions, which is what will happen if you remove the constructor call from an ordinary Java constructor that returns normally, the constructor will now be invalid because it returns but has no ctor call. Thus, attempting to load the class will fail at runtime with a VerifyError.
For completeness, here is what you can do with an uninitialized this
value: compare it against null, and store (but not read) fields defined in the same class.
Not much, right? But the ability to store fields defined in the same class prior to the ctor call is actually used by the Java compiler. There is no notion of inner classes at the bytecode level, so when you reference the outer class in the inner class, the compiler generates a hidden field in the inner class containing a reference to the outer class. In order to make sure this works properly if the superclass constructor calls a method that is overriden in the inner class and that accesses the outer class, this hidden field has to be initialized prior to the superclass constructor call. Of course, there's more you can do with this when you're writing the bytecode yourself.
The latest JVM spec says this in the 4.9.2 Structural Constraints section:
"Each instance initialization method, except for the instance initialization method derived from the constructor of class Object, MUST call either another instance initialization method of this or an instance initialization method of its direct superclass super before its instance members are accessed."
In other words, if you remove the invokespecial <init>
instruction sequence corresponding to the constructor's super(...)
or this(...)
call, then the class file is invalid, and the verifier should detect it.
(And according to @Jimmy T's investigations, it does!)
The reasoning behind this constraint is explained by other Answers.