专栏首页Android技术干货Kotlin 开发中遇到的坑(持续更新)
原创

Kotlin 开发中遇到的坑(持续更新)

1、空类型安全

1.1、可空类型正确用法

kotlin是强类型判断的,每一个对象都有可以为空和不可以为空之分。

var a: String = "abc"
a = null // 编译错误
 
var b: String? = "abc"
b = null // ok
 
val l = a.length // ok
 
val l = b.length // 编译错误:变量“b”可能为空
val l = b?.length ?: 0

1.2、可能出现异常的一些使用方式

1.2.1、getStringExtra 方法 可能导致的 null 异常

举个例子:

private var mHomeWorkId  = ""
mHomeWorkId = intent.getStringExtra(INPUT_HOME_WORK_ID)

由代码可见,mHomeWorkId 是一个不能为null的String。然后通过intent.getStringExtra 方法给mHomeWorkId赋值。

public String getStringExtra(String name) {
    return mExtras == null ? null : mExtras.getString(name);
}

getStringExtra() 方法的返回值是可能为 null的。因此,当getStringExtra返回值为null时,给mHomeWorkId赋值时,就会报错。

正确写法:

mHomeWorkId = intent.getStringExtra(INPUT_HOME_WORK_ID)?:""

1.2.2、条件判断问题

看下面例子:

var mUser:User? = null
if(mUser?.grade != -1){
    //做一些操作
}

上面代码看上去很简单,定义了一个 User类型的属性mUser,是可以为null的。在if条件中通过判断年级是否等于-1 来做一下操作。我们在写这段代码的时候想的可能是:当mUser不为null,而且年级不是-1的时候,通过条件判断,然后做一下操作。

但是在kotlin中,当mUser为null时,mUser?.grade的取值为null,而null != -1 ,在kotlin是成立的,这就不符合我们实际逻辑了。

解决方法如下:通过 ?: 操作,当mUser等于null时,给左边 一个默认值。

var mUser:User? = null
if(mUser?.grade?:-1 != -1){
    //做一些操作
}

1.2.3、is、as 中的坑

obj is String 之后,作用域之中,类型就已经转换了。

fun testAsIs() {
  var obj: Any? = null
 
  if (obj is String) {// 方法体内的作用域,obj 就是 String
      var length = obj.length
  }
}

as的两种不推荐写法,会抛出异常:TypeCastException: null cannot be cast to non-null type kotlin.String

//错误写法1,text不是String或为空时,会报异常
var strAble1 = text as String
//错误写法2,text不是String时,同样会报异常
var strAble2 = text as String?

as的推荐写法:

//正确写法,转换失败自动转换为空对象
var strAble = text as? String

2、TODO 语句报错问题

在kotlin开发中,当实现某个抽象方法时,会自动生成一条TODO语句。

override fun cancelRequest() {
    TODO("Not yet implemented")
}

记得把TODO(“not implemented”)注释掉,不然会抛出An operation is not implemented: not implemented异常

3、尽量避免使用 !!

对于 Null 的检查是 Kotlin 的特点之一。强制你在编码过程中考虑变量是否可为 null,因此可以避免很多在 Java 中隐藏的 NullPointerException。!! 表示这个对象一定不为null。因此只有当你百分百确认该对象不可能为null的时候,才能使用!!。

举个例子:

if (mPrimaryData != null) {
    mSecondaryData = mPrimaryData!!.secondaryList[0]
}

但是,当你用插件直接将 Java 代码转换为 Kotlin 时,你会发现有很多 !! 在里面。但其实 !! 意味着「有一个潜在未处理的 KotlinNullPointerException 在这里」。特别是在多线程开发环境中,而java代码又缺少了对null的强检查,这就更容易会出现空异常了。下面给大家减少及个避免 !! 的方法:

3.1、用 val 而不是 var

