话说呀,数据类本来设计出来就应该是一种纯数据结构,可偏偏它也是一个类,所以我们自然可以为它定义各种成员,甚至扩展,通常来说这倒也不是什么问题。不过如果我们定义了需要在主构造器中执行的代码,那么就可能会有点儿麻烦了。
data class Person(val name: String, val age: Int){
val lastName: String by lazy {
name.split(" ").last()
}
val firstName: String by lazy {
name.split(" ").first()
}
}
假设构造 Person
这个类的时候传入的 name
都是标准的 DonaldTrump
这样的格式,所以下面的代码理论上是可以拿到普爷的名和姓的:
val trump = Person("Donald Trump", 71)
println(trump.firstName)
输出的就是:
Donald
那么问题来了,一般来说数据类都是免不了要序列化和反序列化的,所以有可能普爷是从硬盘上来的:
val trump = Gson().fromJson(json, Person::class.java)
println(trump.firstName)
这个 json 长这样:
{
"name": "Donald Trump",
"age": 71
}
那么结果呢?
Exception in thread "main" java.lang.NullPointerException
at com.bennyhuo.kotlin.ext4nulls.Person.getFirstName(main.kt)
at com.bennyhuo.kotlin.ext4nulls.MainKt.main(main.kt:12)
什么情况。。怎么就出了空指针了呢?
原因是 Person
这个类没有无参构造方法,所以 Gson
会用 Unsafe
去实例化它,这样的话主构造器就被跳过了。我们看下抛异常的位置等效的 Java 代码:
public final String getFirstName() {
Lazy var1 = this.firstName$delegate;
KProperty var3 = $$delegatedProperties[1];
return (String)var1.getValue();
}
而这个 firstName$delegate
实际上只在主构造器调用时才初始化了:
this.firstName$delegate = LazyKt.lazy((Function0)(new Function0() {
...
}));
既然主构造器没有被调用,那么这段代码一定执行不到,自然就有了获取 firstName
时的空指针了。
那么用 noarg
插件行不行呢?答案自然也是不行的。之前有文章讲到,生成的无参构造器,除了调用了父类的默认无参构造之外,什么事儿都没有做。
当然,有人会说, Unsafe
这个我们管不了,可为什么 noarg
插件生成的默认无参构造也不对类当中定义的这些成员进行初始化呢?
首先,类的设计要求主构造器一定要在实例化的时候调用, noarg
也好 Unsafe
也好,都是不得已做出的妥协,我们不应该为它们找理由让严谨的语言设计做出更多的让步。
其次,有些情况下,需要初始化的成员可能需要得到一个正确的主构造器的参数,如果我们将代码稍作修改:
data class Person(val name: String, val age: Int){
val lastName = name.split(" ").last()
val firstName = name.split(" ").first()
}
那么除非主构造器被正确调用,否则 lastName
和 firstName
无论如何也不能正确地被初始化。
数据类的初始化往往会突破 Kotlin 语言的安全条件,这让我们的代码处于危险的境地。因此对于需要序列化数据类的情景,大家在编写代码时还是需要多加注意,不要在数据类当中写有特定初始化逻辑的属性,反序列化的场景中,这样的属性无法保证被正确地初始化。显然,数据类就作为数据结构使用就行了,尽量不要越过这条红线做一些其他的事情,以免产生一些奇怪的问题。