从Java到Kotlin(八)

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操作符表示如果?:左侧表达式非空,就使用左侧表达式,否则使用右侧表达式。
请注意,因为throwreturn在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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容