在 Kotlin 中 val 代表只读,var 代表可变。建议尽可能多的使用 val。val 是线程安全的,并且不需要担心 null 的问题。只需要注意 val 在某些情况下也是可变的就行了。对于普通变量来说,不可变( immutable )和只读( read-only )之间没什么区别。因为你没有办法复写一个 val 变量,所以在这时确实是不可变的。

但如果是对于类的成员变量来说,那只读和不可变的区别可就大了。在 Kotlin 的类中,val 和 var 是用于表示属性是否有 getter/setter:

  • var:同时有 getter 和 setter。
  • val:只有 getter。

但是可以通过自定义 getter 函数来返回不同的值:

class Person(val birthDay: DateTime) {  
  val age: Int
    get() = yearsBetween(birthDay, DateTime.now())
}

可以看到,虽然没有方法来设置 age 的值,但会随着当前日期的变化而变化。

这种情况下,我建议不要自定义 val 属性的 getter 方法。如果一个只读的类属性会随着某些条件而变化,那么应当用函数来替代:

class Person(val birthDay: DateTime) {  
  fun age(): Int = yearsBetween(birthDay, DateTime.now())
}

这也是 Kotlin 代码约定中所提到的,当具有下面列举的特点时使用属性,不然更推荐使用函数:

  • 不会抛出异常。
  • 具有 O(1) 的复杂度。
  • 计算时的消耗很少。
  • 同时多次调用有相同的返回值。

3.2、使用 lateinit

有些情况我们不能使用 val,比如,在 Android 中某些属性需要在 onCreate() 方法中初始化。对于这种情况,Kotlin 提供了 lateinit 关键字。

private lateinit var mAdapter: RecyclerAdapter<Transaction>
 
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   mAdapter = RecyclerAdapter(R.layout.item_transaction)
}
 
fun updateTransactions() {
   if(this::mAdapter.isInitialized){
      mAdapter.notifyDataSetChanged()
   }
}

要注意,访问未初始化的 lateinit 属性会导致 UninitializedPropertyAccessException。因此在使用的时候最好判断是否初始化。

并且 lateinit 不支持基础数据类型,比如 Int。对于基础数据类型,我们可以这样:

private var mNumber: Int by Delegates.notNull<Int>()

3.3、使用 let 函数

下面是 Kotlin 代码常见的编译错误:

许多开发者都会选择快速修复:

private var mPhotoUrl: String? = null
fun uploadClicked() {
    if (mPhotoUrl != null) {
        uploadPhoto(mPhotoUrl!!)
    }
}

但这里选择 let 函数是一个更优雅的解决方法:

private var mPhotoUrl: String? = null
fun uploadClicked() {
    mPhotoUrl?.let { uploadPhoto(it) }
}

3.4、创建全局函数来处理更复杂的情况

let 是一个对于 null 检查很好的替代品,但有时我们会遇到更复杂的情况。比如:

if (mUserName != null && mPhotoUrl != null) {
   uploadPhoto(mUserName!!, mPhotoUrl!!)
}

你可以选择嵌套两个 let,但这样可读性并不好。这时你可以构建一个全局函数:

fun <T1, T2> ifNotNull(value1: T1?, value2: T2?, bothNotNull: (T1, T2) -> (Unit)) {
   if (value1 != null && value2 != null) {
       bothNotNull(value1, value2)
   }
}

然后直接调用:

ifNotNull(name,address){name,address->
    uploadPhoto(name,address)
}

3.5、使用 ?: 操作符

fun getName():String{
    if(name!=null){
        return name!!
    }else{
        return "android coder"
    }
}

上面方法可以简化为:

fun getName():String{
    return name?:"android coder"
}

3.6、自定义崩溃信息

如果我们使用 !!,那么当这个变量为 null 时,只会简单的抛出一个 KotlinNullPointerException。这时我们可以用 requireNotNull 或 checkNotNull 来附带异常信息,方便我们调试。

