Java中子类继承父类,父类中定义定义了抽象方法,子类在实现时,给子类变量赋值,执行构造后,变量值恢复成默认值
文章目录
现象描述与代码
背景
1、定义接口
定义了一个抽象类(java
)如下,目的是提供通用接口,在创建时会调用initData方法。
public abstract class AbstractA {
public AbstractA() {
init();
}
abstract void init();
}
2、创建实现类
class AImpl extends AbstractA {
public int a = 0;
public int b = 0;
public int c = 0;
@Override
void init() {
a = 1;
b = 2;
c = 3;
}
@Override
public String toString() {
return "AImpl{" +
"a=" + a +
", b=" + b +
", c=" + c +
'}';
}
}
3、调试并验证
AImpl a = new AImpl();
System.out.println(a);
按照我的预期,结果应该是:AImpl{a=1, b=2, c=3}
但是结果却是:AImpl{a=0, b=0, c=0}
表现出来的现象是,代码执行完init赋值之后,又被赋值回初始值了。
进一步研究
背景就上面的问题了,通过打断点等等一系列调试之后,发现,在子类执行完父类的构造方法之后,又将自己的属性赋值了。于是推测,是子类属性赋值与父类构造方法的执行顺序导致的问题出现:
子类先执行了父类的构造方法,然后才执行自己属性的赋值
1、查看AImpl的字节码
com.example.studyproject.testConstructor.AImpl();
descriptor: ()V
flags: (0x0000)
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/example/studyproject/testConstructor/AbstractA."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field a:I
9: aload_0
10: iconst_0
11: putfield #3 // Field b:I
14: aload_0
15: iconst_0
16: putfield #4 // Field c:I
19: return
我发现,在invokespecial处是执行父类构造方法,执行之后,果然在下面又对自己的属性赋值了初始值。
2、那如果我不赋初始值呢?
既然是因为我们赋了初始值导致的问题,那么如果我不赋初始值,我们知道Java 会自动帮我们赋初始值,那这样会不会有问题呢?
于是修改AImpl
代码属性赋值部分为如下:
public int a;
public int b;
public int c;
执行,检查结果,结果发现,我的天,怎么事儿,结果竟然不是AImpl{a=0, b=0, c=0}
,反而是正确的AImpl{a=1, b=2, c=3}
了
这是为什么,于是,赶紧看一下字节码:
com.example.studyproject.testConstructor.AImpl();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/example/studyproject/testConstructor/AbstractA."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/studyproject/testConstructor/AImpl;
哦买噶,在我不手动赋初始值的时候,它竟然能执行成功了。而且在构造方法里,没有赋初始值的代码了。这是为什么。
3、那我看看Kotlin怎么样。
Kotlin
定义变量时,必须要赋初始值,我倒要看看,Kotlin
的时候怎么个情况,于是创建个Kotlin
的类
class AImplKotlin : AbstractA() {
var a = 0
var b = 0
var c = 0
public override fun init() {
a = 1
b = 2
c = 3
}
override fun toString(): String {
return "AImplKotlin{" +
"a=" + a +
", b=" + b +
", c=" + c +
'}'
}
}
然后执行测试代码:
AImplKotlin akt = new AImplKotlin();
System.out.println(akt);
结果竟然是正确的:AImplKotlin{a=1, b=2, c=3}
这是什么道理,难道我们的Kotin赋初始值,不在构造方法里执行?这岂不是跟Java不一致了!!
看一下字节码吧:
public com.example.studyproject.testConstructor.AImplKotlin();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method com/example/studyproject/testConstructor/AbstractA."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/studyproject/testConstructor/AImplKotlin;
Oh,No,它竟然真的,没有在构造方法里执行赋值操作。
4、那如果试试,赋的初始值不是0呢?
修改一下Kotlin的代码,把初始值改为不是0的代码。
var a = 4
var b = 5
var c = 6
public override fun init() {
a = 1
b = 2
c = 3
}
继续执行,并验证结果:
结果是:
AImplKotlin{a=4, b=5, c=6}
哦,这会它错了,再看一下字节码吧:
public com.example.studyproject.testConstructor.AImplKotlin();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method com/example/studyproject/testConstructor/AbstractA."<init>":()V
4: aload_0
5: iconst_4
6: putfield #12 // Field a:I
9: aload_0
10: iconst_5
11: putfield #15 // Field b:I
14: aload_0
15: bipush 6
17: putfield #18 // Field c:I
20: return
LineNumberTable:
line 3: 0
line 4: 4
line 5: 9
line 6: 14
line 3: 20
LocalVariableTable:
Start Length Slot Name Signature
0 21 0 this Lcom/example/studyproject/testConstructor/AImplKotlin;
果然Kotlin其实也是会在父类构造方法执行之后,给属性赋值的。
结论
在Java的class中:
- 父类的构造方法会在子类属性赋值之前执行,如果在父类中,涉及到了子类属性的赋值,会覆盖。
- 上述情况发生的前提是:子类定义属性时赋了初始值,如果不赋初始值,则没问题。
在Kotlin的class中
- 父类的构造方法会在子类属性赋值之前执行,如果在父类中,涉及到了子类属性的赋值,当我们手动指定的初始值不为默认初始值时会覆盖。
- 默认初始值:Int类型为0,Long类型为0L,引用类型为null……
在定义接口类型涉及到赋值操作时,一定要注意 不要将定义的接口在父类的构造方法中执行。