我们先看如下代码:
open class Parent(open var name: String) {
var nameLength: Int
init {
nameLength = name.length
}
}
class Child(override var name: String) : Parent(name) {
init {
nameLength = name.length
}
}
fun main(args: Array<String>) {
Parent("xujiafeng")
Child("xujiafeng")
}
复制代码
如上代码运行会报错吗?在哪一步报错?
先抛开结果,我们来具体分析其的执行过程。
我们先来看Parent("xujiafeng")
,这就是简单的构造对象,调用的是其的构造方法,我们结合idea上的kotlin插件的字节码来分析:
public <init>(Ljava/lang/String;)V
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 1
LDC "name"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 3 L1
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
ALOAD 0
ALOAD 1
PUTFIELD other/kotlin/Parent.name : Ljava/lang/String;
L2
LINENUMBER 7 L2
ALOAD 0
ALOAD 0
INVOKEVIRTUAL other/kotlin/Parent.getName ()Ljava/lang/String;
INVOKEVIRTUAL java/lang/String.length ()I
PUTFIELD other/kotlin/Parent.nameLength : I
L3
RETURN
L4
LOCALVARIABLE this Lother/kotlin/Parent; L0 L4 0
LOCALVARIABLE name Ljava/lang/String; L0 L4 1
MAXSTACK = 2
MAXLOCALS = 2
复制代码
这个构造方法逻辑很简单:
- 调用静态方法kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull来检查输入参数是否为空,其的实现是如果为空就会抛出空指针异常,kotlin里的包含非空参数的方法都会调用这个来保证参数确实非空;
- 调用Object的初始化方法,这个是java构造方法默认的规则,需要调用父类的构造方法;然后将传入的输入参数的值赋给那么属性;
- 调用Parent的getName方法获取那么属性,调用java/lang/String.length方法获取长度赋值给nameLength;
如上来看,调用Parent("xujiafeng")
不会有任何问题,毕竟逻辑很简单。
我们再来看Child("xujiafeng")
的执行过程,其字节码如下:
public <init>(Ljava/lang/String;)V
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 1
LDC "name"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 11 L1
ALOAD 0
ALOAD 1
INVOKESPECIAL other/kotlin/Parent.<init> (Ljava/lang/String;)V
ALOAD 0
ALOAD 1
PUTFIELD other/kotlin/Child.name : Ljava/lang/String;
RETURN
L2
LOCALVARIABLE this Lother/kotlin/Child; L0 L2 0
LOCALVARIABLE name Ljava/lang/String; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
复制代码
同样的是先检查输入参数是否为空,然后调用Parent类的构造方法。
在看调用Parent类的构造方法之前,我们先来看一下Child类的结构,它有两个继承于Parent类的属性nameLength
和name
,然后,由于在其的构造方法里添加了var
的,所以他构造函数的name
不是一个单纯的构造方法输入参数,而是一个属性,这点也能够在字节码里看到证明:
public final class other/kotlin/Child extends other/kotlin/Parent {
// access flags 0x2
private Ljava/lang/String; name
}
复制代码
那么我们现在再来看调用Parent的构造方法的过程:
public <init>(Ljava/lang/String;)V
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 1
LDC "name"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 3 L1
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
ALOAD 0
ALOAD 1
PUTFIELD other/kotlin/Parent.name : Ljava/lang/String;
L2
LINENUMBER 7 L2
ALOAD 0
ALOAD 0
INVOKEVIRTUAL other/kotlin/Parent.getName ()Ljava/lang/String;
INVOKEVIRTUAL java/lang/String.length ()I
PUTFIELD other/kotlin/Parent.nameLength : I
L3
RETURN
L4
LOCALVARIABLE this Lother/kotlin/Parent; L0 L4 0
LOCALVARIABLE name Ljava/lang/String; L0 L4 1
MAXSTACK = 2
MAXLOCALS = 2
复制代码
- 第一步和之前一样;
- 第二步,将传入的参数赋值的时候,注意这是赋值给了Parent.name;
- 第三步贤惠调用Parent.getName,注意这是一个虚方法调用,也就是在运行的时候会根据当前的调用对象来确定最终是调用哪一个类的实现,而这里调用这个方法的对象是Child类的实例,而getName方法Child类是有实现的,如下:
public getName()Ljava/lang/String;
@Lorg/jetbrains/annotations/NotNull;() // invisible
L0
LINENUMBER 11 L0
ALOAD 0
GETFIELD other/kotlin/Child.name : Ljava/lang/String;
ARETURN
L1
LOCALVARIABLE this Lother/kotlin/Child; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
复制代码
会调用到如上方法,然后看这里的返回,返回的是Child.name
,而此时Child.name
还未被初始化,要等到Parent的构造方法执行完之后,Child方法继续执行才会初始化Child.name
,所以返回的是空值;然后继续看Parent的构造方法,下一步是尝试获取gatName方法返回值的长度作为nameLength属性的值,显然,这里就出现了空指针异常。
如上就是调用分析的全部过程。
那么如何修复代码呢?
只要吧Child的构造方法的var去掉,不要在新产生一个那么的field,让Child的name单纯的作为构造方法里的输入参数就好。
还或者,产生这个空指针的根源是,调用getName方法北子类重写了,然后子类这个时候没有初始化完成而产生的,所以可以直接让父类的name属性不让被继承。