uploadPhoto(requireNotNull(intent.getStringExtra("PHOTO_URL"), { 
    "Activity parameter 'PHOTO_URL' is missing" 
}))

总而言之,绝大多数情况下你都不需要 !!,可以用上面提到的 6 个技巧来消除 !!。这样能让代码更安全、更容易 debug 并且更干净。

4、Gson与Kotlin碰撞出的不安全操作

4.1、使用 data class 没有设置无参构造函数

在 Kotlin 中,不需要自己动手去写一个 JavaBean,可以直接使用 DataClass,使用 DataClass 编译器会默默地帮我们生成一些函数。例如:

data class Person(var name: String, var age: Int) {}

这个Bean是用于接收服务器数据,通过Gson转化为对象的。例如:

val gson = Gson()
val person = gson.fromJson<Person>("{\"age\":\"12\"}", Person::class.java)
println(person.name)

我们传递了一个json字符串,但是没有包含key为name的值,并且注意:

在Person中name的类型是String,也就是说是不允许name=null的

输出结果:

null

是不是有些奇怪,感觉意外绕过了Kotlin的空类型检查。那么是什么原因导致的呢?

原因是:Person在被转Java代码时,只会生成一个包含两个参数的构造方法,没有提供默认的构造方法。Gson在通过反射创建对象时,会优先尝试获取无参构造函数。如果没有找到无参构造函数时,它就直接通过Unsafe的方法,绕过了构造方法,直接构建了一个对象。

因此我们在使用 data class,在遇到上面类似需求的时候,最好提供一个无参构造方法。

具体原因可以看这篇文章:https://cloud.tencent.com/developer/article/1788617

4.2、bean类继承了父类并在主构造函数中覆盖了父类的属性

使用Gson解析json时,如果bean类继承了父类并在主构造函数中覆盖了父类的属性,那么会报错:declares multiple JSON fields named name(声明多个名为name的JSON字段) 比如:

open class Person: Serializable{
    open var name: String? = null
}
class SpecialPerson(override var name: String?) : Person() {
    override fun toString(): String {
        return name?: ""
    }
}

解决方法是,在子类中用init初始化块将构造函数中获取到的属性值赋给继承的属性,即:

class SpecialPerson(var specialName: String?) : Person() {
    init {
        name = specialName
    }
    override fun toString(): String {
        return name?: ""
    }
}

5、Arouter中使用kotlin编写的Interceptor不生效的问题

问题原因在于kotlin文件中的@Interceptor注解没有被正确处理,因此没有将自定义的Interceptor加入到Interceptor集合中,解决方法为在module的build.gradle文件中:

第一:加入

apply plugin: 'kotlin-kapt'

第二:使用

kapt {
    arguments {
        arg("moduleName", project.getName())
    }
}

代替:

javaCompileOptions {
    annotationProcessorOptions {
        arguments = [moduleName: project.getName()]
    }
}

第三:使用

kapt 'com.alibaba:arouter-compiler:1.0.4'

代替:

annotationProcessor 'com.alibaba:arouter-compiler:1.0.4'

kapt 可以替代annotationProcessor 注释java类

6、Kotlin 复写 Java 父类中的方法,这里有坑

Java 父类定义 onDialogCreate 方法

// JavaKengBase.java
public class JavaKengBase {
    public void onDialogCreate(Object savedInstanceState) {
        // todo nothings
    }
}

Kotlin 继承并复写 JavaKengBase

class Keng : JavaKengBase() {
    override fun onDialogCreate(savedInstanceState: Any) {// 注意:此处,是Any,不是Any?
        super.onDialogCreate(savedInstanceState)
    }
}

利用 Java 多态特性,调用 onDialogCreate,并传入 null 参数

public class KengJava {
    public static void main(String[] args) {
        JavaKengBase keng = new Keng();
        keng.onDialogCreate(null);// 注意:空参数
    }
}

这里可以有两个问题:

