1. Kotlin 作用域函数

如果同学们已经在项目中用过 Kotlin 语言,那么一定见过 let 函数!因为每当 Kotlin 检测到某个对象可能为空时,会自动帮我们修改为用 let 函数实现:user.name?.let{ textView.text = it }。这里的 let 函数就是 Kotlin 的作用域函数。除了 let,还有 run、with、apply、also 等等作用域函数。

作用域函数是 Kotlin 内置的,可对数据做一系列操作、变换的函数,与集合操作符类似,作用域函数不仅仅可被集合对象调用,它们还可以被所有对象调用。让我们来看看它们的用法。

let 和 run 函数类似,都会返回函数内闭包的结果,区别在于 let 有闭包参数,而 run 没有闭包参数。

        使用方法:

        let{ 闭包 }、run{ 闭包 },

        有闭包参数意思是 let 在闭包中可以通过 it 拿到它自己本身;

        而 run 就不行了,只能通过 this 关键字拿到它本身。

看 code 1 例子。

// code 1
data class Car(
    val brand: String,
    val price: Int
)

var car: Car? = null
fun funcExample() {
    car = Car("红旗", 199999)
    // let 闭包里可用 it 访问调用者,可返回闭包的执行结果
    val carBrand = car?.let { "car's brand is ${it.brand}" }
    println(carBrand)
    // run 闭包用 this 访问调用者
    val carPrice = car?.run { "car's price is ${this.price}" }
    println(carPrice)
}

also和 apply 函数不会返回闭包里的结果,而上述的 let 和 run 是可以返回闭包结果的。

        also和 apply 函数的区别也是在于有无闭包参数:

                also 有闭包参数;

                apply 没有闭包参数。

        但是它们都会返回调用者对象,所以它们支持链式调用。

// code 2
// also 闭包里可用 it 访问调用者,后面可链式调用
car?.also {
    println("car 贴牌前 brand = ${it.brand}")
}?.brand = "比亚迪"
// apply 闭包用 this 访问调用者,后面也可链式调用
car?.apply {
    println("car 贴牌后 brand = ${this.brand}")
}?.apply {
    println("car's price = ${this.price}")
}

takeIf 和 takeUnless 这两个作用域函数就用的相对较少了。takeIf 函数里的闭包返回的是 Boolean 类型,如果闭包条件满足,则返回调用者本身,如果不满足,则返回 null。举个栗子来说明吧。

// code 3
car?.takeIf { it.price > 1500000 }
    ?.also { println("车太贵啦!") }    // 闭包为 true,则不为空,执行 also 函数闭包
    ?: run { println("价格还行!") }    // 闭包为 false,则返回空,执行 run 函数闭包

takeUnless 跟 takeIf 是相反的关系,takeUnless 的闭包条件满足则返回空,不满足则返回调用者自己。

repeat 函数

        调用方法:repeat( times ) { 闭包 }

        将闭包的操作执行 times 次。闭包里面的 it 是当前执行的循环次数,从 0 开始计数。

// code 4
repeat(3) {
        println("car' brand is ${car?.brand}, price is ${car?.price} 当前执行次数为:$it")
}

//执行结果:
//car' brand is 比亚迪, price is 199999 当前执行索引为:0
//car' brand is 比亚迪, price is 199999 当前执行索引为:1
//car' brand is 比亚迪, price is 199999 当前执行索引为:2

with 函数

        调用方法:with( T ){ 闭包 }

        将对象 T 去执行闭包里的操作,通常在 Android 开发中,需要对一个 TextView 赋值时,就可以使用 with,比较方便:

// code 5
with(textView) {
    text = "测试"
    textSize = 20F
    setTextColor(ContextCompat.getColor(context, R.color.purple_200))
}

2. Kotlin 自定义操作符

学习 Kotlin 一段时间后,你会发现 Kotlin 给了开发者很大的自我发展空间。比如:支持对类新增扩展函数,支持运算符重载等。所以,我们自己也可以自定义一些操作符,来方便开发。看过 Kotlin 自带的操作符实现的同学们会发现,这些函数都是 inline 关键字修饰的。我们先看下 inline 关键字。

inline 关键字,可以看做是一个是否 内联 的标记。被修饰的函数会在编译时,直接把函数体一起“拷贝”过去,就是将内联函数的代码直接放在内联函数的位置上,这与一般函数不同,在调用一般函数的时候,是指令跳转到被调用函数的入口地址,执行完被调用函数后,指令再跳转回原来跳进来的地方继续执行后面的代码;而由于内联函数是将函数的代码直接放在了函数的位置上,所以没有指令跳转,指令按顺序执行。这样做可以加快代码的运行速度,但是会增加编译时间以及编译后的代码量。

