1、空类型安全
1.1、可空类型正确用法
kotlin是强类型判断的,每一个对象都有可以为空和不可以为空之分。
代码语言:javascript复制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 异常
举个例子:
代码语言:javascript复制private var mHomeWorkId = ""
mHomeWorkId = intent.getStringExtra(INPUT_HOME_WORK_ID)
由代码可见,mHomeWorkId 是一个不能为null的String。然后通过intent.getStringExtra 方法给mHomeWorkId赋值。
代码语言:javascript复制public String getStringExtra(String name) {
return mExtras == null ? null : mExtras.getString(name);
}
getStringExtra() 方法的返回值是可能为 null的。因此,当getStringExtra返回值为null时,给mHomeWorkId赋值时,就会报错。
正确写法:
代码语言:javascript复制mHomeWorkId = intent.getStringExtra(INPUT_HOME_WORK_ID)?:""
1.2.2、条件判断问题
看下面例子:
代码语言:javascript复制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时,给左边 一个默认值。
代码语言:javascript复制var mUser:User? = null
if(mUser?.grade?:-1 != -1){
//做一些操作
}
1.2.3、is、as 中的坑
obj is String 之后,作用域之中,类型就已经转换了。
代码语言:javascript复制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
代码语言:javascript复制//错误写法1,text不是String或为空时,会报异常
var strAble1 = text as String
//错误写法2,text不是String时,同样会报异常
var strAble2 = text as String?
as的推荐写法:
代码语言:javascript复制//正确写法,转换失败自动转换为空对象
var strAble = text as? String
2、TODO 语句报错问题
在kotlin开发中,当实现某个抽象方法时,会自动生成一条TODO语句。
代码语言:java复制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的时候,才能使用!!。
举个例子:
代码语言:javascript复制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 函数来返回不同的值:
代码语言:java复制class Person(val birthDay: DateTime) {
val age: Int
get() = yearsBetween(birthDay, DateTime.now())
}
可以看到,虽然没有方法来设置 age 的值,但会随着当前日期的变化而变化。
这种情况下,我建议不要自定义 val 属性的 getter 方法。如果一个只读的类属性会随着某些条件而变化,那么应当用函数来替代:
代码语言:java复制class Person(val birthDay: DateTime) {
fun age(): Int = yearsBetween(birthDay, DateTime.now())
}
这也是 Kotlin 代码约定中所提到的,当具有下面列举的特点时使用属性,不然更推荐使用函数:
- 不会抛出异常。
- 具有 O(1) 的复杂度。
- 计算时的消耗很少。
- 同时多次调用有相同的返回值。
3.2、使用 lateinit
有些情况我们不能使用 val,比如,在 Android 中某些属性需要在 onCreate() 方法中初始化。对于这种情况,Kotlin 提供了 lateinit 关键字。
代码语言:java复制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。对于基础数据类型,我们可以这样:
代码语言:java复制private var mNumber: Int by Delegates.notNull<Int>()
3.3、使用 let 函数
下面是 Kotlin 代码常见的编译错误:
许多开发者都会选择快速修复:
代码语言:java复制private var mPhotoUrl: String? = null
fun uploadClicked() {
if (mPhotoUrl != null) {
uploadPhoto(mPhotoUrl!!)
}
}
但这里选择 let 函数是一个更优雅的解决方法:
代码语言:java复制private var mPhotoUrl: String? = null
fun uploadClicked() {
mPhotoUrl?.let { uploadPhoto(it) }
}
3.4、创建全局函数来处理更复杂的情况
let 是一个对于 null 检查很好的替代品,但有时我们会遇到更复杂的情况。比如:
代码语言:java复制if (mUserName != null && mPhotoUrl != null) {
uploadPhoto(mUserName!!, mPhotoUrl!!)
}
你可以选择嵌套两个 let,但这样可读性并不好。这时你可以构建一个全局函数:
代码语言:java复制fun <T1, T2> ifNotNull(value1: T1?, value2: T2?, bothNotNull: (T1, T2) -> (Unit)) {
if (value1 != null && value2 != null) {
bothNotNull(value1, value2)
}
}
然后直接调用:
代码语言:java复制ifNotNull(name,address){name,address->
uploadPhoto(name,address)
}
3.5、使用 ?: 操作符
代码语言:java复制fun getName():String{
if(name!=null){
return name!!
}else{
return "android coder"
}
}
上面方法可以简化为:
代码语言:java复制fun getName():String{
return name?:"android coder"
}
3.6、自定义崩溃信息
如果我们使用 !!,那么当这个变量为 null 时,只会简单的抛出一个 KotlinNullPointerException。这时我们可以用 requireNotNull 或 checkNotNull 来附带异常信息,方便我们调试。
代码语言:java复制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 编译器会默默地帮我们生成一些函数。例如:
代码语言:java复制data class Person(var name: String, var age: Int) {}
这个Bean是用于接收服务器数据,通过Gson转化为对象的。例如:
代码语言:java复制val gson = Gson()
val person = gson.fromJson<Person>("{"age":"12"}", Person::class.java)
println(person.name)
我们传递了一个json字符串,但是没有包含key为name的值,并且注意:
在Person中name的类型是String,也就是说是不允许name=null的
输出结果:
代码语言:java复制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字段) 比如:
代码语言:java复制open class Person: Serializable{
open var name: String? = null
}
class SpecialPerson(override var name: String?) : Person() {
override fun toString(): String {
return name?: ""
}
}
解决方法是,在子类中用init初始化块将构造函数中获取到的属性值赋给继承的属性,即:
代码语言:java复制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文件中:
第一:加入
代码语言:java复制apply plugin: 'kotlin-kapt'
第二:使用
代码语言:java复制kapt {
arguments {
arg("moduleName", project.getName())
}
}
代替:
代码语言:java复制javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
第三:使用
代码语言:java复制kapt 'com.alibaba:arouter-compiler:1.0.4'
代替:
代码语言:java复制annotationProcessor 'com.alibaba:arouter-compiler:1.0.4'
kapt 可以替代annotationProcessor 注释java类
6、Kotlin 复写 Java 父类中的方法,这里有坑
Java 父类定义 onDialogCreate 方法
代码语言:java复制// JavaKengBase.java
public class JavaKengBase {
public void onDialogCreate(Object savedInstanceState) {
// todo nothings
}
}
Kotlin 继承并复写 JavaKengBase
代码语言:java复制class Keng : JavaKengBase() {
override fun onDialogCreate(savedInstanceState: Any) {// 注意:此处,是Any,不是Any?
super.onDialogCreate(savedInstanceState)
}
}
利用 Java 多态特性,调用 onDialogCreate,并传入 null 参数
代码语言:java复制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中的单例模式
代码语言:java复制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中配置两个地方:
代码语言:java复制//这个插件提供了很多新特性
apply plugin: 'kotlin-android-extensions'
代码语言:java复制android{
androidExtensions {
//新版本不需要这个了
experimental = true
// 这个配置是为了禁用除了parcelize外的其他功能
features = ["parcelize"]
}
}
但是遇见了一个问题,添加完上面两个地方后,@Parcelize注解死活不能用,根本不能识别。
原来,第一处的kotlin-android扩展插件写的顺序是有要求的。我们必须先写apply plugin: 'kotlin-android',然后再写apply plugin: 'kotlin-android-extensions',如果顺序写反了就会出现不能识别的情况。