相信大多数开发者都和我一样,最开始听到kt的介绍就是空指针安全,包括google的IO大会也在说这个特性。声明一个String类型的成员变量大概有以下几种方式,不知道大家平时用的是哪一种?
var a: String = ""
var b: String? = null
lateinit var c: String
val d: String by lazy {
"xxx"
}
a
个人感觉这是不规范的(当然确实有实际意义的默认值除外),这个默认值没有任何意义,仅仅试了实现语法上的“不为空”,方便使用而已。至少我不会这样去使用。不要为了语法方便随意给默认值
b
这是一种可空类型的String字符串,当我们要给它赋值的时候,直接就可以赋值,很方便。但是当我们要用它的时候就比较麻烦了,必须b?.xxx
的形式调用String的实例方法。可以保证空指针安全,但是这样是有问题的!比如现在有一个严格的计算一个人的账户资金的问题,大概形式如下sumFund1()
。本来计算资金是一个非常严谨的问题,从user到account以及fund全部都不为null的,如果出现任何一个为null,说明逻辑有严重问题,但是这一种形式可能就会把这个问题隐藏掉,非常不易于排查。sumFund2()
则是主动抛出这个异常,指明具体错误原因,上层catch到这个错误上报即可。xxx?.xxx?.xxx形式一时爽,出了问题难排查
var mUser: User? = null
fun sumFund1() {
val user = mUser
val fund = user?.mAccount?.mFund
val sum = fund?.plus(3)
println(sum)
}
fun sumFund2() {
val user = mUser ?: throw NullPointerException("user is null")
val account = user.mAccount ?: throw NullPointerException("account is null")
val fund = account.mFund ?: throw NullPointerException("fund is null")
val sum = fund + 3
println(sum)
}
c
这是一种kt给开发者自己做空指针校验的操作符。你可以声明一个不即时初始化的成员变量,什么时候初始化开发者自己做决定,但是你要用之前没有初始化,kt会毫不留情的给你抛出一个运行时的kotlin.UninitializedPropertyAccessException: lateinit property xxx has not been initialized
的错误。lateinit
感觉就像DataType?=null
的对立面一样,它可以让我们的程序更加严谨,同时也对开发者对数据的处理要求更高。个人是比较喜欢这一种操作成员变量的方式,这种方式可以明显避免程序中?
满天飞。能用lateinit不用?=null,严格保证程序逻辑严谨性
d
延迟加载不可变成员参数。d
的获取和前面几种方式有明显的不同,是val
也就是不可变成员参数。不可变参数有很多好处,我们可以方便大胆操作不会有任何多线程问题。并且在kt中实现了延迟加载,更加方便了开发者使用。简直是“最佳实践”!当然,不可变参数有很多应用上的局限,还得可变参数去实现。能用val不用var,能用lateinit不用?=null,避免?满天飞
总结:kt给数据类型分区“可空”数据类型和“不可空”数据类型,并且全部都是标准的数据类型,没有java中的类似于int、long这种基本数据类型,统一了数据类型。让我们在使用的时候更加安全,但是在实际开发的时候很容易滥用。应该根据不同场景采用不同的方式(默认值、可空、延迟加载、懒加载等等)去初始化参数,并且应该明白各种方式的优缺点。
这一种错误在开发中很常见,大致形式如下testSmartCast1
,而testSmartCast2
则是允许的
var mUser2: User? = null
//报错
fun testSmartCast1() {
if (mUser2 == null) throw NullPointerException("mUser2 is null,please check!")
val account = mUser2.mAccount
}
//不报错
fun testSmartCast2() {
val user = mUser2
if (user == null) throw NullPointerException("mUser2 is null,please check!")
val account = user.mAccount
}
为什么testSmartCast1
报错,我不是已经判空了吗?因为多线程!kt不知道你这个方法是否会在多线程中调用,如果确实在多线程中调用,确实有null的可能不是吗?
那为什么testSmartCast2
没有问题?这就涉及到java基本功了,局部变量user指向mUser2,再去操作user。这个时候即使mUser2在其它线程中进行修改,也不会影响user。这种场景也很容易模拟,模拟如下
//点击
an_b_smartCast2.setOnClickListener {
mUser2 = User()
testSmartCast2()
mUser2 = null
}
//fun
fun testSmartCast2() {
val user = mUser2
Log.d(TAG, "testSmartCast2:${Thread.currentThread()}, $user,$mUser2")
Thread {
Thread.sleep(3000)
Log.d(TAG, "testSmartCast2:${Thread.currentThread()}, $user,$mUser2")
}.start()
}
//输出
04-20 17:45:13.830 6601-6601/com.gy.myapplication D/NpeActivity: testSmartCast2:Thread[main,5,main], com.gy.myapplication.User@946ae6b,com.gy.myapplication.User@946ae6b
04-20 17:45:16.832 6601-6626/com.gy.myapplication D/NpeActivity: testSmartCast2:Thread[Thread-376,5,main], com.gy.myapplication.User@946ae6b,null
现在想想我们之前用java写的代码是不是有类似的存在逻辑上的不严谨???kt会在语言层面保证你操作对象的空指针安全。另外,在一些java最佳实践的书中,也比较推荐的在方法中不直接操作成员变量,而是赋值给一个局部变量,再去操作对应的局部变量。
不知道你有没有这样的感觉,lamdba用着不爽。阅读性差应该是最大的原因。
//java 匿名函数
findViewById(R.id.af_b_btn1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "onClick1: ");
}
});
//java lamdba
findViewById(R.id.af_b_btn2).setOnClickListener(v -> Log.d(TAG, "onClick2: "));
//kt 匿名函数
af_b_btn1.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
Log.d(TAG, "onClick1: ")
}
})
//kt lamdba
af_b_btn1.setOnClickListener { Log.d(TAG, "onClick2: ") }
至少最开始我接触函数式编程的时候是这样的感觉的,非常排斥。看了很多相关文章,为啥这玩意这么火?各个语言都在用,java从8开始也引入这玩意,kotlin从开始都支持。 后来就逐渐接受,并喜欢上。
阅读性差:所谓的阅读性差,个人感觉可能是java代码写多了,更加注重“过程”。为什么这么说?比如上面的点击事件,看java的匿名方式,我知道参数(View)、处理过程(这里其实就是简单回调方法)、以及返回参数(void)。可谓标准的一个java方法。其实可以再简单点,我们只需要关注开始和结束,简化中间过程,简单化。
举个例子,在一个字符串中过滤出可转化为int类型的再转化为int类然后过滤出>3,最后求和。下面是采用原始的java自己写算法和采用kt的内置函数式去当作数据流处理(jdk8开始java也有对应数据流)。用标准的数据流处理方,可以看出函数式有更加清晰的特点。
//java sum
findViewById(R.id.af_b_btn3).setOnClickListener(v -> {
String[] list = new String[]{"1", "2", "a", "..", "3", "--", "4", "5", "6"};
int sum = 0;
for (String s : list) {
try {
int i = Integer.parseInt(s);
if (i <= 3) continue;
sum += i;
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
Log.d(TAG, "initListener: " + sum);
});
//kt fun
af_b_btn3.setOnClickListener {
val list = listOf("1", "2", "a", "..", "3", "--", "4", "5", "6")
val sum = list.filter { isInt(it) }.map { it.toInt() }.filter { it > 3 }.sum()
Log.d(TAG, ": $sum")
}
private fun isInt(it: String): Boolean {
return try {
it.toInt()
true
} catch (e: NumberFormatException) {
e.printStackTrace()
false
}
}
有接触过后端数据库的会知道查询select * from user where id="xxx"
。开发者只用采用特定语句告诉数据库“我想要查询用户xxx的信息,具体你怎么查(具体查询算法细节)我不管”。其实调用后端的API也是类似,只需要给出指定API及参数,就可以获取到对应信息,简化或者忽略中间过程。
kt有更加彻底的函数式编程。用过Rxjava的都知道,如果不采用lamdba,写这玩意会崩溃,各种回调,能写很多很多模板代码。可能因为历史原因,java的lamdba比较繁琐一点,相对而言kt会简单很多。看下面功能一样并且都采用lamdba的例子:
@SuppressLint("CheckResult")
private void sumFun() {
String[] list = new String[]{"1", "2", "a", "..", "3", "--", "4", "5", "6"};
List strings = Arrays.asList(list);
Observable
.fromIterable(strings).filter(s -> isInt(s))
.map(Integer::parseInt)
.filter(integer -> integer > 3)
.toList()
.map(integers -> sum(integers))
.flatMapObservable((Function>) integer ->
Observable.just(integer))
.subscribe(
it -> Log.d(TAG, ": " + it),
t -> Log.e(TAG, ": " + t.getLocalizedMessage()),
() -> Log.d(TAG, ":compelt ")
);
}
//kt
@SuppressLint("CheckResult")
private fun sumFun() {
val list = listOf("1", "2", "a", "..", "3", "--", "4", "5", "6")
list
.toObservable()
.filter { isInt(it) }
.map { it.toInt() }
.filter { it > 3 }
.toList()
.map { it.sum() }
.flatMapObservable { Observable.just(it) }
.subscribe(
{ Log.d(TAG, ": $it") },
{ Log.e(TAG, ": " + it.localizedMessage) },
{ Log.d(TAG, ":compelt ")
})
}
未完