Java中子类继承父类,父类中定义定义了抽象方法,子类在实现时,给子类变量赋值,执行构造后,变量值恢复成默认值


文章目录

  • Java中子类继承父类,父类中定义定义了抽象方法,子类在实现时,给子类变量赋值,执行构造后,变量值恢复成默认值
  • 现象描述与代码
  • 背景
  • 1、定义接口
  • 2、创建实现类
  • 3、调试并验证
  • 进一步研究
  • 1、查看AImpl的字节码
  • 2、那如果我不赋初始值呢?
  • 3、那我看看Kotlin怎么样。
  • 4、那如果试试,赋的初始值不是0呢?
  • 结论


现象描述与代码

背景

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……

在定义接口类型涉及到赋值操作时,一定要注意 不要将定义的接口在父类的构造方法中执行。