Kotlin的其他技术
目录
一、解构声明
二、区间
三、类型检查与转换
四、this表达式
五、相等性
六、操作符重载
七、空安全
八、异常
九、类型别名
一、解构声明
解构声明能同时创建多个变量,将对象中的数据解析成相对的变量。举个例子:
//创建一个数据类User
data class User(var name: String, var age: Int)
//获得User的实例
var user = User("Czh", 22)
//声明变量 name 和 age
var (name, age) = user
println("name:$name age:$age")
//输出结果为:name:Czh age:22
上面代码中用解构声明同时创建两个变量的时候,会被编译成以下代码:
//指定变量name的值为user第一个参数的值
var name = user.component1()
//指定变量name的值为user第二个参数的值
var age = user.component2()
println("name:$name age:$age")
//输出结果为:name:Czh age:22
- 解构声明和Map
Map可以保存一组key-value键值对,通过解构声明可以把这些值解构出来。如下所示:
var map = mutableMapOf<String, Any>()
map.put("name", "Czh")
map.put("age", 22)
for ((key, value) in map) {
println("$key:$value")
}
运行代码,输出结果:二、区间
1.in
假如现在要判断 i 是否在 1-5 内,可以这样写:
if (i in 1..5) {
println("i 在 1-5 内")
}
上面代码中,1..5
指的是 1-5,in
指的是在...范围内,如果 i 在范围 1-5 之内,将会执行后面的代码块,输出结果。如果想判断 i 是否不在 1-5 内,可以这样写:
//!in表示不在...范围内
if (i !in 1..5) {
println("i 不在 1-5 内")
}
上面两段代码等同于:
if (i >= 1 && i <= 5) {
println("i 在 1-5 内")
}
if (i <= 1 && i >= 5) {
println("i 不在 1-5 内")
}
2.downTo
如果想输出 1-5 ,可以这样写:
for (i in 1..5) println(i)
//输出12345
如果倒着来:
for (i in 5..1) println(i)
//什么也不输出
这个时候可以用downTo
函数倒序输出 5-1
for (i in 5 downTo 1) println(i)
3.step
上面的代码顺序输出12345或倒序54321,按顺序+1或者-1,也就是步长为1。如果要修改步长,可以用step
函数,如下所示:
for (i in 1..5 step 2) println(i)
//输出135
//倒序
for (i in 1 downTo 5 step 2) println(i)
//输出531
4.until
上面的代码中,使用的范围都是闭区间,例如1..5
的区间是[1,5],如果要创建一个不包括其结束元素的区间,即区间是[1,5),可以使用until
函数,如下所示:
for (i in 1 until 5) println(i)
//输出1234
三、类型检查与转换
1.is操作符
在Kotlin中,可以通过is
操作符判断一个对象与指定的类型是否一致,还可以使用is
操作符的否定形式!is
,举个例子:
var a: Any = "a"
if (a is String) {
println("a是String类型")
}
if (a !is Int) {
println("a不是Int类型")
}
运行代码,输出结果为:2.智能转换
在Kotlin中不必使用显式类型转换操作,因为编译器会跟踪不可变值的is
检查以及显式转换,并在需要时自动插入(安全的)转换。举个例子:
var a: Any = "a"
if (a is String) {
println("a是String类型")
println(a.length) // a 自动转换为String类型
//输出结果为:1
}
还可以反向检查,如下所示:
if (a !is String) return
print(a.length) // a 自动转换为String类型
在 && 和 || 的右侧也可以智能转换:
// `&&` 右侧的 a 自动转换为String
if (a is String && a.length > 0)
// `||` 右侧的 a 自动转换为String
if (a is String || a.length > 0)
在when表达式和while循环里也能智能转换:
when(a){
is String -> a.length
is Int -> a + 1
}
需要注意的是,当编译器不能保证变量在检查和使用之间不可改变时,智能转换不能用。智能转换能否适用根据以下规则:
- val 局部变量——总是可以,局部委托属性除外;
- val 属性——如果属性是 private 或 internal,或者该检查在声明属性的同一模块中执行。智能转换不适用于 open 的属性或者具有自定义 getter 的属性;
- var 局部变量——如果变量在检查和使用之间没有修改、没有在会修改它的 lambda 中捕获、并且不是局部委托属性;
- var 属性——决不可能(因为该变量可以随时被其他代码修改)
3.强制类型转换
在Kotlin中,用操作符as
进行强制类型转换,如下所示:
var any: Any = "abc"
var str: String = any as String
但强制类型转换是不安全的,如果类型不兼容,会抛出一个异常,如下所示:
var int: Int = 123
var str: String = int as String
//抛出ClassCastException
4.可空转换操作符
null
不能转换为 String
,因该类型不是可空的。举个例子:
var str = null
var str2 = str as String
//抛出TypeCastException
解决这个问题可以使用可空转换操作符as?
,如下所示:
var str = null
var str2 = str as? String
println(str2) //输出结果为:null
使用安全转换操作符as?
可以在转换失败时返回null
,避免了抛出异常。
四、this表达式
为了表示当前的接收者我们使用this表达式。当this
在类的成员中,this
指的是该类的当前对象;当this
在扩展函数或者带接收者的函数字面值中,this
表示在点左侧传递的接收者参数。
- 限定的this
如果this
没有限定符,它指的是最内层的包含它的作用域。如果要访问来自外部作用域的this
(一个类或者扩展函数, 或者带标签的带接收者的函数字面值)我们使用this@label
,其中@label
是一个代指this
来源的标签。举个例子:
class A { // 隐式标签 @A
inner class B { // 隐式标签 @B
fun Int.foo() { // 隐式标签 @foo
val a = this@A // A 的 this
val b = this@B // B 的 this
val c = this // foo() 的接收者,一个 Int
val c1 = this@foo // foo() 的接收者,一个 Int
val funLit = lambda@ fun String.() {
val d = this // funLit 的接收者
}
val funLit2 = { s: String ->
// foo() 的接收者,因为它包含的 lambda 表达式
// 没有任何接收者
val d1 = this
}
}
}
}
五、相等性
在Kotlin中存在结构相等和引用相等两中相等判断。
1.结构相等
使用equals()
或==
判断,如下所示:
var a = "1"
var b = "1"
if (a.equals(b)) {
println("a 和 b 结构相等")
//输出结果为:a 和 b 结构相等
}
var a = 1
var b = 1
if (a == b) {
println("a 和 b 结构相等")
//输出结果为:a 和 b 结构相等
}
2.引用相等
引用相等指两个引用指向同一对象,用===
判断,如下所示:
data class User(var name: String, var age: Int)
var a = User("Czh", 22)
var b = User("Czh", 22)
var c = b
var d = a
if (c == d) {
println("a 和 b 结构相等")
} else {
println("a 和 b 结构不相等")
}
if (c === d) {
println("a 和 b 引用相等")
} else {
println("a 和 b 引用不相等")
}
运行代码,输出结果为:六、操作符重载
Kotlin允许对自己的类型提供预定义的一组操作符的实现,这些操作符具有固定的符号表示 (如 +
或 *
)和固定的优先级。为实现这样的操作符,我们为相应的类型(即二元操作符左侧的类型和一元操作符的参数类型)提供了一个固定名字的成员函数或扩展函数。 重载操作符的函数需要用 operator
修饰符标记。
重载操作符
+
是一个一元操作符,下面来对一元操作符进行重载:
//用 operator 修饰符标记
operator fun String.unaryPlus(): String {
return this + this
}
//调用
var a = "a"
println(+a) //输出结果为:aa
当编译器处理例如表达式 +a 时,它执行以下步骤:
- 确定 a 的类型,令其为 T;
- 为接收者 T 查找一个带有 operator 修饰符的无参函数 unaryPlus(),即成员函数或扩展函数;
- 如果函数不存在或不明确,则导致编译错误;
- 如果函数存在且其返回类型为 R,那就表达式 +a 具有类型 R;
除对一元操作符进行重载外,还可以对其他操作符进行重载,其重载方式和原理大致相同。下面来一一列举:
1.一元操作符
表达式 | 对应的函数 |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
a++ | a.inc() |
a-- | a.dec() |
2.二元操作符
表达式 | 对应的函数 |
---|---|
a+b | a.plus(b) |
a-b | a.minus(b) |
a*b | a.times(b) |
a/b | a.div(b) |
a%b | a.mod(b) |
a..b | a.rangeTo(b) |
3.in操作符
表达式 | 对应的函数 |
---|---|
a in b | b.contains(a) |
a !in b | !b.contains(a) |
4.索引访问操作符
表达式 | 对应的函数 |
---|---|
a[i] | a.get(i) |
a[i, j] | a.get(i, j) |
a[i_1, ……, i_n] | a.get(i_1, ……, i_n) |
a[i] = b | a.set(i, b) |
a[i, j] = b | a.set(i, j, b) |
a[i_1, ……, i_n] = b | a.set(i_1, ……, i_n, b) |
5.调用操作符
表达式 | 对应的函数 |
---|---|
a() | a.invoke() |
a(i) | a.invoke(i) |
a(i, j) | a.invoke(i, j) |
a(i_1, ……, i_n) | a.invoke(i_1, ……, i_n) |
6.广义赋值
表达式 | 对应的函数 |
---|---|
a += b | a.plusAssign(b) |
a -= b | a.minusAssign(b) |
a *= b | a.timesAssign(b) |
a /= b | a.divAssign(b) |
a %= b | a.remAssign(b), a.modAssign(b)(已弃用) |
7.相等与不等操作符
表达式 | 对应的函数 |
---|---|
a == b | a?.equals(b) ?: (b === null) |
a != b | !(a?.equals(b) ?: (b === null)) |
8.比较操作符
表达式 | 对应的函数 |
---|---|
a > b | a.compareTo(b) > 0 |
a < b | a.compareTo(b) < 0 |
a >= b | a.compareTo(b) >= 0 |
a <= b | a.compareTo(b) <= 0 |
七、空安全
在Java中,NullPointerException 可能是最常见的异常之一,而Kotlin的类型系统旨在消除来自代码空引用的危险。
1.可空类型与非空类型
在Kotlin中,只有下列情况可能导致出现NullPointerException:
- 显式调用 throw NullPointerException();
- 使用了下文描述的 !! 操作符;
- 有些数据在初始化时不一致;
- 外部 Java 代码引发的问题。
在 Kotlin 中,类型系统区分一个引用可以容纳 null (可空引用)还是不能容纳(非空引用)。 例如,String 类型的常规变量不能容纳 null:
如果要允许为空,我们可以声明一个变量为可空字符串,在字符串类型后面加一个问号
?
,写作 String?
,如下所示:
var b: String? = "b"
b = null
2.安全调用操作符
接着上面的代码,如果你调用a
的方法或者访问它的属性,不会出现NullPointerException,但如果调用b
的方法或者访问它的属性,编译器会报告一个错误,如下所示:
这个时候可以使用安全调用操作符,写作
?.
,在b
后面加安全调用操作符,表示如果b
不为null则调用b.length
,如下所示:
b?.length
安全调用操作符还能链式调用,例如一个员工 Bob 可能会(或者不会)分配给一个部门, 并且可能有另外一个员工是该部门的负责人,那么获取 Bob 所在部门负责人(如果有的话)的名字,我们写作:
Bob?.department?.head?.name
//如果Bob分配给一个部门
//执行Bob.department.head?获取该部门的负责人
//如果该部门有一个负责人
//执行Bob.department.head.name获取该负责人的名字
如果该链式调用中任何一个属性为null,整个表达式都会返回null。
如果要只对非空值执行某个操作,安全调用操作符可以与let
一起使用,如下所示:
val listWithNulls: List<String?> = listOf("A", null, "B")
for (item in listWithNulls) {
item?.let { println(it) }
}
运行代码,输出结果为:- 安全的类型转换
如果对象不是目标类型,那么常规类型转换可能会导致ClassCastException
。 另一个选择是使用安全的类型转换,如果尝试转换不成功则返回null,如下所示:
val i: Int? = i as? Int
- 可空类型的集合
如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用filterNotNull
来实现。如下所示:
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
3.Elvis 操作符
先看一段代码:
val i: Int = if (b != null) b.length else -1
val i = b?.length ?: -1
这两行代码表达的都是“如果b
不等于null,i = b.length
;如果b
等于null,i = -1
”。第一行代码用的是if
表达式,而第二行代码使用了Elvis
操作符,写作?:
。Elvis
操作符表示如果?:
左侧表达式非空,就使用左侧表达式,否则使用右侧表达式。
请注意,因为throw
和return
在Kotlin中都是表达式,所以它们也可以用在Elvis
操作符右侧。如下所示:
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ……
}
4. !! 操作符
!!
操作符将任何值转换为非空类型,若该值为空则抛出异常。如下所示:
var a = null
a!!
//运行代码,抛出KotlinNullPointerException
八、异常
Kotlin中所有异常类都是Throwable
类的子类。每个异常都有消息、堆栈回溯信息和可选的原因。
使用throw
表达式可以抛出异常。举个例子:
throw NullPointerException("NPE")
使用try
表达式可以捕获异常。一个try
表达式可以有多个catch
代码段;finally
代码段可以省略。举个例子:
try {
//捕获异常
} catch (e: NullPointerException) {
//异常处理
} catch (e: ClassNotFoundException) {
//异常处理
} finally {
//可选的finally代码段
}
因为Try
是一个表达式,所以它可以有一个返回值。举个例子:
val a: Int? = try {
parseInt(input)
} catch (e: NumberFormatException) {
null
}
try
表达式的返回值是 try
块中的最后一个表达式或者是catch
块中的最后一个表达式。finally
块中的内容不会影响表达式的结果。
九、类型别名
Kotlin提供类型别名来代替过长的类型名称,这些类型别名不会引入新类型,且等效于相应的底层类型。可以通过使用关键字typealias
修改类型别名,如下所示:
//使用关键字typealias修改类型别名Length
//相当于 Length 就是一个 (String) -> Int 类型
typealias Length = (String) -> Int
//调用
fun getLength(l: Length) = l("Czh")
//编译器把 Length 扩展为 (String) -> Int 类型
val l: Length = { it.length }
println(getLength(l)) //输出结果为:3
使用类型别名能让那些看起来很长的类型在使用起来变得简洁,如下所示:
typealias MyType = (String, Int, Any, MutableList<String> ) -> Unit
//当我们使用的时候
var myType:MyType
//而不需要写他原来的类型
//var myType:(String, Int, Any, MutableList<String> ) -> Unit
总结
相对于Java来说,Kotlin有很多新的技术和语法糖,这也是为什么使用Kotlin来开发Android要优于Java。运用好这些新的东西,能大大加快开发速度。
参考文献:
Kotlin语言中文站、《Kotlin程序开发入门精要》
推荐阅读:
从Java到Kotlin(一)为什么使用Kotlin
从Java到Kotlin(二)基本语法
从Java到Kotlin(三)类和接口
从Java到Kotlin(四)对象与泛型
从Java到Kotlin(五)函数与Lambda表达式
从Java到Kotlin(六)扩展与委托
从Java到Kotlin(七)反射和注解
Kotlin学习资料汇总
更多精彩文章请扫描下方二维码关注微信公众号"AndroidCzh":这里将长期为您分享原创文章、Android开发经验等!
QQ交流群: 705929135