第一个:"overrides nothing"

原因就在 onDialogCreate(savedInstanceState: Any) 方法定义中的:Any,不是Any?上。

注意:不要相信 AS 编译器,使用快捷键 Override Method 时,还是需要额外关注参数是否 Nullable?

第二个:IllegalArgumentException: Parameter specified as non-null is null

就算通过了编译,但在运行时,可能会抛出 Parameter specified as non-null is null异常,这个异常也是Java与Kotlin混合开发中的高频异常。

综上:上述问题,很好解决,只需要在方法参数后面,增加一个?即可。

override fun onDialogCreate(savedInstanceState: Any?) 

7、kotlin中的单例模式

class Singleton private constructor() {
    companion object {
        val instance: Singleton by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            Singleton()
        }
    }
}

8、Kotlin 使用@Parcelize注解实现Parcelable

这里不介绍@Parcelize注解的具体使用,只记录使用过程中遇到的问题。使用方法大家可以自己百度下,很简单。

8.1、apply plugin:'kotlin-android-extensions'引用问题

大家都知道@Parcelize使用需要在module的build.gradle中配置两个地方:

//这个插件提供了很多新特性
apply plugin: 'kotlin-android-extensions'
android{
    androidExtensions {
        //新版本不需要这个了
        experimental = true
        // 这个配置是为了禁用除了parcelize外的其他功能
        features = ["parcelize"]
    }
}

但是遇见了一个问题,添加完上面两个地方后,@Parcelize注解死活不能用,根本不能识别。

原来,第一处的kotlin-android扩展插件写的顺序是有要求的。我们必须先写apply plugin: 'kotlin-android',然后再写apply plugin: 'kotlin-android-extensions',如果顺序写反了就会出现不能识别的情况。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • ViewRootImpl 源码解析(三大主要功能实现解析)

    在Android2.2以后用ViewRootImpl代替ViewRoot,对应于ViewRootImpl.java,他是链接WindowManager和Deco...

    李林LiLin
  • Android避坑指南,Gson与Kotlin碰撞出一个不安全的操作

    是的,确实很偏,跳过这个问题,我们往下看,看看是怎么在Android开发过程中遇到的,而且看完后,这个问题就迎刃而解了。

    李林LiLin
  • Kotlin 集合使用详细解析

    Kotlin 标准库提供了基本集合类型的实现: set、list 以及 map。 一对接口代表每种集合类型:

    李林LiLin
  • 如何使用Java调用CM的API动态配置Yarn资源池

    用户在使用CDH集群大数据平台时会有需求在自己的统一管理平台上通过API接口能够动态的设置Yarn资源池,Cloudera Manager提供了丰富的API接口...

    Fayson
  • 使用Optional来减少null检查

    平常我们使用null检查在项目中简直太常见了,从数据库中查询到的数据可能不存在返回null,service中处理中发现不存在返回一个null,在互相调用的时候每...

    Dylan Liu
  • Docker swarm 获取service的container信息

    服务创建好后,如何来获取该service包含的容器信息呢?比如获取刚才创建的mysql服务的容器。我们可以通过docker service ps命令来获取,

    JadePeng
  • 获取ztree树的选中子菜单信息并且提交给后端

    前面写过,ztree实现一棵树的文章,https://www.jianshu.com/p/c2b919e91e91 现在要用ajax+json模拟交互效果

    王小婷
  • 浅谈(String),toString() ,String.ValueOf() 3种类型转换

    这是标准的类型转换,将object转成String类型的值。使用这种方法时,需特别小心的是因定义为 Object 类型的对象在转成String时语法...

    用户6182664
  • 关于 JavaScript 中 null 的一切

    JavaScript 有两种类型:原始类型(strings, booleans, numbers, symbols)和对象

    GopalFeng
  • JDK8的LinkedList源码学习笔记

    itliusir

扫码关注云+社区

领取腾讯云代金券