Kotlin 实战经验中的那些坑+最佳实践

2,528

确认过眼神,遇上对的人;确认过“坑”点,遇上对的 kotlin

Num 1:方法入参是常量,不可修改

在Kotlin中,参数是常量,这一点可能让 Java 程序员的思维范式有点不太适应。

class Main {

    /**
     * Kotlin 入参是常量
     */
    fun print(a: Int = 1, b: String = "") {
        // a = 10; // 错误:Val cannot be reassigned!!!
    }

}

Num 2:不想要 CompanionINSTANCE关键字?

Java 访问 Kotlin 中定义的静态变量以及静态方法,需要 Companion关键字。例如:

// Main.kt
class Main {
  companion object {
      val EMPTY = ""

      fun isEmpty(string: String = EMPTY) {
          //todo code
      }
      
      @JvmField
      val FULL_NUMBER = "1234567890"

      @JvmStatic
      fun isNumber(string: String = FULL_NUMBER) {
          //todo code
      }
  }
}
// Test.java
class Test {
  public static void main(String[] args) {
      // Java 访问 Kotlin 中的常量
      Keng.Companion.getEMPTY();
      Keng.Companion.isEmpty("");
      
      KengInstance.INSTANCE.getEMPTY();
      
      // Java 访问 Kotlin 中带有 JvmField 修饰的常量,无需 Companion
      String FULL_NUMBER = Keng.FULL_NUMBER;
      // Java 访问 Kotlin 中带有 JvmStatic 修饰的方法,无需 Companion
      Keng.isNumber("");
  }
}

不想出现 Companion 关键字, 可以考虑下 @JvmField@JvmStatic 注解,注解适用于通过 object Main{...} 定义的静态对象场景。

注:特别推荐在Kotlin中使用注解,这种方式,让Java与Kotlin互调,如丝般顺滑。

Num 3:Java 高效的调用 Kotlin 中的重载方法

Kotlin 调用自身中的方法,默认参数是可以不传递参数的,相当于方法重载了。但 Java 调用 Kotlin 的这些方法,还是需要传递默认参数的,例如:

例如: isNumber(string: String = FULL_NUMBER)

// Test.java
class Test {
  public static void main(String[] args) {
    Keng.isNumber("");// 必须要传递个参数
  }
}

能不能让 Java 也享受到 Kotlin 默认参数的快乐?可以的,

// Test.java
class Test {
  public static void main(String[] args) {
    // JvmOverloads 注解的作用,默认实现了 重载 特性
    Keng.isNumberWithOverLoads();
    Keng.isNumberWithOverLoads("");
  }
}

注意:@JvmOverloads与Android View 体系控件搭配使用时,可能会出现一些问题,需要额外关注下。详情传送门:不要总是相信 @JvmOverloads

Num 4:Kotlin 中的判空“姿势”

如下代码,在 Java 中应该还好,但在 Kotlin 中是无法通过编译器编译的。

var nullableString: String? = null
...
fun testNullableString() {
    if (nullableString != null) {
        var nullableStringLength = nullableString.length // 此处会报错!!!
    }
}

会报如下错误:

Error:(9, 40) Kotlin: Smart cast to 'String' is impossible, because 'nullableString' is a mutable property that could have been changed by this time

所以,在kotlin中,正确的判空姿势应该是如下的样子:

fun testNullableString() {
    var nullableStringLength = nullableString?.length
}

Num 5:Kotlin 复写 Java 父类中的方法时,有大坑:

第一步:JavaKeng 类中定义了 onDialogCreate 方法,该方法包含一个可空参数:savedInstanceState ,代码如下:

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

第二步:Kotlin 继承并复写 JavaKeng

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

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

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

这里可以有两个问题:

第一个:"Error:overrides nothing"

原因就在 onDialogCreate(savedInstanceState: Any) 方法定义中的:Any,不是Any?上。错误日志如下:

Error:(17, 5) Kotlin: 'onDialogCreate' overrides nothing

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

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

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

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

override fun onDialogCreate(savedInstanceState: Any?) 

Num 6:Kotlin “狠”起来,连TODO都不放过!

就下面这个平淡朴实无奇的 TODO() ,却会抛出一个 RuntimeException

fun testTodo(): Unit {
    TODO()
}

这个错误就是:kotlin.NotImplementedError: An operation is not implemented.,让我们看看 TODO() 的实现:

public inline fun TODO(): Nothing = throw NotImplementedError()

Num 7:isas 中的坑

obj is String之后,作用域之中,类型就已经转换了,有点类似 java 的多态。

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

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

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

as的推荐写法:

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

Num 8:Kotlin 中的 Property 的理解

在 Kotlin 中,属性(property)不管有没有 get/set 都称之为 property,而在 Java 中 field + get、set方法一起才能是一个 property。

class Person { var age = 0 }

在 Kotlin 里不需要额外的 get/set 方法,当然你也写不了,因为 Kotlin 已经默认实现了 get/set 。

Kotlin 中可以通过如下方式,复写 field 的 get/set :

var age = 0
    set(value){
        age = value + 1
    }

但,这样写其实是会发生递归,无法赋值成功。

正确的写法应该是:

var age = 0
    set(value){
        field = value + 1
    }

区别就在于 field 上。

关于这块,珠玉在前,就不重复造轮子了。感兴趣的,可以进入传送门 : Howshea 理解 Kotlin 中的属性(property)


因为 实践中的坑总是伴随着最佳实践一起出现,所以,最后附上几则最佳实践,以飨读者:

最佳实践 Num 1:also 关键字

while (bufferReader.readLine().also({ line = it }) != null) {
	// do something
}

相当于 Java 中的:

while ((line = bufferReader.readLine()) != null) {
	// do something
}

最佳实践 Num 2:takeIf 关键字

// 原代码
if (someObject != null && status) {
   doThis()
}

// 最佳实践
someObject?.takeIf{ status }?.apply{ doThis() }

最佳实践 Num 3:单例模式的写法

关于设计模式的写法,珠玉在前,同样不重复制造轮子了,传送门:Kotlin的5种单例写法和java对比

//Java实现
public class Singleton {
    private volatile static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance==null){
            synchronized (Singleton.class){
                if(instance==null){
                    instance=new Singleton();
                }
            }
        }
        return instance;
    }
}

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

此处:需要关注下 双重校验加锁 实现中的 lazy 关键字的用法。传送门

最佳实践 Num4:判空

// Java 实现
String value = TextUtils.isEmpty(content)?"":content;

// Kotlin 实现
val value = content ?: ""

最佳实践 Num5:if/elseif/else

// Java 实现
if(a != null) {
    // do something
} else {
    // do something
}

// Kotlin 实现

a?.let {
... *//do your non null logic*
} ?: {
... *//do your null logic*
}