Kotlin基础学习从入门到入土
Kotlin基础
数据类型
在kotlin中,变量使用var
关键字声明,常量使用val
关键字声明。
变量声明可以通过变量名:数据类型
的形式显式声明,也可以通过自动推导的方式声明。
1 | //通过【变量名:数据类型】的形式声明变量 |
Kotlin支持八种基本数据类型:Byte
、Short
、Int
、Long
、Float
、Double
、Char
、Boolean
。
数字类型
数字类型包括
Byte
、Short
、Int
、Long
、Float
、Double
。1
2
3
4
5
6
7
8
9
10
11val byteNum: Byte = 127
val shortNum: Short = 32767
val intNum: Int = 2147483647
val longNum: Long = 9223372036854775807L
val floatNum: Float = 3.14F
val doubleNum: Double = 3.141592653589793
//无符号类型
val uByteNum: UByte = 255u
val uShortNum: UShort = 65535u
val uIntNum: UInt = 4294967295u
val uLongNum: ULong = 18446744073709551615u各个类型的取值范围如下所示:
类型 位数 取值范围 Byte
8
-128 ~ 127
Short
16
-32768 ~ 32767
Int
32
-2^31^ ~ 2^31^-1
Long
64
-2^63^ ~ 2^63^-1
Float
32
1.4^-45^ ~ 3.4028235^38^
Double
64
4.9^-324^ ~ 1.7976931348623157^308^
整型默认的数据类型为
Int,
浮点型为Double
。1
2
3
4
5
6
7
8//以下数据类型均为Int类型
val num1 = 1
val num2 = 123
val num3 = 123456
//以下数据类型均为Double类型
val num4 = 1.0
val num5 = 123.456
val num6 = 123456.789在Kotlin中,可以用”_”分隔数字,便于阅读。
1
val num = 1_000_000
Kotlin支持二进制和十六进制表示,不支持八进制表示。
1
2val binNum = 0b11110000
val hexNum = 0x1f1e33boolean
类型boolean
类型只有true
和false
两个值。1
2val booleanTrue = true
val booleanFalse = falseboolean
类型也可以通过关系运算得到。常用的关系运算符如下:1
2
3
4
5
6
7
8
9
10val boolean1 = 12 > 10 //true
val boolean2 = 12 == 10 //false
val boolean3 = 12 != 10 //false
val boolean4 = 12 <= 10 //false
val boolean5 = 7 in 1..<10 //true [1,10)
val boolean6 = 10 !in 1..10 //false [1,10]
val boolean7 = 12 is Int //true
val boolean8 = 7 < 10 && 12 > 10 //true
val boolean9 = 7 < 10 || 12 > 10 //true
val boolean10 =!true //false字符和字符串类型
Char
类型是一个单一的unicode字符。1
2
3
4val char1 = 'a' //a
val char2 = '\u0061' //a
val char3 = '\uFF21' //A
val char4 = 'A'.code //65String类型通常用” “表示,也可以使用”””(模板字符串)多行表示。
1
2
3
4
5
6
7
8val str1 = "Hello World"
val str2 = """
Hello
World
Kotlin
""".trimIndent() //trimIndent()用于清除缩进
val str3 = "Kotlin"
val str4 = "$str3 hello, world!${str3}" //Kotlin hello, world!Kotlin
转义字符
转义字符如下所示。
转义字符 | 说明 |
---|---|
\t |
制表符 |
\b |
退格 |
\n |
换行(LF) |
\r |
回车(CR) |
\' |
单引号 |
\“ | 双引号 |
\\ |
反斜杠 |
\$ |
美元符 |
位运算
Kotlin提供了相关的位运算操作,仅适用于Int
和Long
类型的变量。
操作符 | 操作说明 |
---|---|
shl(num) |
左移 |
shr(num) |
右移 |
ushr(num) |
无符号右移 |
and(bits) |
按位与 |
or(bits) |
按位或 |
xor(bits) |
按位异或 |
inv() |
取反 |
[!IMPORTANT]
不存在无符号左移。
1 | //左移shl(num) |
流程控制语句
if-else和when
在Kotlin中,if
语句与java类似,但是还提供了一种单行表达式的写法,类似于三元运算符。
1 | val x = 10 |
when
类似于java中的switch
。
1 | val x = 10 |
for和while
for
采用 for(x in array)
的形式,除此之外break
和continue
的用法同java。
1 | val array = arrayOf(1, 2, 3, 4, 5) |
while
和do-while
语句同java。
普通函数
在Kotlin中,函数使用fun
关键字声明。
Kotlin中的函数定义更加简洁和灵活。例如,我们可以在一个函数中定义另一个局部函数。
1 | //执行foo(2)会返回4 |
函数的参数可以带有默认值,如果调用时不传入参数,则使用默认值作为传入的参数。我们也可以手动指定传入的参数对应哪一个形参。
1 | fun sayHello() { |
如果一个函数较为简单,类似于上面的sub()
,则可以采用如下方式简写。
1 | fun multiply(a: Int, b: Int): Int = a * b |
然而使用上述方法求解斐波那契时,存在了大量的重复运算。通过使用tailrec
关键字对函数进行修饰,我们可以进行尾递归优化。
1 | // 1 1 2 3 5 8 13 21 34 55 89 144 |
[!NOTE]
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作。
tailrec
就是让递归变成了迭代,因此tailrec
关键字只能优化尾递归算法,其它递归算法无法优化。
Kotlin支持在缺省函数名的情况下,直接定义一个函数。
1 | fun calculate(num: Int, condition: (Int) -> Boolean): Int { |
函数的重载同java。
高阶函数
函数类型变量
在Kotlin中,函数类型的格式非常简单。举个例子:(Int) -> Unit
。
函数类型的声明需要遵循以下几点:
- 通过
->
组织参数类型和返回值类型。左边是参数类型,右边是返回值类型。 - 必须用
()
包裹参数类型。 - 返回值类型即使是
Unit
也必须显式声明。
如果一个函数类型没有参数,则可以通过() -> Unit
表示。如果有多个参数,则通过”,”进行分隔:(Int, String) -> Unit
。
如果一个参数是可选的,则可以用”?”来表示。如果该函数类型的变量也是可选的,则可以将整个函数类型变为可选。
1 | //msg在某种情况下不需要传入,则可以如下表示 |
函数类型的变量还可以返回另一个函数。
1 | //这表示传入一个Int类型的参数,返回一个类型为(Int) -> Unit的函数 |
Lambda表达式
在Kotlin中,可以通过Lambda表达式简化匿名函数。
1 | //执行println(sum(1, 2))会返回3 |
我们可以直接把Lambda作为参数传入,作为实参使用。下述情况被称为尾随Lambda表达式,只有在最后一个参数是函数类型的情况下才可以使用。
1 | fun test(func: (Int) -> String) { |
如果函数调用的是尾随Lambda表达式,默认的标签名为函数名。
1 | fun printName(func: (Int) -> String){ |
在Lambda中不能使用return
返回结果。
内联函数
在Kotlin中使用Lambda表达式会带来一些额外的开销。我们可以通过inline
关键字修饰函数,使其成为内联函数。被inline
修饰的函数,在执行时会将函数体嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行时的时间开销。
1 | fun main() { |
以上代码相当于:
1 | fun main(){ |
内联函数会导致编译后的代码变多,但是获得了性能上的提升。该操作对高阶函数有显著的效果,对于普通的函数即使使用内联函数也不会获得多少提升。
内联函数中的函数形参无法作为值给到变量,只能调用。
由于内联导致代码直接被搬运,所以Lambda中的return语句可以不带标签。
1 | fun main(){ |
上述代码运行后不会输出任何内容。这种情况被称为非局部返回。因为Lambda中的return
作用于整个main(),可以通过@标签
更改其作用域来防止非局部返回。
为了避免带有return
的Lambda参数产生破坏,可以用crossinline
关键字修饰该参数。
1 | //以下代码执行后将会打印"调用内联函数之后..."和"调用上述方法之后..." |
一个函数一旦被定义为内联函数,边不能获取闭包类中的私有成员,除非将它们声明为internal
。
通过noinline
关键字修饰参数可以使其不具有内联的效果。
中缀函数
通过infix
关键字修饰的函数被称为中缀函数,在调用时可以通过A 中缀方法 B
的形式调用,省略了”.”和”()”。中缀函数在使用时必须满足以下条件:
- 必须是某个类型的扩展函数或者成员方法;
- 只能有一个参数,参数不能有默认值;
- 该参数不能是可变参数。
1 | class Name(val name: String){ |
[!NOTE]
Kotlin通过
vararg
关键字定义函数中的可变参数,类似于java中的...
。在java中可变参数必须是最后一个参数,而在Kotlin中没有这个限制。我们还可以使用
*
来将外部变量作为可变参数的变量。
1
2
3
4
5 fun printLetter(vararg letters: String) = println(letters.joinToString())
printLetter("a", "b", "c") // "a, b, c"
val letters = arrayOf("a", "b", "c","d","e")
printLetter(*letters) // "a, b, c, d, e"
Kotlin核心
类和初始化
Kotlin中的类和java类似。
通常,类中一般具有一些属性。在Kotlin中,可以通过constructor
关键字添加属性。
1 | class Student constructor(name: String, age: Int) {} |
如果主构造函数没有任何注释或可见性修饰符,则可以省略constructor
,如果类中没有内容,则可以省略{}
。
1 | class Student(name: String, age: Int) |
上述仅仅定义了构造函数的参数,还不是类的属性。可以在上述参数前添加var
或者val
来表示这个属性是可变还是不可变的。
1 | class Student(var name: String, val age: Int) |
一个类可以有一个主构造函数和多个次构造函数。次构造函数中可以自定义函数体。
1 | class Student(var name: String, var age: Int){ |
在Kotlin中,可以通过init
关键字进行初始化,类似于java中的static
。但在Kotlin中,可以存在多个init
。
1 | class Student(var name: String, var age: Int) { |
类的扩展
Kotlin提供了扩展类或接口的操作,来为某个类添加一些额外的函数或属性。
1 | // 为Kotlin内置的String类添加一个test函数 |
类的扩展是静态的,并不会修改它们原本的类,也不会将新成员插入到类中,仅仅是将我们定义的功能变得可调用。如果要扩展属性,只能明确定义一个getter
和setter
来创建扩展属性。
如果命名发生冲突时,需要特别处理。
1 | class A{ |
如果类本身就具有同名同参数的函数,那么扩展函数将失效。但如果通过扩展类实现函数的重载,完全没有问题。
1 | class Test{ |
如果将一个扩展函数作为参数给到一个函数类型变量,需要在具体操作之前增加类型名称。
1 | val len:String.() -> Int = { this.length } |
运算符重载函数
Kotlin支持为程序中已知的运算符集提供自定义实现。要实现运算符重载,应为相应类型提供具有对应运算符指定名称的成员函数,当前的类对象直接作为运算符左边的操作数。
1 | class Student(val name: String, val age: Int) { |
运算符对应的名称是固定的。
下面是基本的一元运算符的固定名称。
符号 | 名称 |
---|---|
+a |
a.unaryPlus() |
-a |
a.unaryMinus() |
!a |
a.not() |
a++ |
a.dec() |
a-- |
a.inc() |
inc()
和dec()
函数比较特殊,他们返回新生成的对象,将变量的值直接引用到这个对象。
例如,a++
的操作步骤如下:
- 将
a
的初始值存储到临时对象a0
; - 将
a0.inc()
的结果分配给a; - 返回
a0
作为表达式的结果。
同理,++a
的操作步骤如下:
- 将
a.inc()
的结果分配给a; - 作为表达式的结果返回
a
的新值。
下面是基本的二元运算符的固定名称。
符号 | 名称 |
---|---|
a + b |
a.plus(b) |
a - b |
a.minus(b) |
a * b |
a.times(b) |
a / b |
a.div(b) |
a % b |
a.rem(b) |
a .. b |
a.rangeTo(b) |
a ..< b |
a.rangeUntil(b) |
a in b |
b.contains(a) |
a !in b |
!b.contains(a) |
对于in
来说,返回值必须是Boolean类型。
运算符重载有可能会出现歧义。对于a = a + b
和a + = b
来说,两者的功能基本一致,如果同时重载了plus()
和plusAssign()
,编译器会不知道用哪个,因此会出现歧义。
1 | operator fun plus(other: Student): Student { |
上述两个运算符都能匹配到下方的使用,因此会报错。
比较运算符只需要实现一个即可。
运算符 | 名称 |
---|---|
a > b |
a.compareTo(b) > 0 |
a < b |
a.compareTo(b) < 0 |
a >= b |
a.compareTo(b) >= 0 |
a <= b |
a.compareTo(b) <= 0 |
所有的比较都会转为compareTo()
进行调用,返回Int
类型的值用于判断是否满足条件。
1 | operator fun compareTo(other: Student): Int { |
Kotlin非常强大,甚至连()
和[]
也可以重载。
运算符 | 名称 |
---|---|
a() |
a.invoke() |
a(i) |
a.invoke(i) |
a(i, j) |
a.invoke(i, j) |
a(i_1, i_2, ..., i_n) |
a.invoke(i_1, i_2, ..., i_n) |
a[i] |
a.get(i) |
a[i, j] |
a.get(i, j) |
a[i_1, i_2, ..., i_n] |
a.get(i_1, i_2, ..., i_n) |
a[i] = b |
a.set(i, b) |
a[i, j] = b |
a.set(i, j, b) |
a[i_1, i_2, ..., i_n] = b |
a.set(i_1, i_2, ..., i_n) |
空值和空类型
在Kotlin中,对空值处理非常严格。正常情况下我们不能直接给变量赋值为null
。
可以通过在类型后面添加?
使其变为可空类型,使该变量在初始情况下使用null
。
1 | val name: String? = null |
如果我们已经清楚该变量在某情况下一定不为空,我们可以通过非空断言操作符!!.
来告诉编译器一定是安全的,可以执行。
1 | val name: String? = null |
Kotlin提供了一种更安全的空类型操作,使用安全调用运算符?.
来安全的使用对象,如果对象的属性为null
则安全调用运算符返回null
。
1 | val name: String? = null |
如果想让安全调用运算符返回一个自定义的结果而不是null
,可以使用Elvis运算符?:
。如果Elvis左侧为null
,则返回右侧的自定义结果。
Elvis运算符有点像三元运算符,但不是同样的作用。
1 | val name: String? = null |
解构
在使用对象时可能需要访问对象内部的一些属性。
1 | val stu = Student("Soria", 18) |
这样的操作不太优雅,我们可以通过解构的方式对属性进行处理。
要让一个类的属性支持解构,只需要添加约定的函数即可。在Kotlin中可以自定义解构出来的结果,通过定义componentN()
并通过返回值的形式返回解构结果。
1 | class Student(var name: String, var age: Int){ |
访问权限
当我们指定一个类、方法或属性的修改或者重写权限时,需要用到限制修饰符。在Kotlin中,类的默认修饰符为final
,因此类和方法默认是不可被继承或重写的。我们可以通过open
修饰符使其可以被继承和重写。
修饰符 | 含义 | 与java比较 |
---|---|---|
open |
允许被继承或重写 | 相当于java类与方法的默认情况 |
abstract |
抽象类或抽象方法 | 与java一致 |
final |
不允许被继承或重写(默认情况) | 与java主动指定final 的效果一致 |
1 | open class Bird { |
[!NOTE]
实际过程中应遵循里氏替换原则,子类可以扩展父类的功能,但不能改变父类原有的功能。包含以下四个设计原则:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;
- 子类可以增加自己特有的方法;
- 子类的方法实现父类的方法时,形参应比父类的输入参数更宽松;
- 子类的方法实现父类的抽象方法时,返回值要比父类更严格。
Kotlin还可以通过sealed
关键字修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类无法继承这个类。
密封类是基于一个抽象类实现的,因此密封类不能被初始化。
1 | sealed class Bird { |
通过可见性修饰符可以指定类、方法及属性的可见性。Kotlin的可见性修饰符与java类似,但仍有几点不同:
- Kotlin中默认修饰符为
public
,而java中是default
; - Kotlin可以在一个文件内单独声明方法及常量,同样支持可见性修饰符;
- java中除了内部类以外,其他类不允许使用
private
修饰,Kotlin可以; - Kotlin中的
protected
修饰符只允许类和子类能访问,而java中的包也可以访问; - Kotlin中的独特修饰符
internal
,叫做模块内访问。被internal
修饰的类,别人在引用我们的项目时不可访问。
1 | // 整个项目模块都可以访问该类,但别人用我们的项目作为第三方库时无法访问 |
类的封装和继承
封装的目的是为了保证变量的安全性,使我们不必在意具体实现细节,只通过外部接口即可访问类的成员。如果不进行封装,类中的实例变量可以直接查看和修改,会给整个程序带来不好的影响。在编写类时,一般将成员变量私有化,外部类需要通过Getter
和Setter
进行查看和设置变量。
1 | class Student(private var name: String, private var age: Int) { |
Kotlin中的继承和实现接口没有采用extends
和implements
关键字,而是使用”:”来代替。
如果父类存在一个有参构造函数,子类必须在构造函数中调用。如果父类存在多个构造函数,则可以任选一个。
1 | abstract class Person(val name: String, val age: Int) { |
重载
和java一样,在Kotlin中也是用override
关键字来重写对象。但需要注意的是,被重写的对象需要用open
关键字修饰。
1 | open class Student() { |
内部类
在Kotlin中声明内部类,必须用inner
关键字修饰这个类,若没有修饰则会被视为嵌套类。
内部类包含对其外部类实例的引用,可以使用外部类中的属性。而嵌套类不包含,所以无法调用外部类的属性。
1 | class InnerClass{ |
一个类的内部可以定义多个内部类,每个内部类的实例都有自己独立的状态,与外部对象的信息相互独立。利用private
修饰内部类,可以使其他类都不能访问内部类,具有良好的封装性。
1 | open class Horse{ |
枚举类
Kotlin中的枚举和java相同,但是声明是要通过enum class
的方式声明。
枚举类可以具有成员。
1 | enum class LightState(val color: String) { |
枚举类还提供了很多方法用于我们快速使用。
1 | // 通过valueOf以字符串形式转化为对应名称的枚举 |
数据类
我们可以通过data class
声明一个数据类。声明一个数据类必须满足以下条件:
- 必须拥有一个构造方法,该方法至少包含一个参数;
- 构造方法的参数强制使用var或val进行声明;
- data class前不能用abstract、open、sealed或inner进行修饰。
数据类既可以实现接口也可以继承类。
1 | data class Animal(val name: String, val food: String){ |
Kotlin提供了Pair
和Triple
两个数据类,前者有两个属性,后者有三个,让使用者不必主动声明数据类。
抽象类
有时候我们设计的类仅仅是作为给其他类集成使用的类,其本身不需要创建任何实例对象。通过abstract
关键字可以将一个类声明为抽象类。
1 | abstract class Person { |
抽象类不仅可以具有抽象的属性,同时也具有普通类的性质,同样可以定义非抽象的属性或函数。同时,抽象类也可以继承自其他的类(可以是抽象类也可以是普通类)。
接口
接口只能包含函数或属性的定义,所有的内容默认用abstract
修饰。
1 | interface Student { |
伴生对象
通过companion object
关键字可以创建伴生对象。伴生对象和java中static
的修饰效果性质一样,且全局只有一个单例。伴生对象需要声明在类的内部,在类被装载时会被初始化。
1 | class Prize { |
object单例
单例模式最大的特点是系统中只能存在一个实例对象,所以在java中必须设置构造方法私有化,以及提供静态方法创建实例的方式来创建实例对象。
1 | public class DatabaseConfig { |
上面是java实现一个最基本单例模式的精简例子(省略了多线程以及多种参数创建单例对象的方法)。它依赖static
关键字,同时还必须将构造方法私有化。
在Kotlin中,通过object
关键字可以直接实现单例。object
的全局声明对象只有一个,所以并不需要语法上的初始化,甚至不需要构造方法。
1 | object DatabaseConfig { |
object
还可以代替java的匿名内部类。例如,通过java对字符串列表进行排序:
1 | List<String> list = Arrays.asList("apple", "banana", "orange"); |
在Kotlin中,可以通过object
表达式对其进行改善。
1 | val list: List<String> = listOf("apple", "banana", "orange") |
在java中,匿名内部类只能继承一个类以及实现一个接口,而object
表达式没有这个限制。而且object
表达式在运行时并不会全局只存在一个对象,而是每次运行时都会生成一个新的对象。
我们还可以将上面的代码通过Lambda表达式改造一下。
1 | val comparator = Comparator<String> { o1, o2 -> |
当只需要实现一个方法时,使用Lambda表达式更适合;当匿名内部类内有多个方法需要实现时,使用object
表达式更加合适。
类型系统
可空类型
在Kotlin中,处理NPE(NullPointerException)非常容易。Kotlin能够很好的区分非空类型和可空类型。在Kotlin中访问非空类型的变量永远不会抛出空指针异常。
具体见上一章节。
通过Either
代替可空类型
Either
类型只有Left
和Right
两个子类型。通常来说Left
代表出错的情况,Right
代表正确的情况。如果Either[A, B]
对象包含的是A的实例,它就是Left
实例,否则就是Right
实例。
在Kotlin中并没有Either
类,但是可以通过密封类便捷的创造出Either
类。
1 | sealed class Either<A, B> { |
[!NOTE]
let
的概念
1 public inline fun <T, R> T.let(block: (T) -> R): R = block(this)调用某对象的
let
函数,该对象会作为函数的参数,在函数块内可通过it
代指该对象。返回值为函数的最后一行或指定的return
表达式。
按照上述写法代码量会变多,但是我们需要用ADT更好地组织业务。定义一个Error类,将所有步骤中的错误都抽象为不同的子类型,便于最终的处理以及后期排查错误。
类型检查
在Kotlin中,可以通过is
关键字判断一个对象是什么类型。
1 | if(obj is String){ |
1 | when(obj){ |
上述obj为Any
类型,虽然做了类型判断,但是在没有类型转换的前提下,我们直接使用了length
方法。这主要通过智能类型转换实现。
智能类型转换
智能类型转换时隐式完成的。
1 | val stu:Student = Student(Glasses(10)) |
Kotlin和其他语言一样,无法直接将父类型转化为子类型。当类型需要强制转换时,可以通过as
关键字实现。
1 | open class Human data class Teacher(val name:String) :Human() |
as
是不安全的类型转换,一般使用as?
,如果对象为空则返回null
。
1 | val teacher = getTeacher() as? Teacher |
Any和Any?类型
Any类型
Any
类型式非空类型的根类型。如果定义了一个没有指定父类型的类型,则该类型将是Any类型的直接子类型。如果为定义的新类型指定了父类型,则该父类型将是新类型的直接父类型,但新类型的最终根类型仍为Any
。
如果新类型实现了多个接口,那么将具有多个直接的父类型,但最终根类型还是Any
。
1 | abstract class Animal(val weight:Double) |
Kotlin把java方法参数和返回类型中的Object
类型看作是Any
(又或者是平台类型)。当Kotlin在函数中使用Any
时,会被编译成java字节码中的Object。
[!NOTE]
平台类型本质上是Kotlin不知道可空性信息的类型。所有java引用类型都在Kotlin中表现为平台类型。当在Kotlin中处理平台类型的值的时候,它既可以被当做可空类型来处理,也可以被当做非空类型来处理。
Any?类型
Any?
是Any
的父类型,而且**Any?
类型是所有类型的根类型。**
Any?
可以看作是Any∪null
。
自动装箱与拆箱
Kotlin没有int、float等原始类型,而是Int、Float等引用类型包装类。用java举例,Kotlin中的Int
相当于int
,Int?
相当于Integer
。
数组
在Kotlin中,数组是一个Array
类型的对象。数组在创建完成之后,数组容量和元素类型是固定不变的,后续无法进行修改。数组在内存中是连续的,所以性能比较好。
1 | val arr = arrayOf(1, 2, 3, 4, 5) |
数组的打印
Kotlin提供了很多种打印数组的方式。
1 | val arr = arrayOf(1, 2, 3, 4, 5) |
多维数组
多维数组与java类似。
1 | val arr = arrayOf(intArrayOf(1, 2, 3), intArrayOf(4, 5, 6), intArrayOf(7, 8, 9)) |
泛型
Kotlin的泛型和java相同,都通过<>
表示。
类型上界
约束一个泛型类只接受一个类型的对象或者类及其子类,称为上界约束。
1 | open class Fruit(val weight: Double) |
在Kotlin中,可以通过where
关键字对泛型参数类型添加多个约束条件。
1 | interface Ground {} |
类型擦除
泛型的类型检查仅仅只存在于编译阶段,在编译之后并不会保留任何关于泛型的内容。这便是类型擦除。类型擦除后一般为Any?
,如果存在上界,那么擦除后是上界的类型。
1 | class TypeClear<T>(private val t: T) { |
对于内联函数,泛型擦除的处理有一些不同。内联函数的泛型参数的具体类型信息是可用的,编译一起可以使用这些信息来生成具体的字节码。
为了避免频繁地类型转换,通常可以配合泛型封装成一个类型转换方法。我们还要避免类型擦除,可以用reified
修饰T
,并在方法前用inline
修饰。**reified
关键字可以理解为具体化,使我们可以在方法体内访问泛型指定的JVM对象。**
1 | inline fun <reified T> cast(original: Any): T? = original as? T |
协变和逆变
对于泛型来说,可以简单理解为:协变是可读不可写,逆变可写不可读。协变用out
关键字表示,逆变用in
关键字表示。
对于协变,如果类型A是类型B的子类型,那么Generic<A>
也是Generic<B>
的子类型。
对于逆变,如果类型A是类型B的子类型,那么Generic<B>
将变为Generic<A>
的子类型。
用out
关键字声明的泛型参数类型将不能作为方法的参数类型,但是可以作为方法的返回值类型,对应的setter
也会被限制。in
关键字与out
相反,可以作为方法的参数类型,但是不能作为方法的返回值类型,对应的getter
也会被限制。
1 | class Test<T>(var data:T) |
可以通过*
来代替任意类型,本质上相当于out Any?
。
1 | class Test<T>(var data: T) |
Lambda和集合
任何函数接收了一个java的SAM(单一抽象方法)都可以用Kotlin函数进行替代。
1 | view.setOnClickListener(new OnClickListener(){ |
对于上述java代码,我们可以看成在Kotlin中定义了以下方法:
1 | // listener是一个函数类型的参数,它接收一个类型为View的参数,返回Unit |
带接收者的Lambda
我们可以定义带有接收者的函数类型。
1 | val sum:Int.(Int) -> Int = { other -> plus(other) } |
with和apply
Kotlin提供了with
和apply
两个函数,省略了需要多次书写的对象名,默认用this
指向它。
1 | fun bindData(bean: ContentBean){ |
以下是with
和apply
在Kotlin中的定义。
1 | inline fun <T, R> with(receiver: T, block: T.() -> R): R |
with
函数的第一个参数为接收者类型,然后通过第二个参数创建这个类型的block方法,因此接收者对象调用block方法时,可以直接使用this
来代表这个对象。apply
函数直接被声明为T
类型的扩展方法,block参数是一个返回Unit
类型的函数。因此,with
的block可以返回自由的类型,然而二者在很多情况下都是可以互相替换的。
集合
Kotlin中常用的集合包括List
、Set
和Map
。
List
表示一个有序可重复的列表。
Set
表示一个不可重复的集合。Set
的实现方式分为HashSet
和TreeSet
。HashSet通过Hash散列来存储元素,不能保证元素的有序性。TreeSet
底层结构是二叉树,可以保证元素的有序性。
Map
和其他集合不同,没有实现Iterable
或者Collection
。Map
通过键值对来表示元素。
可变集合和有序集合
可变集合的创建方式带有mutable前缀,而只读集合一般为默认的创建方式。
1 | // 可变集合 |
迭代器
迭代器是每个集合类、数组都可以生成的东西,其作用是用于对内容的遍历。
1 | val list = mutableListOf(1, 2, 3, 4, 5) |
对于List
集合来说,Kotlin提供了一个特殊的迭代器listIterator
。
1 | val list = mutableListOf(1, 2, 3, 4, 5) |
Kotlin还为Mutable集合提供了一个特殊的用于生成MutableIterator的函数,只要不是只读的集合类,都可以使用这个特殊的迭代器。其支持在遍历集合时对元素进行删除。
1 | val list = mutableListOf(1, 2, 3, 4, 5) |
惰性集合
在Kotlin中,可以通过sequence
(序列)节省资源开销。在序列中,元素的求值是惰性的,只有在需要时才进行求值,而不是在它被绑定到变量后就立刻求值。(惰性求值)
1 | val list = mutableListOf(1, 2, 3, 4, 5) |
上述操作中,filter
和map
中的println
并没有被执行。
中间操作和末端操作
在对普通集合进行链式操作的时候,有些操作会产生中间集合,当用这类操作对序列进行求值时,它们就被称为中间操作,例如上面的filter
和map
。这就是惰性求值的提现,也被称为延迟求值。然而,在对集合进行操作时,我们在意的大部分都是结果,而不是中间过程。末端操作就是返回结果的操作。在执行末端操作时,会触发中间操作的延迟计算。也就是满足了惰性操作的“被需要时”。
1 | val list = mutableListOf(1, 2, 3, 4, 5) |
在上述例子添加了末端操作toList
后,所有的中间操作都被执行了。
无限序列
利用惰性求值的特性,可以通过序列构造出一个无限的数据类型。因为元素只有在被需要时才进行求值,所以无限序列只是实现了一种无限的状态,让我们在使用时感觉它是无限的。
1 | // 将自然数存储到序列中: 0, 1, 2, ..., n |
多态和扩展
多态
常见的多态分为子类型多态、参数多态和特设多态三种。
当我们用子类型去继承一个父类的时候,就是子类型多态。
参数多态
参数多态是指声明与定义函数、复合类型、变量时不指定其具体的类型,而是把这部分作为参数使用,使得该类型对各种具体类型都适用。
1 | interface KeyI{ val uniqueKey: String } |
特设多态
对于特设多态,可以理解为一个多态函数是有多个不同的实现,依赖于其实参而调用相应版本的函数。例如函数重载、操作符重载和泛型。以上三种方式,编译器会根据传入参数的类型来选择最匹配的函数或操作符重载来执行,从而实现特设多态的效果。
扩展
在面向对象中已经介绍了类的扩展,在此将对扩展进行详细的说明。
在修改现有代码时,遵循开放封闭原则,对扩展开放,对修改封闭。
扩展函数
扩展函数的声明非常简单,其关键字是<Type>
。此外还需要一个接收者类型作为它的前缀。在扩展函数体里,可以用this
代表接收者类型的对象。
以MutableList<Int>
为例,我们扩展一个exchange
方法,实现元素位置的交换。
1 | fun MutableList<Int>.exchange(from: Int, to: Int){ |
实现机制
我们可以将扩展函数理解为java中的静态方法,独立于该类的任何对象,且不依赖类的特定实例,被该类的所有实例共享。因此扩展函数不会带来额外的性能消耗。
作用域
我们习惯将扩展函数直接定义在包内。
为了便于管理,可以将扩展函数定义在一个类内部。当扩展方法定义在一个类内部时,只能在该类和其子类中进行调用。
扩展属性
与扩展函数类似,可以为一个类添加扩展属性。
还是以MutableList<Int>
为例,我们可以为其扩展一个判断其中元素之和是否为偶数的属性sumIsEven
。
1 | val MutableList<Int>.sumIsEven: Boolean |
我们也可以定义类似java静态函数一样的扩展函数,但是必须将其定义在伴生对象上。
1 | class Son{ companion object{ val age: Int = 18 } } |
这样就可以在Son
没有实例对象的情况下,也能调用这个扩展函数。语法类似于java的静态函数。
在使用扩展函数时,同名的类成员方法的优先级高于扩展函数。
1 | class Fruit{ fun eat() = println("eat fruit") } |
元编程
描述数据的数据叫做元数据,操作元数据的编程就是元编程。通过元编程可以消除一些模板代码。
例如,我们需要将data class
转化为Map
。
1 | data class USer(val name: String, val age: Int) |
如果在data class
比较多的情况下,就会出现大量和上面类似的样板代码。我们可以通过反射解决这个问题。
1 | object Mapper{ |
通过使用反射,我们可以让上述代码适用于所有data class
,而不需要针对每个去单独构造出一个toMap
函数。也不需要手动创建Map
里的属性名,可以根据KClass自动获取。
Kotlin中的反射
先来对比一下java和Kotlin中的反射。
我们可以得出以下结论:
KClass
和Class
可以看做同一个含义的类型,并且可以通过.java
和.kotlin
方法实现两者的相互转化。KCallable
和AccessiableObject
都可以理解为可调用元素。java中的构造方法作为一个独立的类型,Kotlin则统一用KFunction
处理。KProperty
通常指相应的Getter
和Setter
整体作为一个KProperty
,java的Field
通常仅仅指字段本身。
KClass
KClass
出了和java的Class
非常相似之外,还有独属于Kotlin的属性或方法。
属性或函数 | 含义 |
---|---|
isCompanion |
是否为伴生对象 |
isData |
是否为数据类 |
isSealed |
是否为密封类 |
objectInstance |
object实例(如果是) |
companionObjectInstance |
伴生对象实例 |
declaredMemberExtensionFunctions |
扩展函数 |
declaredMemberExtesionProperties |
扩展属性 |
memberExtensionFunctions |
本类及超类扩展函数 |
memberExtensionProperties |
本类及超类扩展属性 |
starProjectedType |
泛型通配类型 |
下面通过一个例子理解上述方法或属性。
1 | sealed class Cat { |
上述方法的调用结果如下表所示。
方法 | 结果 |
---|---|
Cat.Companion::class.isCompanion |
true |
Cat::class.isSealed |
true |
Cat.Companion::class.objectInstance |
包名.Cat$Companion@地址 |
Cat::class.declaredMemberExtensionProperties.map { it.name } |
[getCat] |
Cat::class.declaredMemberExtensionFunctions.map { it.name } |
[proceed] |
Animal::class.memberExtensionProperties.map { it.name } |
[getCat] |
Animal::class.memberExtensionFunctions.map { it.name } |
[proceed] |
Animal::class.starProjectedType |
包名.Animal<*> |
KCallable
Kotlin把Class
中的属性Property
和函数Function
以及构造函数都看做KCallable
,因为它们是可调用的,都是Class
的成员。
KCallAble提供了一些实用的API。这些API和java中的反射的API很相似,都是对KCallable
(Class成员)的信息的获取。
API | 描述 |
---|---|
isAbstract: Boolean<KParameter> |
此KCallable 是否为抽象的 |
isFinal: Boolean |
此KCallable 是否为final |
isOpen: Boolean |
此KCallable 是否为open |
name: String |
此KCallable 的名称 |
parameters: List<KParameter> |
调用此KCallable 所需的参数 |
returnType: KType |
此KCallable 的返回类型 |
typeParameters: List<KTypeParameter> |
此KCallable 的类型参数 |
visibility: KVisibility? |
此KCallable 的可见性 |
call(vararg args: Any?): R |
给定函数调用此KCallable |
在java中,我们可以通过Field.set(...)
来更改某个属性的值。在Kotlin中,可以很轻松的通过call
实现。
1 | data class Stu(val name: String, val age: Int, var address: String) |
KMutableProperty
的API仅仅只比KProperty
多了一个setter
函数。
其他反射及其API自行查阅官网文档。
Kotlin的注解
在 Kotlin 中,注解(Annotations)具有以下作用:
- 提供元数据:注解可以用来提供关于程序元素的额外信息。通过在代码中添加注解,你可以指定程序元素的某些特定细节或属性,这些信息可以被其他代码或工具所利用。
- 用于静态检查和编译时处理:Kotlin 中的注解可以被用于在编译时检查代码,并触发特定的行为或处理。这样可以帮助你在编译时对代码进行额外的检查或生成额外的代码。
- 框架集成和元编程:注解在许多框架中起到重要作用,比如依赖注入、RESTful 服务的开发、ORM、序列化和反序列化等。注解还被广泛用于编写元编程的代码,比如自动生成代码或进行代码分析等。
- 用于标记和文档化:注解可以用于标记特定的代码块、类或函数,并且在文档生成工具中用于生成文档。通过使用注解,你可以方便地为代码添加标记和文档信息。
Kotlin还引入了精确的注解控制语法。
用法 | 含义 |
---|---|
@file:annotation |
annotation 注解作用于文件 |
@property:annotation |
annotation 注解作用于属性 |
@field:annotation |
annotation 注解作用于字段 |
@get:annotation |
annotation 注解作用于Getter |
@setter:annotation |
annotation 注解作用于Setter |
@receiver:annotation |
annotation 注解作用于扩展函数或属性 |
@param:annotation |
annotation 注解作用于构造函数参数 |
@setparam:annotation |
annotation 注解作用于Setter 的参数 |
@delegate:annotation |
annotation 注解作用于存储代理实例的字段 |
在Kotlin中,通过在class
前增加annotation
关键字即可创建注解。
1 | annotation class Cache(val namespace: String, val expires: Int) |
协程
协程的启动
协程需要运行在协程上下文环境,在非协程环境中凭空启动协程,有三种方式。
runBlocking{}
启动一个新协程,并阻塞当前线程,直到其内部所有逻辑以及子协程逻辑全部执行完成。
GlobalScope.launch{}
在应用范围内启动一个新协程,协程的生命周期与应用程序一致。这样启动的协程并不能使线程保活,就像守护线程。
由于这样启动的协程存在启动协程的组件已被销毁但协程还存在的情况,极限情况下可能导致资源耗尽,因此并不推荐这样启动,尤其是在客户端这种需要频繁创建销毁组件的场景。
实现
CoroutineScope + launch{}
这是在应用中最推荐使用的协程使用方式——为自己的组件实现
CoroutineScope
接口,在需要的地方使用launch{}
方法启动协程。使得协程和该组件生命周期绑定,组件销毁时,协程一并销毁。从而实现安全可靠地协程调用。