Kotlin 踩坑记和建议(持续更新)

813 阅读4分钟

数据解析与定义

1. Json解析与Bean的定义

Json转Bean的时候,不会进行空安全判断,当设置参数类型为非空类型,如果Json中指定为null, 参数会被赋值为null,后面引用此参数时,会进行空安全检查checkNull, 此时就会执行kotlin.jvm.internal.Intrinsics.checkNotNullParameter 抛出Parameter specified as non-null is null

例如:要解析的实体Bean为

@Keep
data class ResponsePaging(
    @JvmField var paging: Paging? = null,
    @JvmField var timestamp: Long = 0,
    @JvmField var data: List<String> = arrayListOf()
) : Serializable

接口数据为:

{
  "data": null,
  "paging": {
    "hasMore": true,
    "limit": 10,
    "offset": 10,
    "total": 100
  }
}

此时,ResponsePaging 的 data = null,如果在调用data的地方没有使用data?.或者判空,此时就会抛出异常;
所以接口实体类,所有的参数都应该声明为可空,因为不确定服务器返回的数据是否全部有值;

⚠注意
num 和 boolean 如果声明为非空类型,如果指定了默认值,那么值就等于其指定的默认值,即使 Json 的数据明确指定为 null,解析时也会为其赋值为默认值;
如果想要接收 Json 的数据 null,需要声明为可空类型,并且不设置默认值,如:offset: Int? = null

2. JvmField 在通用基类 Bean 定义中的注意事项

定义了分页相关的接口解析通用实体ResponsePaging如下:

@Keep
open data class ResponsePaging<T>(
    @JvmField open var paging: Paging? = null,
    @JvmField open var timestamp: Long = 0,
    @JvmField open var data: List<T> = arrayListOf()
) : Serializable

此时对应接口响应的Json应为:

{
  "data": ...,
  "paging": {
    "hasMore": true,
    "limit": 10,
    "offset": 10,
    "total": 100
  }
}

如果某个接口响应的data参数换名了,例如换成具体的名称feedList,此时我们希望通过继承ResponsePaging重写data来增加例如SerializedName来兼容新的名称,Example:

open data class CustomBean(
    @SerializedName("feedList")
    @JvmField
    override var data: List<String>? = null,
) : ResponsePaging<String>()

这时候问题就出现了:接口解析成功,并且调用CustomBean.data可以获取到正确的值,但如果(CustomBean as ResponsePagingdata).data,此时就不能获取到正确的值;

原因:增加了@JvmField之后,相当于Java中声明字段为public,此时,字段是不能被重写的;
建议:声明通用基类时,尽量不要添加@JvmField注解,并且,重写基类字段时,被重写的字段同样不要增加@JvmField,Kotlin转Class时,子类和父类都会自动生成getter、setter方法,而方法是可以重写的,就可以达到我们做兼容不同字段名的目的;

附Java测试用例:

class SuperClass {
    public String str = "SuperClass";
}
class SubClass extends SuperClass {
    // @Override 加上会报错
    public String str = "SubClass";
}

public static void main(String[] args) {
    SubClass subClass = new SubClass();
    // 将子类的对象赋值给父类的引用
    SuperClass superClass = subClass;    
    System.out.println("-----------------字段------------------");
    System.out.println("superClass.str     " + superClass.str);
    System.out.println("subClass.str       " + subClass.str);
}
// 输出
-----------------字段------------------
superClass2.str     SuperClass2
subClass2.str       SubClass2

要不要广泛使用 Data Class ?

kotlin data class 会自动生成以下函数:

  • gettersetter方法;
  • componentN解构方法,有几个属性,就会生成几个结构方法;
  • copy浅拷贝方法;
  • constructor构造函数,如果有Nullable属性,将会生成多个重载构造函数;
  • toString,数据实体应重写;
  • equals,数据实体应重写;
  • hashCode,数据实体应重写; 由此看到,gettersettercomponentNcopy都是使用频率比较低的函数,并且会增加大量方法数,
    toStringequalshashCode可以使用快捷键生成,并不影响开发效率和使用;
    所以 非必要情况下,不建议使用data class

要不要广泛使用内联函数 inline ?

使用高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭包,即那些在函数体内会访问到的变量。
内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销。

内联函数主要的使用场景是在循环或者尾递归中的函数调用,所以不应该随意在函数上加上inline修饰;

内联函数也有相应的局限性,例如不能使用return,需要用crossInline标记函数作为参数传递到另一个函数中或者嵌套函数调用,需要使用noinline标记需要作为返回值的函数参数;

大量使用内联函数可能会导致字节码的激增,增加包体积;