inline 关键字适合修饰不太复杂的但会频繁调用的函数。所以 Kotlin 自带的操作符都是 inline 函数,我们如果要自定义一个操作符,也是需要修饰为 inline 函数。如下就是自定义了一个 convert 操作符,功能类似集合中的 map 函数。

// code 6
inline fun <T, E> Iterable<T>.convert(action: (T) -> E): Iterable<E> {
    val list: MutableList<E> = mutableListOf()
    for (item in this) list.add(action(item))
    return list
}

3. Kotlin 中反引号 ` 的用法

反引号还有一个作用,就是在 Kotlin 代码中将一个不合法的字符转变为合法字符。举个栗子:

// code 7
object SmallTips {
    // 反引号可将非法字符转换为合法字符
    fun `123`(){
        println("函数名居然为`123`!")
    }
    fun ` `(){
        println("函数名居然为` `!")
    }
    fun `  `(){
        println("函数名居然为`  `!")
    }
}

// 调用也需将反引号加上
SmallTips.`123`()
SmallTips.` `()
SmallTips.`  `()

不可思议吧!函数名本来不能为纯数字或空格符,但是加上反引号就可以了!神奇!那么这有啥用?还记得 Kotlin 的 internal 访问修饰符吗?它限定了被它修饰的函数只能在当前模块使用,而不能在其他模块使用。但是 Java 中是没有这个修饰符的,而 Kotlin 和 Java 又必须完全兼容,所以 Java 也不得不支持这一特性。

那么问题来了,通过反编译查看 Kotlin 中 internal 修饰的函数,在生成的 Java 代码里被编译成了 public 修饰的函数。为了让 Java 不能访问 Kotlin 中的函数,可以在 Kotlin 中将这些函数的命名改为不合法的形式,然后用反引号包起来,这么做之后,Java 代码是不能调用这些方法的,而 Kotlin 可以调用,从而可以实现在 Java 中屏蔽某些 Kotlin 函数的效果。最后,这种反引号的用法不推荐使用!了解即可!

4. Kotlin 对象比较

在 Java 中,要比较两个对象是否相等,通常用的是 == 或 equals 方法。

Java 中的 == 运算符比较的是两个对象本身的值,即两个对象在内存中的首地址。如果是两个字符串,就是比较的这两个字符串存储的地址是否是同一个。

Java 中,对象的首地址是它在内存中存放的起始地址,它后面的地址是用来存放它所包含的各个属性的地址,所以内存中会用多个内存块来存放对象的各个属性值,而通过这个首地址就可以找到该对象,进而可以找到该对象的各个属性。

Java 中的 equals 方法比较的是两个对象中各个属性值的是否相同。如果是两个字符串,就是比较的两字符串所包含的内容是否相同。

在 Kotlin 语言中,判断两个对象是否相等用的是 == 和 ===。没错,两个等号和三个等号。

Kotlin 的 == 相当于 Java 中的 equals 方法;

Kotlin 的=== 相当于 Java 中的 == 运算符,记住即可。栗子也有,看下方:

// code 8
val str1 = java.lang.String("我发")
val str2 = java.lang.String("我发")
println("str1 == str2 结果为 ${str1 == str2}") // 输出:str1 == str2 结果为 true
println("str1 === str2 结果为 ${str1 === str2}") // 输出:str1 === str2 结果为 false
//示列1   
fun main(args: Array<String>) {
	val a : Int = 1000
	println(a == a) //true 单纯比较数值大小
	println(a === a) //true //同一对象
	val a1 : Int = a 
	val a2 : Int = a 
	println(a1 == a2) //true 数值大小
	println(a1 === a2) //true  同一对象和数值比较
}
//示列2
fun main(args: Array<String>) {
	val a : Int = 1000
	println(a == a) //true
	println(a === a) //true
	val a1 : Int? = a
	val a2 : Int? = a
	println(a1 == a2) //true
	println(a1 === a2) //false 之所以为false,因为经过了装箱
}
//示列3
fun main(args: Array<String>) {
	val a : Int? = 1000
	println(a == a) //true
	println(a === a) //true
	val a1 : Int? = a    //a经过装箱之后本身已经是一个对象此时a是一个对象
	val a2 : Int? = a
	println(a1 == a2) //true
	println(a1 === a2) //true  此时a1 a2 同时指向了同一个对象a
}
//示列4
fun main(args: Array<String>) {
	val a : Int = 100  //和上面不一样,a的值发生了变化
	println(a == a) //true
	println(a === a) //true
	val a1 : Int? = a
	val a2 : Int? = a
	println(a1 == a2) //true
	println(a1 === a2) //true  之所以不变是因为和java一样,[-128,127]之间的数装箱并不会产生创建新的对象。
}

因为在 Kotlin 的 String 构造方法中,不能直接传入一个字符串,所以这里用的是 Java 中的 String 类进行的初始化。也可以用 Kotlin 的 String 另外两种初始化方法:

1)val str1 = StringBuilder("我发").toString();

2)val str1 = String("我发".toByteArray())。

5. Kotlin 的常量变量

        Kotlin 有两种变量:

                 val 关键字修饰的不可变的变量;

                var 关键字修饰的可变的变量。

如何在类中对这两种变量进行初始化呢?val 因为是不可变,所以只能重写变量的 getter 方法,var 则可以重写 getter 和 setter 方法,当然类会自动帮我们生成 getter 和 setter 方法。

// code 9
class Person {    // 此类无实际意义,为了举个栗子而已
    var age: Int = 0 // 可变变量我们可以重写 getter 和 setter
        get() {
            return field.plus(10)
        }
        set(value) {
            field = value - 1
        }

    val name: String = "" // 不可变变量我们只能重写 getter 方法
        get() {
            return field + "haha"
        }

    val height: Float // height 是用 val 修饰的,但 height 并不是一个常量
        get() {
            return (age * 2F + 10)
        }
}

在重写 getter 和 setter 方法时,可以通过 field 拿到该属性的值。val 和 var 最本质的区别就是,val 没有 setter 方法。val 并不是常量,而是说,我们不能再次对 val 变量进行赋值操作!为啥 val 修饰的并不是常量?可以看一下 code 9 中的 height 变量,当 age 变化时,height 也会变化,它并不是一个常量。

如果要声明一个常量,则要用到 const 关键字。它有两个注意点:

1)const 只能修饰 object 的属性,或 top-level 变量。

2)const 变量的值必须在编译期间就确定下来,所以类型只能是 String 或基本数据类型。

Kotlin 中用 const 修饰的常量类似于 Java 中的一个不可变的静态变量。它声明的地方只有三种

        1. object 类的内部,object 修饰的都是静态类;
        2. top-level 位置,也就是在一个类的外部进行声明;
        3. companion object 内部,也就是用于声明静态变量的位置。

// code 10
object ValAndVarExample {
    const val t2 = "heiheihei"
}

const val t1 = "hahaha" // top-level,类外

class Person {
    companion object{
        const val t3 = "hehehe"
    }
}

6. Kotlin 的 inline、crossinline、noinline 关键字的特殊使用

在前面的第2节 Kotlin 的自定义操作符中,已经说明了 inline 关键字的基本用法,知道了内联函数可以通过直接将代码拷贝到调用的地方从而加快程序执行速度的特性。除了 inline 关键字外,还有 crossinline 和 noinline 两个关键字,来看看它们还有什么特殊的用法。

在讲之前,还是需要明白一些前提知识。inline 关键字既会影响函数对象本身,也会影响传入的 Lambda 闭包参数,两者都会被内联到调用点。

编译预处理器会对内联函数进行扩展,省去了参数压栈、生成汇编语言的 CALL 调用、返回参数、执行 return 等过程,从而提高运行速度。优点是,在函数被内联后编译器可以通过上下文相关的优化技术对结果代码执行更深入的优化;但会使得编译后的代码体积变大,只是省去了函数调用的开销。所以 inline 适合用于较简单的频繁调用的函数。

6.1. 被 inline 修饰的函数中的 Lambda 表达式,可以中断外部函数的调用。
// code 11
fun main(args: Array<String>) {
    test1 {
        println("我要好好学 Kotlin")
        return
    }
    println("我要好好学习 Android")
}

inline fun test1(lambda: () -> Unit) {
    lambda.invoke()
}
// 输出:我要好好学 Kotlin

test1 函数被 inline 修饰,它有个 Lambda 闭包,在该闭包中有个 return 返回函数,这个函数可以中断外部的 main 函数,所以只会输出 “我要好好学 Kotlin”。

通常情况下,Kotlin 中函数内部 Lambda 闭包是不能中断外部函数的执行的,可以尝试下将 code 11 中 test1 修饰的 inline 去掉,此时编译器就会提示 return 只能写成 return@test1,即只能返回 test1 函数,并不能返回 main 函数。

6.2. crossinline 关键字不允许被 inline 修饰的函数中的 Lambda 表达式中断外部函数的执行。

意思就是,在 code 11 中,如果 Lambda 表达式的 return 只是想中断该闭包的执行,而不想中断外部 main 函数的执行,该咋办?有人会说,那我不用 inline 不就可以了?但这里又需要用 inline 呢?那就可以使用 crossinline 去修饰这个 Lambda 闭包,编译器就不会去对这个 Lambda 表达式做内联操作。

// code 12
fun main(args: Array<String>) {
    test1 {
        println("我要好好学 Kotlin")
        return@test1
        println("我不想学习了~")
    }
    println("我要好好学习 Android")
} 

inline fun test1(crossinline lambda: () -> Unit) {
    lambda.invoke()
}
// 输出:
//我要好好学 Kotlin
//我要好好学习 Android
6.3. noinline 关键字不允许被 inline 修饰的函数中的 Lambda 表达式被内联处理。

首先,noinline 关键字是作用于 Lambda 闭包的;其次,它是用于在修饰了 inline 关键字的函数中,剔除 inline 关键字对 Lambda 闭包的影响,让它就作为一个普通的 Lambda 闭包。说明不够,代码来凑!

// code 13
inline fun test2(lambda0: () -> Unit, noinline lambda1: () -> Unit): () -> Unit {
    lambda0.invoke()
    lambda1.invoke()
    return lambda1
}

test2 函数被 inline 修饰,有两个 Lambda 闭包作为参数,而且它的返回值也是一个 Lambda 闭包。如果 lambda1 没有 noinline 关键字修饰,那么它就会跟 lambda0 一样,将函数体直接拷贝到调用的地方,这种情况下,lambda1 就不能作为闭包返回了,所以去掉 noinline 之后,code 13 代码会报错。所以,这里如果要将 test2 用 inline 修饰,同时,又想返回一个闭包的话,就可以用 noinline 关键字去除 inline 对闭包的影响。

上面说的都是关于 inline 关键字的进阶用法,通常情况下不会用到,作为知识储备即可。

7、基础语法快速掌握

1 数据类型
Kotlin 中我们常使用的类型跟Java基本一样,包括: Byte、Short、Int、Long、Float、Double、Char、String。跟Java区别于,Java中区分装箱类型和基本类型(像int和Integer、long和Long),而在Kotlin中不区分装箱类型和基本类型,它内部会根据实际情况自动转换,开发者无需关心是否需要使用大写还是小写,统一首字母大写。

补充和注意:

长整型的数值

在Java中,长整型的赋值后面是数字+大写或小写的L,而Kotlin中,语言设计者认为小写l容易造成跟数字1混淆,所以在Kotlin中只允许使用大写L。如:

var a:Long = 123L

禁止隐式转换在Java中,可以将一个int值直接赋值给long作隐式转换,而在Kotlin中,语言设计者认为,类型转换是危险的,真的需要就要明确进行。如:

var i = 1
var a:Long = i.toLong()

无符号类型

在Java中,不存在无符号类型,而在Kotlin中跟C一样,是可以有无符号类型的,如:UByte、UShort、UInt、ULong等,其数值需要在数值后加u,便是无符号类型,它的取值范围就是普通数值类型正数部分。如:

var a:ULong = 123u

字符串判断两个等号和三个等号

在Java中,使用==来判断两个字符串是判断其引用值,而的Kotlin中,==和String.equals等价,是值判断,===才是引用判断。

字符串模板在Java中,连接两个字符串一般使用+,而在Kotlin中,同样可以使用+,但是还可以使用字符串模板。如:

var a = "World"
var b = "Hello ${a}"

字符串三个引号情况

在Java中没有三个引用的语法,在Kotlin中,使用三个引号引用的字符串,可其进行换行和缩进,一般还配合trimIndent方法一同使用,表示裁剪字符串内容前面的空格(前导空格)。如:

var a = """
<html>
    <body>
        ...
    </body>
</html>
""".trimIndent()

//字符串最终结果:
<html>
    <body>
        ...
    </body>
</html>

还可以配合 trimMargin 方法一同使用,表示移除字符串每行前面的空格。如

var a = """    
<html>
    |<body>
        |...
    |</body>
</html>
""".trimMargin()        // 无参默认边界字符是"|"
 
var b = """    
<html>
    %<body>
        %...
    %</body>
</html>
""".trimMargin("%")    // 参数"%"表示边界字符
//字符串最终结果:
<html>
<body>
...
</body>
</html>

2 变量和常量的声明
在Kotlin中,当使用到变量和常量的声明时,一般使用关键字“var”来声明变量,使用“val”来声明“常量”,后面可带数据类型或省略由编译器来推断。说明:为什么上面那句话中“常量”需要加双引号?因为用val声明的“常量”在Kotlin中一般叫作只读变量,或者也可以叫作运行时常量。它并非真正意义上的常量。它们的区别在于:像Java的常量是编译期常量,编译机在编译时已经知道它是常量,并会把它的引用全部替换成它的值,而val声明的变量虽然是不可变,但它实质上还是一个变量,所以在运行时的时候,还是可以使用反射将它的值进行改变的。如果你一定要使用像Java中加final声明的常量的话,就要在其前面加const。它们的使用如:

var name1:String = "子云心"              // 变量
var name2 = "子云心"             // 不写类型即编辑器自动推断类型
val name3 = "子云心"              // 只读变量,或者叫运行时常量
const val name4 = "子云心"            // 常量(编译期常量),等价于Java中的 static final String name4 = "子云心"

注意:使用const声明常量只适配全局范围、只能修饰基本类型、必须立即用字面量初始化。所以我们在一般情况下想使用常量使用val即可。3 数组、区间和集合
3.1 数组
数组的使用可以使用Array类型声明。如:

var array1: Array<String> = arrayOf("Hello", "World")
var array2: Array<Char> = arrayOf('H','e','l','l','o')
var array3: Array<Int> = arrayOf(1,2,3)

设计者为了避免装箱和折箱,所以又为基本类型定义了一套。如:

var array2: CharArray = charArrayOf('H','e','l','l','o')
var array3: IntArray = intArrayOf(1,2,3)

上述array3变量又可以写以这样,IntArray类的构造方法接收数组的大小,it的值为数组下标:

var array3 = IntArray(3) { it + 1 }

补充:

joinToString方法

使用joinToString方法可让数组所有元素连接成一个字符串,使用如:

println(array2.joinToString())     // 输出结果:H, e, l, l, o
println(array2.joinToString(""))   // 输出结果:Hello

判断元素是否存在

判断某个元素是否存在于数组中,已经不需要像Java那样在循环中进行每个判断的判断,可以直接使用contains方法或关键字in、!in便可。如:

var result1: Boolean = 'w' !in array2
var result2: Boolean = 1 in array3
var result3: Boolean = array3.contains(1)

循环中使用indices关键字

数组循环中使用indice属性可以像Java中的for的i++用法,如:

for (i in array2.indices) {
    println(i)
}

循环中使用withIndex关键字

数组循环中使用withIndex属性可以同时输出下标和值,如:

for ((i, e) in array2.withIndex()) {
    println("${i} ${e} ")
}

3.2 区间
区间是一个数学上的概念,表示一段范围,在Kotlin中一般情况下使用两个点..、until、downTo等来表示。使用如:

var a = 0..100          // [0,100]
var b = 0 until 100     // [0,100),不包括100
var c = 0 downTo 100    // 倒序
var d = c.reversed()    // 创建一个基于原来倒序的区间
var e = 'a'..'z'        // 也适用于字母

补充:

判断元素是否存在和步长

//判断是否存在于区间内也是使用contains方法或使用关键字in
var result1: Boolean = 1 in a
var result2: Boolean = 'A' !in e
var result3: Boolean = e.contains('a')

//步长表示区间内的间隔
var e = 0..10 step 2    // 值是:0 2 4 6 8 10

3.3 集合
Java中的集合有:List<T>、Map<K,V>和Set<T>,而在Kotlin中将它们细分为可变集合和不可变集合。其中不可变集合依然是List<T>、Map<K,V>和Set<T>,而它们对应的可变集合是MutableList<T>、MutableMap<K,V>和MutableSet<T>。其使用如下:

val list1: List<Int> = listOf(1, 2, 3)                  // 不能添加和删除元素
val list2: MutableList<Int> = mutableListOf(1, 2, 3)    // 可以添加和删除元素
val list3 = ArrayList<Int>()                            // 可以添加和删除的空List
 
val map1: Map<Int, String> = mapOf(1 to "一", 2 to "二")  // to 理解为K-V
val map2: MutableMap<Int, String> = mutableMapOf(1 to "一", 2 to "二")
 
val set1: Set<Int> = setOf(1, 2, 3)
val set2: MutableSet<Int> = mutableSetOf(1, 2, 3)

补充:

//添加和删除元素

//添加和删除元素可以如果是List或Set的话,使用add和remove方法,又或者直接使用+=和-=运算符,如果是Map的话,使用push方法或可像数组的形式进行添加
list2.add(4)
list2.remove(3)
list2 += 5
list2 -= 2
map2.put(3, "三")
map2[4] = "四"

//读取和修改元素
//读取和修改元素跟数组类型的访问方式相同

val a = list2[0]
list2[0] = 9
map2[3] = "四"

Pari键值对

上面在Map的创建中传入的就是pari对象,它通过to关键字连接key和value值。使用如:

val pair1 = 1 to "一"           // 创建
var pair2 = Pair(2, "二")       // 不同的创建方式
val first = pair1.first         // 获得第一个元素
val second = pair1.second       // 获得第二个元素
val(x, y) = pair1               // 解构

//Triple 三个元素版本的Pair
val triple = Triple(1, "一", "壹")
val first = triple.first
val second = triple.second
val third = triple.third
val (x, y, z) = triple

有时候一个函数需要返回多个参数,那完全可以使用Pari或Triple来实现

4 空类型安全和智能类型转换
4.1 空类型安全
在Kotlin中,任意类型都有可空和不可空两种情况,在默认情况下定义的变量是不能为空的,若需要为空的情况,需要在类型后面加一个问号”?”,所以在Kotlin开发中减少了很多空指针崩溃的恶梦。我们来看看可空和不可空的定义:

val notNull:String = null               // 代码报错,编译不通过
val nullable:String? = null             // 正确的代码
 
val length1:Int = notNull.length        // 正确的代码
val length2:Int = nullable.length       // 代码报错,因为可能为空,不能直接获取长度
val length3:Int = nullable!!.length     // 正确的代码,表示认定一定不为空
val length4:Int? = nullable?.length     // 正确的代码,表示若为空,则返回空
val length5:Int = nullable?.length?:0   // 正确的代码,表示若为空,则返回0

注意:

关键符号!!、?.、?:

!!               表示我已明确清楚类型一定不为空,强制转成非空类型

?.               表示若不为空就返回后面属性,为空就返回空

?:               表示若不为空就返回本身,为空就返回后面语句(助记:像Java中的 a != null ? a : 0,但为了代码不冗余,把 “判空” 和 “本身” 省略,变成 a ? : 0)

不可空类型是可空类型的子类

我们通过里氏替换原则可以发现不可空和可空类型它们的关系是父类和子类的关系,如代码:

var x: String = "Hello"
var y: String? = "World"
x = y                       // 代码报错
y = x                       // 代码正确,因为String是String?的子类

平台类型

在Kotlin就一定不会出现空指针异常了吗?非也,当使用Kotlin去调用一段Java的代码类时还是会存在空指针的可能。如:

// Java code
public class JavaClass {
    public String getA() {
        return null;
    }
}
// Kotlin code
var javaClass = JavaClass()
var a: String = javaClass.getA()    // 发生异常
var b: String? = javaClass.getA()

上述代码中,javaClass的getA方法实质上返回的类型就叫平台类型,在IDE中会提示其类型是String!(实质上是不存在类型加感叹号出现的),因为它可能为空也可能不为空,所以程序在运行时才会发生空指针异常。

4.2 智能类型转换
在Kotlin中,类型的强转换可以使用as和as?关键字。如:

open class Parent()
class Son() : Parent(){
    fun sonFun(){ ... }
}
class Boy()
 
val son: Son = parent as Son    // 转换成功
val boy1:Boy = parent as Boy    // 转换失败,程序抛出异常
val boy2:Boy? = parent as? Boy  // 转换失改,返回null

//编译器推断类型

val parent: Parent = Son()
parent.sonFun()                 // 编译报错,因为parent中不存在sonFun方法
if (parent is Son) {
    parent.sonFun()             // 正确代码,因为已经进行过判断了,编译器已经推断了类型
}
parent.sonFun()                 // 编译报错,因为离开了智能转换的范围

5 条件、循环、异常捕获
5.1 条件
在Java中,条件语句就是if else、switch case以及三元运算符 ?a:b,下面就来看看Kotlin中对应的使用。

//if else还是一样的if else

var a:Int = 1
var b:String
 
// 跟Java一样,没特别
if (a == 1) {
    b = "true"
} else {
    b = "false"
}
 
// 表达式方式,像函数返回值一样(一行时大括号省略了),也就是Java中的三元运算符
b = if (a == 1) "true" else "false"
//使用when代替switch
when(a) {
    0-> b = "一"
    1-> b = "二"
    else -> b ="未知"
}
// 表达式,像函数返回值一样
b = when(a) {
    0-> "一"
    1-> "二"
    else -> "未知"
}
// 表达式,when后不跟括号和对象,条件直接写在每个分支中
var x: String? = "123"
b = when {
    x is String -> x.length.toString()
    x.isNullOrEmpty() -> "为空"
    else -> "未知"
}

5.2 循环
for循环

//for循环配合in关键字,使用如下:
var array: Array<Char> = arrayOf('H', 'e', 'l', 'l', 'o')
 
for(str in array) {
    println(str)
}
for ((i, v) in array.withIndex()) {
    println("${i} , ${v} ")
}
for (item in array.withIndex()) {               // item为每一项的下标和值
    println("${item.index},  ${item.value} ")
}
for (i in array.indices) {                      // i为每一项的下标
    println(i)
}

forEach

var array: Array<Char> = arrayOf('H', 'e', 'l', 'l', 'o')
array.forEach({ it -> println(it) })

while循环

跟Java的使用一样,不列举。

跳了多层循环

//在循环处声明一个名称用@连接,在break处指定跳出位置。如:

var array1: Array<Char> = arrayOf('H', 'e', 'l', 'l', 'o')
var array2: Array<Char> = arrayOf('W', 'o', 'r', 'l', 'd')
 
Outter@for(i in array1) {
    Inner1@for(j in array2) {
        break@Outter
    }
}

给任意类实现Iterator

实现Iterator,也就是说可支持循环,需要实现next 和hasNext。如:

class A(val iterator:Iterator<Int>) {
    operator fun next():Int{
        return iterator.next()
    }
    operator fun hasNext():Boolean{
        return iterator.hasNext()
    }
}
class AList() {
    private val list = ArrayList<Int>()
 
    fun add(int:Int) {
        list.add(int)
    }
    fun remove(int:Int) {
        list.remove(int)
    }
    operator fun iterator():A {
        return A(list.iterator())
    }
}
 
// 调用示例
val list = AList()
list.add(1)
list.add(3)
list.add(5)
// for循环形式
for (i in list) {
    println(i)
}
// while循环形式
val i = list.iterator()
while (i.hasNext()) {
    println(i.next())
}

5.3 异常捕获
Kotlin中异常捕获跟Java中的语法一样,这里提一下其表达式的用法。如:

val result = try {
    1
} catch (e:Exception) {
    e.printStackTrace()
    0
}

6 函数、具名参数、变长参数、默认参数和Lambda表达式
6.1 函数
我们先来说说方法和函数的区别,在Java中只有方法没有函数,而在C++中又一般叫函数不叫方法,这是为什么呢?其实很好理解,类里的funcation一般叫方法,而函数可以放在类的外边。所以也有说,方法其实是函数的一种特殊类型。Kotlin中对函数的语法如:

fun 方法名(参数1:参数类型, 参数N:参数类型):返回类型 {
    函数体
}

其中,如果返回类型是Unit的话,可以省略不写。

函数的类型和引用

//函数的引用,使用两个分号::。如:
class A() {
    fun fun1(p1:String, p2:Long):Int {
        return 1
    }
}
 
// 调用
val a:A=A()
val f:(String, Long)->Int = a::fun1
val result = f("Hello", 50L)

匿名函数

//函数可以不命名,但要赋值给一个变量。将函数看作一个代码块,或者说是一个类型,直接赋值给一个变量。如:

val result = fun(x:Int):Long {
    return x.toLong()
}

6.2 具名参数

//明确告诉编译器哪个参数给哪个变量,参数的顺序也可以变换。如:

fun add(p1:String, p2:String):String {
    return p1 + p2
}
 
// 调用
add(p2 = "World", p1 = "Hello")

6.3 变长参数

//参数使用关键字vararg来表示,使用起来就是可以接收多个不确定数量的同类型参数或者传入一个数组,
//如果传入的是一个数组,需要在其前面加个*,表示把数组展开成一个个元素,
//目前只支持变长参数的展开数组,并非C++的指针。如:

fun add(vararg ints: Int) {
    ints.forEach {
        println(it)
    }
}
 
// 调用
add(1, 2, 3, 4, 5)
var list: IntArray = intArrayOf(1, 2, 3)
add(*list)

注意:变长参数还不支持list,只支持array。
结合具名参数时,变长参数可以放在函数参数列表的任何位置。
6.4 默认参数

//默认参数的出现大大减少了使用方法重载的麻烦

fun add(int:Int, string:String = "Hello") {
    println(int)
    println(string)
}
 
// 调用
add(100)
add(100, "子云心")

//兼容Java的注解
//因为Java中是不存在默认参数的,所以如果你写的Kotlin代码也想提供给Java开发者调用的话,最好可以加上@JvmOverloads注解。如:

class A () {
    @JvmOverloads
    fun a(p:Int = 0) {
        println(p)
    }
}
// Java中的调用也可以这样
new A().a();

注意:

结合具名参数时,默认参数也是可以放在函数参数列表的任何位置。
 

6.5 Lambda表达式
Lambda表达式其实就是匿名函数比较有表示力的一种写法,Lambad表达式返回值是大括号里的最后一行。写法如下:

{[参数列表] –> [函数体,最后一行是返回值]}

()->Unit             // 无参,返回值为Unit
(Int)->Int           // 传入整型,返回一个整型
(String, (String)->String)->Boolean        // 传入字符串、Lambda表式式,返回Boolean

示例:

// 匿名函数
val sum = fun(a: Int, b: Int): Int {
    return a + b
}
// Lambda表达式
val sum = { a: Int, b: Int -> a + b }
 
// 调用    
sum(2, 3)
sum.invoke(2, 3)  

//简化过程:

var array: IntArray = intArrayOf(1, 2, 3)
 
// for循环的形式
for (item in array) {
    println(item)
}
 
// forEach循环形式
array.forEach({ it -> println(it) })
 
//如果一个函数最后一个参数是Lambda表达式,可以将它移到括号外
array.forEach() { println(it) }
 
// 如果小括号没有参数,可以省略
array.forEach { println(it) }
 
// 入参、返回值与形参一致的函数可以用函数引用方式作为实参传入
array.forEach(::println)

注意:如果在Lambda中执行return,是直接对外边的函数执行,因为Lambda是表过式,并非函数。

//如果一定要return表达式,可以这样写:

fun AA() {
    var array: IntArray = intArrayOf(1, 2, 3)
    array.forEach ForEach@ {
        if (it == 2) return@ForEach
        print(it)
    }
    println("AA End")
}
fun BB() {
    var array: IntArray = intArrayOf(1, 2, 3)
    array.forEach {
        if (it == 2) return
        print(it)
    }
    println("BB End")
}
 
// 调用
AA()          // 输出:1 3 AA End
BB()          // 输出:1

7 运算符重载、扩展成员、中缀表达式
7.1 运算符重载

//运算符其实本身就是一个函数,所以可对其进行重载,只需要在函数前使用operator关键字。

class A(var a: String) {
    operator fun plus(other:A):A {
        var result = (a.toInt() + other.a.toInt()).toString()
        return A(result)
    }
 
    override fun toString(): String {
        return a
    }
}
// 调用
var a1 = A("2")
var a2 = A("3")
println(a1 + a2)

说明:示例中,将两个A对象进行用加号相加,其实“+”对应就是plus方法。
Kotlin不支持自定义运算符,但可以使用中缀表达式做到类似的效果。
更多详细运算符对应的函数名,可以参考官方文档:https://www.kotlincn.net/docs/reference/operator-overloading.html。
7.2 中缀表达式

//中缀表达式,使用infix关键字进行声明,它的使用看起来像是自定义运算符似的

class Book{
    infix fun on(any:Any):Boolean{
        return false
    }
}
class Desk {
}
// 调用
var result1 = Book() on Desk()
var result2 = Book().on(Desk())

说明:这里写义了一个中缀表达式,也就是on方法,它在使用起来就跟表达式很相似了。
中缀表达式一般在DSL中使用,平时不建议使用,因为会使代码可读性降低。
7.3 扩展成员

在Java中是不支持扩展方法我,我们往往需要写一个工具方法就是通过静态方法来实现,而在Kotlin的扩展方法的定义要加.。如:

fun String.multiply(int:Int):String {           // 扩展方法
    var stringBuilder = StringBuffer()
    for (i in 0 until int) {
        stringBuilder.append(this)
    }
    return stringBuilder.toString()
}
 
operator fun String.times(int:Int):String {     // 扩展方法+运算符重载,times对应的运算符是*
    var stringBuilder = StringBuffer()
    for (i in 0 until int) {
        stringBuilder.append(this)
    }
    return stringBuilder.toString()
}
// 调用
println("abc".multiply(2))          // 输出结果:abcabc
println("abc" * 2)                  // 输出结果:abcabc

补充:

除了方法可扩展外,属性也是可以进行扩展的

val String.a:String                 // 扩展属性
    get() = "abc"
// 调用
println("123".a)                    // 输出结果:abc

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