这个笔记把所有重点知识和语法都罗列出来了,方便复习使用,对于一些比较生僻比如和Java区别比较大或者Java完全没有的可列出了一些示例.
# Kotlin应用于Android开发
- 兼容性: 兼容JDK6,在AS中支持
- 速度与Java类似.非常相似的字节码结构,另外,支持内联函数,使得lambda表达式比Java运行更快.
- 互操作: 100%互操作
- 占用:非常紧凑的运行时库,通过Proguard可以进一步减少.Kotlin运行时只增加几百个方法,给apk增加不到100K大小.
- 编译时长: 支持增量编译.一般与Java一样快或更快
- 学习曲线:对于Java入门很容易.Java一键转Kotlin工具;
Kotlin心印
学习.
# 工具
- Kotlin Android plugin
- Anko ( 666 )
# 习惯用法
创建DTOs(POJOs,POCOs)
使用数据类.
data class Customer(val name: String, val email: String)
尽量指定函数的默认参数(减少重载方法)
过滤list使用lambda表达式
val positives=list.fitler{it>0}
String模板
println("Name:$name")
类型判断
使用when
表达式或语句+is
关键字
遍历map/pair型的list(item是键值对类型的)
可以直接解构
for((k,v) in map){
println("$k->$v")
}
使用区间
包括in
,downTo
,step
,until
,..
这些
使用只读list,只读map,访问map使用[]
listOf(1,2,3)
val map=mapof("a" to 1,"b" to 2,"c" to 3)
map["a"]=3
使用延迟属性
val p:String by lazy{
//计算该字符串
}
使用扩展函数
可以代替很多静态工具类
创建单例
使用object
关键字
非空判断
使用?.
,?:
,?.let{}
使用when表达式
when表达式和when语句.
try/catch表达式
try/catch也是一个表达式,你敢相信
使用if/else if/else
表达式
通过if表达式获取结果
Builder风格的用法
就是构造器模式,如果返回类型时Unit,就让他返回当前对象,这样可以链式调用
单表达式函数
如果可以的话,可以使用单表达式的函数,直接=表达式
,不需要大括号和return,而且可以自动推断类型.另外,比如可以结合when
表达式也也可以写比较复杂逻辑的函数.
使用with
关键字
对同一个对象调用多个方法
需要泛型信息的泛型函数的适宜形式
//这里就不需要再参数里面指定泛型类型了`Class<T> classOfT`,但是只能内联函数使用
inline fun <reified T: Any> Gson.fromJson(json: JsonElement): T = this.fromJson(json, T::class.java)
没有受检异常
使用可空的Boolean(Boolean?
)
好处应该是除了true和false两种状态,还有一种就是未设置的状态吧.
# 关键字
其他: 官方中文文档中总结的关键字与操作符
Kotlin中是区分硬关键字(类似Java的)和软关键字(在有些上下文他就不是关键字),还有修饰关键字
.
1. import
:类似Java,所有导入都用import,没有import static
2. as
: 导入包时重命名(防止冲突);类型转换,可以使用as?
做安全的类型转换,如果是指定类型则返回,否则返回null.
3. fun
:定义函数
4. vararg
:可变数量参数,类似Java的...
5. infix
: 修饰函数:可以让他用中缀表示法调用(后面部分有介绍,类似sql那样).
6. tailrec
:修饰函数,尾递归函数;编辑器会优化该递归为循环的形式,防止递归导致的堆栈溢出风险且提高了执行效率.
7. var|val
:变量(可读写)和常量(只读)定义
8. get|set
:自定义访问器中的两个方法名
9. lateinit
:延迟初始化属性(对于非空类型可以不立马初始化,如果第一次读取的时候还没有初始化则抛出特定异常)
10. open|override
: 开放和覆盖(前者声明当前类或成员(属性或函数)可以被覆盖)(后者显式说明该属性或方法是覆盖的)
12. if else
:可以做表达式,也可以做语句
13. when
:when表达式或语句,语句块中->
左侧是条件表达式,右侧是满足条件时执行的内容.结合else
来满足所有情况(如果编译器能检测出所有情况已经满足,可以没有else).
14. in
和!in
:判断左侧内容在|不在右侧的某个集合或区间中;循环中in
用来遍历
15. is
和!is
:判断左侧内容是|不是右侧指定的类型
16. for
循环: 类似Java,更强大,只要右侧的类型实现了Iterable
,即他有一个operator fun iterator()
成员或扩展方法即可遍历.
17. operator
:操作符重载,非常强大,这样任何对象都可以使用各种操作符了
18. while|do while
:类似Java
19. break|continue
:类似Java,但是可以加标签,定义标签是标签@
,使用是@标签
20. class
:声明类
21. constructor
:声明构造函数(包括主和次构造函数)
22. interface
:声明接口,和Java一致
23. abstract
: 声明抽象类和抽象方法和属性(类似Java)
24. data
: 声明一个数据类,默认实现equals()/hashCode()/toString()/componentN()/copy()这些方法
25. sealed
:修饰类表示这是一个密封类,类似枚举,其实就是限制死了所有子类的类.
28. object
:用作对象表达式(类似Java的实例化匿名类)和对象声明(单例模式)
29. companion object
:伴生对象(表现层类似Java的静态方法和变量,但是真正底层机制不一样,但是初始化时机一样),每个类最多有一个伴生对象.
30. by
:类委托(组合代替继承),该关键字后面跟着一个对象实例(这个对象对象一般来源于类的主构造函数的参数),这样就不用继续实现超类型的一些委托给委托对象的方法了;属性委托(延迟属性,可观察属性,属性值存储到Map中等)
31. inline|noinline
:内联和不内联.
32. crossline
: 用来修饰函数参数,内联中的特殊情况,用来表示调用函数参数的特殊情况,就是告诉你,就算是内联的,你的return也退出不了方法,因为我的方法中调用你的lambda也是在另外一个函数字面量里面.
33. typealias
: 类型别名,用来缩短类型名称,也可以为函数类型,嵌套类和内部类设置别名.
34. it
: 在单个参数的lambda表达式中代表参数,这样就不用写it->
这段了
35. field
: 属性自定义访问器中的幕后字段名称,每个属性默认有一个幕后字段的
36. external
: 与Java的native
类似.
# 符号
其他: 官方中文文档中总结的关键字与操作符
1. .
: 类似Java.
2. :
: 方法声明中后面跟着函数返回类型或参数类型;类和接口声明中后面跟着父类或接口
3. *
: 伸展操作符,将可变数量参数vararg
以命名形式传入;另外,可以展开数组(示例);另外就是泛型中的星投影
4. ?
: 放到类型(变量声明类型,函数参数类型,函数返回类型等)后面表示该变量可空.
5. !!
:强制调用一个可空属性的方法或属性,有可能导致NPE;
6. ?.
:安全调用,非空时调用,否则返回null
7. ?:
:Elvis表达式,左侧为空则使用右侧的结果,类似if(不为空)a else b
中的else.
8. ..
:区间(1..10)是一个区间,配合一些标准库函数until,downTo,step等可以进行区间遍历.
9. ===|!==
:引用相等,即指向同一个对象
10. ==|!=
: 解构相等,类似Java的equal()
但是是做了非空判断的.
11. <>
: 声明泛型类型;子类解决超类型接口冲突时指定超类型;
//3
fun foo(vararg strings: String) { /* …… */ }
foo(strings = *arrayOf("a", "b", "c"))
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)
# Kotlin语法参考
这里都着重强调了和Java语言的异同.
包
- Kotlin的包名也是定义在文件头部,但是他不要求和真实的目录结构一致(Java必须和目录结构一致); 没有包声明则默认是无名字的包(这个和Java一样).
- Kotlin默认导入了很多包(示例),Java默认只导入了
java.lang.*
. - Kotlin导入包如果有名字冲突可以使用
as
关键字重命名 - Kotlin没有
import static
,统一用import
. - Kotlin的
import
可以导入包,类,顶层函数和属性,对象声明中的函数和属性,枚举常量.
//2. 默认导入的包
kotlin.*
kotlin.annotation.*
kotlin.collections.*
kotlin.comparisons.* (自 1.1 起)
kotlin.io.*
kotlin.ranges.*
kotlin.sequences.*
kotlin.text.*
//对于JVM平台还包括:
java.lang.*
kotlin.jvm.*
定义函数
- 基本样子
fun 方法名(参数名:参数类型=<默认值>):返回值{方法体}
- 单表达式函数体可以省略大括号,直接用
=
连接函数声明和函数体(就是那个单表达式),并且可以省略声明中的返回类型(因为可以自动推断). - Kotlin中的
Unit
对应Java的void
,并且可以省略 - Kotlin函数的参数可以指定一个默认值(参数后面跟
=默认值
,这样减少重载方法数量,调用时可以通过命名参数方式调用; - 这样,子类override的函数的默认参数值和父类一直,且不能重新指定默认参数值;
- 这导致方法参数序列中如果前面有一个默认参数值,后面没有,则调用函数时只能通过
param=value
这样的命名参数方式指定后面的参数值; - 如果最后一个是lambda表达式且调用时写到了括号外面时就可以让默认参数不传值;
- 其实只要保证不混淆即可;这里和Java区别很大
- Kotlin调用函数时可以使用
命名参数
,也就是调用的时候param=value
的形式,这样参数多且默认参数值比较多就会非常方便简洁;Java则不行,因为Java一般不保存参数名. - 如果混用
命名参数
(param=value
方式)和位置参数
(传统的参数调用方式),则位置参数都要放在前面(否则容易引起混淆) - 函数的
中缀表示法
:条件:是成员函数或扩展函数;只有一个参数;用infix关键字标注(示例). - 函数作用域: 函数可以声明在文件顶层作为
顶层函数
而不需要类包裹;函数可以声明在函数内部,他可以访问外部函数的局部变量;函数定义在类内部(和Java一致);泛型函数(在函数名前面用尖括号指定);内联函数;扩展函数;高阶函数和Lambda表达式(这几个参考后面的介绍);尾递归函数(tailrec
修饰):编译器改为循环形式,首先你必须是一个递归函数才行(且递归调用是最后一个操作且他不能再try/catch/final
中,只在JVM后端支持);
// 给 Int 定义扩展
infix fun Int.shl(x: Int): Int {
……
}
//调用
1 shl 2
高阶函数和lambda表达式
- 高阶函数就是将函数作为参数或返回值的函数(示例)
- lambda表达式:(1)
lambda
表达式总是被大括号括着;(2)其参数(如果有的话)在->
之前声明(参数类型可以省略);(3)函数体(如果存在的话)在->
后面。 - 如果lambda表达式是最后一个参数,就可以把它放在圆括号外面(后面),如果只有一个lambda参数,则可以省略圆括号
- 如果
函数字面值
(看6.
)只有一个参数,可以省略->
,并且这个参数名称为it
.这些约定可以写成LINQ-风格
的代码. - (1.1起)lambda表达式
->
左侧的参数列表中,可以用_
代替不想使用或未使用的参数名称; 函数字面值
包括lambda表示
和匿名函数
(看后面9.
),即一个未声明的函数,但是可以立即作为表达式传递.- 函数类型: 高阶函数中作为参数或者返回值的函数需要指定函数类型,格式是
(参数列表)->返回类型
这个. - lambda表达式中返回值是最后一个表达式的结果,如果要用return,麻烦
@标签
,否则就从函数出去了. 匿名函数(示例)
:就是没有函数名的函数;lambda表达式语法缺少一个指定函数返回类型的能力,虽然这很多情况是不需要的.这时候我们可以使用匿名函数;匿名函数参数都必须放到圆括号,不能放到外面;一个不带标签的return会从lambda表达式所在的函数返回,但是对于匿名函数他只会从匿名函数中返回.- 闭包:lambda表达式和匿名函数(以及局部函数和对象表达式)可以访问其
闭包
,闭包即在外部作用域中声明的变量,Kotlin还可以修改闭包中捕获的变量(Java不能修改)(示例). - 带接收者的函数字面值.就是可以给函数字面值指定
接收者对象
(类似于扩展函数)(应用是类型安全的Groovy-风格构建起)(参考后面的内容,下面也有示例),这样在函数体中就可以访问接收者对象的成员;如果接收者可以从上下文推断出来,lambda表达式可以用作带接收者的函数字面值(示例);匿名函数也可以指定接收者类型
//1. 高阶函数典型示例:lock(),还有一个典型例子:List.map().filter()这种
fun <T> lock(lock: Lock, body: () -> T): T {
lock.lock()
try {
return body()
}
finally {
lock.unlock()
}
}
//调用
val result = lock(lock, { sharedResource.operation() }) //传入lambda表达式
val result=lock (lock) { //将lambda表达式放到外面
sharedResource.operation()
}
//或者
fun toBeSynchronized() = sharedResource.operation() //定义资源操作的函数
val result = lock(lock, ::toBeSynchronized)
//9. 匿名函数
fun(x: Int, y: Int): Int {
return x + y
}
ints.filter(fun(item) = item > 0)
//10. 闭包:下面的两个lambda表达式可以访问比包中的sum字段
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)
//11. 带接收者的函数字面值
//接收者为Int
fun test(sum: Int.(other: Int) -> Int): Int {
return 1.sum(5)
}
//在main中调用
val testR = test({
this + it
})
println("testR=$testR") //结果为: testR=6
//lambda表达式做带接收者的函数字面值,Groovy风格示例
class HTML{
fun body(){...}
}
//这个init函数指定给了HTML类;所以你就不能直接调用init()了,而是必须通过HTML的实例调用init(),另外,对应的函数体实现中就可以直接调用HTML的成员了.
fun html(init:HTML.()->Unit):HTML{
val html=HTML() //创建接收者
html.init() //将该接收者对象传给lambda
return html
}
//调用结果
html{ //带接收者的lambda由此开始
body() //调用该接受者对象的一个方法
}
//匿名函数设置接收者
val sum = fun Int.(other: Int): Int = this + other
val r = 1.sum(2)
println(r)
内联函数
- 高阶函数会带来一定的性能损耗:每个函数都是一个对象且都会捕获闭包. 内存分配和虚拟调用都会导致时间.所以可以通过内联化的lambda表达式来消除这些开销.如前面的例子
lock(lock){foo()}
并没有创建新函数对象和调用,而是直接将lock()函数代码内联进来,要达到这个目的,我们需要用inline
修饰这个lock()函数. 内联会影响函数本身和传递他的lambda表达式,如果不想一些一些参数内联,可以使用noinline
非局部返回
:lambda如果是内联的,那么裸return也是允许的,因为他返回出他内联以后的外部函数,这种称为非局部返回
.crossline
: 如果一些内联函数,调用的时候,他们的某些函数参数的调用在另外一个上下文(如局部对象和嵌套函数中),这种情况下,这个lambda表达式中也不能有非局部控制流.所以,为了标识这种情况,这个lambda表达式参数就需要加上crossline
修饰符(示例):也就是我调用的地方是在局部对象或者嵌套函数里面,所以就算你把外层内联了,这个lambda参数还是不在inline函数的函数体中,所以还是不能用局部控制流,用这个关键字去标识这种情况.- 具体化的类型参数
reified
. 内联函数支持具体化的类型参数(示例),而且函数是内联的,所以这里也不会用到反射. - 内联属性(1.1起):用来修饰没有幕后字段的属性的访问器,其实属性可以理解为一个字段加get和set方法,所以属性本来应该可以内联.
- 如果一个内联函数是
public
或protected
时,他会被认为是模块级别的API.可以在其他模块调用,也可以内联这个调用.这样就会导致二进制兼容风险:声明了一个内联函数但是调用他的模块在他修改后并没有重新编译. 所以为了消除这种非公有API变更引入的不兼容风险,公有API内联函数体内不允许使用非公有声明,即不允许使用private和internal声明以及其部件;一个internal声明可以由@PublishedApi标准,就允许他在公有内联API中使用.
//3. crossline
inline fun f(crossinline body: () -> Unit) {
val f = object: Runnable {
//这个body()是在这个对象表达式里面调用的,所以body的lambda表达式里面还是不能使用类似裸return这种语句.
override fun run() = body()
}
// ……
}
//4. 具体化的类型参数.
inline fun <reified T> TreeNode.findParentOfType(): T? {
//这里的T用reified修饰,这样,这里面可以直接通过`p is T`这样的形式去判断类型
var p = parent
while (p != null && p !is T) {
p = p.parent
}
return p as T?
}
//调用方法
myTree.findParentOfType<MyTreeNodeType>()
目前内联的lambda表达式还不支持
break
和
continue
,不过后续会支持.
泛型函数
就是使用了泛型的函数,参考泛型章节
,泛型基础理解是和Java相同的,但是相比Java要更好用些.
协程(实验性的)
略
属性与字段
val
只读;var
可变;Java没有.Java有final
- 声明属性的完整语法(示例):属性定义如果可以从初始器或getter中推断出来则可以省略.初始器如果没有设置则必须在构造函数中初始化;getter和setter部分是
自定义访问器
- 改变访问器的可见性可以注解或者只定义访问器而不实现(示例)
- Kotlin中的类不能有字段,但是自定义访问器可以有一个幕后字段(backing field)为
field
;只要有一个访问器使用了默认实现或者自定义某个访问器中用到了field
,就会为该属性生成一个幕后字段(所以说不是每个属性都有幕后字段,这个幕后字段类似Java的private成员变量),属性类似Java的一个成员变量+get和set方法组合. - 初始器的结果直接存储到幕后字段.
field
只能在访问器中使用. 幕后属性
: 私有的属性定义,然后一个公有的属性的访问器中为该私有属性赋值或返回;类似Java的set和get.不过多定义了一个属性还是有一丢丢的开销的(这里幕后属性的调用会被优化:应该是可以直接内联调用而不是调用get和set方法)编译器常
量:可以用到注解中;他们需满足: 顶层或某个Object的成员,用String或者原生类型初始化;没有自定义getter.延迟初始化属性
: 如果想一个属性非空,但是暂时也没法给他赋值,就可以使用lateinit
修饰,限制: (1)只用在类体中的var属性且该属性没有自定义访问器;(2)该属性必须是非空类型;(3)该属性不能是原生类型. 如果在初始化之前使用了他就会抛出特定异常表明该属性被访问时还没有初始化.覆盖属性
:必须用open
修饰,子类才能覆盖,且子类必须用override
修饰.委托属性
: 包括可观察属性(监听器会收到这个值变更的通知);延迟属性(第一次访问时计算);把多个属性存储在map中,而不是每个存在单独的字段中.这块内容比较多(参考后面的章节).
//2 属性定义完整语法
var <propertiyName>[:PropertyType][=<初始器>]
[<getter>]
[<setter>]
//3. 改变访问器的可见性
var setterVisibility:String="abc"
private set //set是私有的且有默认实现
var setterWithAnotation:Any?=null
@Inject set //??? 这个注解需要导入
注释
Kotlin的注释和Java的一样;区别一点是Kotlin的块注释可以嵌套.
基本类型
- Kotlin中所有东西都是对象.
- Kotlin基本类型: 数字,字符,布尔,数组,字符串
数字
:Double,Long是64位,Int,Float是32位,Short是16位,Byte是8位.字面常量
: 123(十进制),0x0F(16进制),0b00000101(二进制),不支持八进制;默认是Double类型:123.5,1.23e10. 123.4f(Float)- 数字字面量中的下划线(1.1开始,同Java8)
- Java平台数字是存储为JVM原生类型的.除非我们使用可空引用(如Int?)或者在泛型中使用,才会装箱,这点Kotlin的底层实现和Java是一样的.
- 显式转换: 一个Long和Int即使是一个值,判断
==
也是false(调用equals()也是false,因为他和==
是等价的).所以需要手动转换(这点比较烦啊). - 运算: 数字运算的标准运算集会被定义为相应的类成员,但是编译器会优化为响应的指令,运算符重载).位运算需要使用特定的中缀方式调用命名函数(位运算只适用于Int和Long):shl,有符号左移,ushr,无符号右移等等.
- 字符:
Char
不能直接当数字,用单引号val c='1'
,支持转义序列有t
,\b
,\r
,\n
,\"
,\'
,\\
,\$
,其他字符要用Unicode转义序列语法:如\uFF00
. 转Int使用c.toInt()
- 布尔:
Boolean
:true|false. 内置的布尔运算:||
,&&
,!
. - 数组:
Array
类表示数组. 它定义了get和set函数(对应的运算重载符为[]
),他也有一个operator iterator()
函数,所以支持迭代; - 数组可以使用arrayOf()并传递元素来创建;arrayOfNulls(n)创建元素都为空指定大小的数组.
- Kotlin中数组是不
型变
(invariant
)的,也就是Array不能赋值给Array(但是Java是可以的,也就是一个Integer[]数组可以赋给一个Object[]),这是为了防止运行时的失败(参考泛型部分的类型投影
). - Kotlin支持不需要装箱的原生类型数组:
ByteArray
,IntArray
,ShortArray
等,他们本身和Array没有继承关系,但是有类似的方法属性集和工厂方法(示例).
//7. 显式转换
val a: Int = 3
val b: Long = a.toLong() //这里必须显式toLong()
//13. 数组
var arr=arrayOfNulls<Int>(3)
val arr2=arrayOf(1, 2, 3)
val array = Array(5, { i -> i*i }) //i是数组索引,5是数组大小
//15. Kotlin支持的原生封装类数组(没有装箱开销)
val intArrayOf = intArrayOf(1, 3, 4)
字符串String
- Java不支持字符串模板.
- 通过
$property_name
或者${表达式}
可以在字符串中访问或执行语句. - 字符串可以直接在for循环中使用(示例)
- 转义字符串
""
: 可以包含转义字符\
;原生字符串
用"""
三个引号括起来,内部没有转移并且可以包含换行和任意其他字符(原生字符串默认使用|
作为边界,你可以通过参数传入其他):这个边界只是会让每行边界左边的空白符不存在而已,也就是他是每行的开头.而且边界前面不能用非空白字符,否则他就不是边界了. - 原生字符串和转移字符串都支持模板,但是原生字符串没有转义字符,所以要想在原生字符串中打印出
$
需要使用${"$"}
这样.
//3. for迭代字符串
for(c in str){
println(c)
}
//4. 原生字符串
val text="""
|tell me and i forget.
|teach me and I rememer.
""".trimMargin()
控制流
if表达式和if语句
:Java没有if表达式,只有if条件语句,if表达式必须有else,因为表达式要返回一个值,没有else可不行).- if和else中都可以是代码块,最后一个表达式为该块的值.(示例)
when表达式和when语句
:会顺序比较,直到某个条件满足则终止 类似C语言的switch
,和Java的switch不同,功能更强大;而且Java的switch满足条件以后没有break
还是会继续,when
更类似一个更强大的if-else
.如果是表达式,则必须覆盖所有情况,否则必须由else分支;- when的分支中,可以把多个分支条件放在一起,用逗号分隔;条件也可以是一个表达式;也可以使用
in
,!in
这样的判断是否在某个集合或区间中;使用is
,!is
判断是否是某个类型,判断完毕,后面的执行语句就不需要像Java那样的强制转换而直接使用转换后的类型. for
循环: 可以对任何提供了iterator
迭代器的对象进行遍历(这个对象提供了一个成员函数或者扩展函数iterator()
,它的返回类型有成员或扩展函数next()
和hasNext():Boolean
,三个函数都被operator
修饰),比如集合,区间,数组(数组的遍历会被优化为索引访问而不是迭代器),字符串(字符串的遍历应该也是索引而不是迭代器)等,数组包含索引的访问(示例).while
循环和do while
: 类似Java- 循环中的
break
和continue
: 返回和跳转: Kotlin中有三种跳转表达式:return
:默认从最直接包围他的函数或者匿名函数返回;break:终止最直接包围他的循环;continue:继续下一次最直接包围他的循环. 这些表达式的类型都是Nothing
(参考异常那一节) - Kotlin中任何表达式都可以加标签(标识符加
@
符号,如label@),这样如果有多层循环,我们要跳出或继续外层的循环就可以在break或者continue后面加@标签名
(如break@label)即可(示例). - Kotlin中有函数字面量/局部函数/对象表达式,因此Kotlin的函数是有嵌套的,所以return@标签可以让我们从lambda表达式返回,而不是包围他的函数或匿名函数(先给那个lambda加上标签即可);当然可以使用隐式标签,即标签名和接收他的函数同名;可以将lambda替换为匿名.
val c= if(a>b) a else b
val max=if(a>b){
print("choose a")
a //这里不能是return,下同
}else{
print("choose b")
b
}
//5. 数组遍历
for (i in array.indices) {
print(array[i])
}
for ((index, value) in array.withIndex()) {
println("the element at $index is $value")
}
//8. 标签
loop@ for (i in 1..100) {
for (j in 1..100) {
if (……) break@loop
}
}
//9. return@标签
fun foo() {
ints.forEach lit@ {
if (it == 0) return@lit
print(it)
}
}
fun foo() {
ints.forEach {
if (it == 0) return@forEach //这样return的就是lambda表达式而不是foo()了
print(it)
}
}
可空值以及null
检测
参考: 空安全
1. Kotlin从类型系统层面防止NPE
,Java中没有空安全,只能通过if判断或者使用Optional包装.
2. Kotlin中NPE可能发生:(1)主动throw NullPointException;(2)使用了!!
操作符;(3)外部Java代码导致;(4)初始化中数据不一致导致.
3. 如果某个变量可以为null,必须在声明处的类型后面加?
表示该变量可以为null
(包括成员变量,函数参数,函数返回值等)
4. 访问可空变量的属性和方法的方式:(1)if
条件检查非空中执行(适用于变量不可变时,否则if判断结束以后,其他线程修改变量为null,则还是会发生NPE);(2)安全调用?.
:表示只有非空时才会调用,否则返回null
(示例);(3)Elvis操作符:即如果非空则使用它,否则使用指定的非空值(示例)(4)!!
操作符:强制访问,可能产生空指针.
5. 安全的类型转换as?
,如果不是指定类型则返回null
6. 可空类型的集合:通过filterNull
来实现过滤非空元素(示例).
//4. ?.使用
val name:String?=tom?.department?.head?.name
//4. ?:使用
val age:Int=tom?.age?:-1
//6. 可空类型的集合过滤非空元素
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
类型检查与转换
- 判断:
is
和!is
,Java是instanceOf
- 转换:
as
- 智能转换:只要编辑器能确定了类型,后面就立马可以按照这种类型来操作而不需要显示转换.比如在if中,甚至在
&&
或者||
右侧,还有在when
和while
中. - 安全的转换符:
as?
:表示如果是指定类型,则转换成功,否则返回null. 不安全的转换:建议左右两侧都加上?
//4. 转换:有可能抛异常
val x: String = y as String
//4. 不安全的和安全的转换
val x: String? = y as String?
val x: String? = y as? String
区间
- 区间
<start>..<end>
,对应函数是rangeTo
,还可以在迭代是指定步长step
,可以逆向迭代downTo
(9 downTo 0),如果不想包含结束元素,可以使用until
而不是..
. - 使用
in
和!in
来检测是否在(或不在)某个区间中,对于整型区间(IntRange,LongRange,CharRange),编译器会进行优化实现,转换为基于索引的for循环. - 区间实现了公共接口
ClosedRange<T>
表示一个闭区间.他有两个端点start
和endInclusive
. 主要操作是contains
(这个方法使用operator修饰的),这个操作一般以in/!in
形式使用. 整型数列(如IntProgression)表示等差数列.他由first
,last
,非0 的step
定义. 这里可以直接写step 是因为step()是一个infix函数,所以可以通过这种方式调用(中缀表达式). 而且他是Iterable
的子类型,所以可以迭代. 这些操作符后面都藏着一个函数,所以也可以认为是函数调用,只是一些特殊情况会被编译器优化. - Java中是没有区间这个概念的.
//2. 区间迭代 [1,10),步长为2,打印结果为:13579
for (i in 1 until 10 step 2) {
print(i)
}
集合
- 有一些列标准库函数可以快速创建集合.
- 集合可以迭代,可以使用
in
,可以使用lambda
表达式来过滤(filter)或者映射(map). - Kotlin区分只读和可变即可.如List他是只读的,可变的是
MutableList
.Kotlin中提供了一些标准库方法来创建List,Set,Map. 不可变集合是协变的,可变集合不是. - List和Set有很多扩展方法,如first(),last(),filter(),reduce(),sort(),zip(),fold()等
- Map也类似,如hashMapOf()
类和继承
- 使用
class
关键字声明类.最简单的类class Person
- 构造函数,用
constructor
关键字声明. - 主构造函数:跟在类名后面,如果主构造函数没有注解和可见性修饰符,则可以省略
constructor
关键字.主构造函数不包括任何代码,初始化的代码可以放到类体中的init{}
初始化代码块,主构造方法的参数可以在init
中使用,也可以在类体中声明的属性中使用. 主构造函数的参数声明的是属性,和一般属性一样,他可以指定默认值,可以设置val
,var
,但是必须指定类型,类体中的属性如果可以推断出类型可以不指定. - 次构造函数: 声明前缀为
constructor
的为次级构造函数,每个次级构造函数必须直接或者间接调用主构造函数,通过在属性列表后面+:
+主构造函数调用.次级构造函数的属性不能用var|val
修饰(WHY???). - 可以通过
private
修饰constructor,这样,别人就访问不了这个构造函数了.如果所有主构造函数的参数都有默认值,那么编译器会生成一个无参的使用默认值的构造函数. - 创建类的实例: Kotlin没有
new
关键字.调用类似函数调用. - 类成员可以包括:(1) 构造函数和初始化块;(2)函数;(3)属性;(4)嵌套类和内部类;(5)对象声明(包括伴生对象);
- 继承:(1)所有类默认继承超类
Any
.Any
不是java的Object
.他除了equals()
,hashCode()
和toString()
以外没有任何方法;(2)设置父类的方法,将类型放到类头的冒号之后(示例),而且如果父类有构造函数,设置继承的时候也要对应的调用父类的构造函数,没有主构造函数,次构造函数也要直接或间接通过super
调用父类构造函数.默认Kotlin中的所有类和方法,属性等都是final
的,除非用open
修饰才能继承或重写;(4)var属性可以重写val属性,反之则不行.(5)重写的属性和方法可以通过super
调用父类的属性或方法.(6)内部类访问外部类的超类实现(通过super@标签
,标签是外部类名. - 抽象类:通过
abstract
修饰,这个时候不需要再加open
了.可以使用抽象成员覆盖非抽象成员(如抽象类的父类是一个非抽象类)(Java里面也是可以的) - 伴生对象:参考后面
//8. 子类
open class Base(p:Int)
class SubClass(p:Int):Base(p)
接口
interface
声明: 同Java8类似,接口可以包含抽象方法,也可以包含非抽象方法.但是无法保存状态,他可以有属性,但是必须声明为抽象的或者有访问器(访问器也不能使用幕后字段,感觉这个幕后字段和Java中的成员变量类似,所以接口中自然不能有)(这点和Java不同,Java不能有普通字段,只能有常量).- 实现接口:和继承类似,不过接口没有构造函数之类的,所以接口名后面没有括号(和参数调用)
- 解决覆盖冲突: 如果一个方法实现多个接口时,可能这多个接口包含同一个方法,这样,子类在重写调用
super
的时候就会发生冲突,所以可以通过super<A>.method()
这样,在尖括号中指定要调用的是哪个接口的方法.
可见性修饰符
- 可见性修饰符包括:
private
,protected
,internal
,public
. 他们可以修饰(类,对象,接口,构造函数,方法,属性和他们的setter,getter总是和属性有相同的可见性. - 包名: 默认是public,也就是你的声明随处可见;private的可见性是当前文件;internal的可见性是相同模块.protected不适用于顶层声明.
- 类和接口: 类内部的成员:private仅在类内部可见;protected+子类可见;internal+本模块可见;public:随处可见.覆盖一个protected成员,默认他也是protected
- 构造函数: 在
constructor
前面添加.默认构造函数是public(类可见的地方他就可见),可以设置为private. - 局部声明:不能用可见性修饰符
- 模块:一个模块是编译在一起的一套Kotlin文件:如一个IDEA模块;一个Maven项目;一个Gradle源集;一次 Ant任务执行编译的一套文件.
扩展函数和扩展属性
- 扩展方法:同声明方法的方式,只是方法前面多了一个类型.
fun 类型.扩展方法
. - 扩展是静态的,并没有修改被扩展的类,只是通过该类型的变量用
.
表达式去调用这个新函数而已.所以真正调用的扩展函数是调用所在的表达式决定的.不是运行时的求知结果决定的.比如有一个B集成A.两个类都实现了一个相同的扩展函数. 那么调用的时候,虽然你运行时传入的一个B(),但是你调用的时候指定的是A的话,他就调用的是A的扩展函数而不是B的. - 扩展函数无法覆盖成员函数(如果函数签名相同,不同是可以的,签名不同也就不算覆盖了).
- 扩展属性: 扩展属性不能有初始化器(因为有初始化器意味着有幕后字段,我们不可能给一个类加成员变量),他的行为只能有
getter/setter
指定. - 伴生对象的扩展: 他也可以有扩展方法和属性.
- 扩展的作用域:一般我们都在顶层定义扩展(包内),其他地方需要导入这个扩展才能使用.
- 扩展声明为成员:
分发接收者
:扩展声明所在的类的实例;扩展接收者
:扩展方法调用所在的接收者类型. 分发接受者和扩展接收者的成员名字冲突则以扩展接收者优先(当然是自己的方法优先了),有冲突可以用this@标签
的形式调用.(示例); 声明为成员的扩展函数对于分发接收者是虚拟的(可以被子类覆盖),对于扩展接收者是静态的. - 扩展函数的目的是为了减少各种
Utils
工具类导致的非常不友好不简洁的写法.
//7. 分发接收者和扩展接收者
class A{
fun a(){}
}
class B{
fun b(){}
fun A.foo(){ //A是扩展接受者.因为这个方法是给A的扩展
this@B.toString(); //默认调用的是A的toString(),所以要调用B这个分发接收者的toString()要用这种方式
a(); //调用A.a
b(); //调用B.b
}
fun caller(a:A){ //B是分发接受者,这个扩展函数是写到B里面的
a.foo();
}
}
this
- this表示当前接收者.
- 类的成员中,this代表当前对象
- 扩展函数或者带接收者的函数字面量中,this表示
.
左侧传递的接收者参数. - 如果this没有限定符,他指的是最内层包含他的作用域,要引用其他作用域可以用
@标签
(示例)
//2,3,4 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
}
}
}
}
数据类
- 用
data
修饰,默认实现equals()/hashCode()/toString()/componentN()/copy()这些方法. - 数据类的要求:(1)主构造函数至少有一个参数;(2)主构造函数所有参数都要标记为
var
或val
(还有其他标记?);(3)数据类不能是抽象的,开放的,密封的,内部的;(4)(1.1)之前,数据类只能实现接口.(5)如果数据类本身自己实现了toString/equals/hashCode
,或者父类中已经有final
实现(也就不是open的,子类没法覆盖了),则不会自动生成这些函数,而是用现有的.(6)如果超类有open的componentN()
且返回兼容的类型,那么会为数据类生成相应的函数,并且覆盖超类实现.如果超类的函数由于签名不兼容或者final的导致无法覆盖会报错;(7)不允许为componentN
和copy()
提供显式的实现.(8)1.1起,数据类可以从其他类扩展来.(8)copy()方法的实现是传入每个参数,且他们都有默认值(即当前类的值),所以copy()方法可以很好的去修改部分属性的值); - 数据类的结构声明: 数据类自动生成的的componentN()导致他们可以使用结构声明(示例)
- 标准库提供了
Pair
(二元)和Triple
(三元)两个数据类.
//3. 解构使用
val jane=User("Jane",34)
val (name,age)=jane //解构
println("$name,$age year of age")
密封类
- 密封类是更强大的枚举类(Java).Java的枚举的每个常量只能存在一个实例.但是密封类的一个子类就可以有包含状态的多个实例(我估计Java的枚举也是通过继承的类似的实现).
- 密封类用
sealed
修饰.密封类的子类只能和密封类在同一个文件(1.1以后,密封类的子类必须声明在密封类的内部) - 其实密封类就是一个普通的抽象类,但是他限制了你必须一次向把所有子类都生命好,其他地方就不能声明了.所以每个子类都有自己的状态. 这导致密封类的构造函数默认且只能是
private
,而且不能直接实例化.但是扩展密封类子类的类就可以放到任何地方了(也就是密封类的孙子辈他是不管的) - 所以,密封类适合用在
when
表达式中,编译器可以很容易的判断你是否已经满足了所有情况了.
枚举类
- 类似Java的枚举类,但是声明方式为
enum class Name{...}
,也就是只要是类,这个class一般都不会省的.基本用法和Java一致(比如可以有属性,有抽象方法;如果有成员需要将常量和成员之间用分号分割,每个枚举常量都有name
(名称)和ordinal
(位置)属性).
嵌套类和内部类
- Kotlin的嵌套类类似Java的static内部类,就是在类里面声明的类默认就是嵌套类,这个和Java不同,Java加了static才是Kotlin中的嵌套类;
- Kotlin的内部类使用
inner
修饰,类似Java的非static内部类,他会持有外部类的引用. - 匿名内部类使用对象表达式创建实例,就和Java的匿名内部类是一样的,只是语法不同(示例)
- 如果对象是函数式的Java接口(即单个抽象方法的Java接口),可以用带接口类型前缀的lambda表达式创建(这里说的是lambda表达式)(示例)
//3. 对象表达式,对应Java的匿名内部类
window.addMouseListener(object: MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
// ……
}
...
})
//4. 带接口类型前缀的lambda表达式
val listener = ActionListener { println("clicked") }
对象
对象表达式
对应Java的匿名内部类.方式object:超类型(..),超类型2(..){..}
.- 也可以没有超类型,如果我们需要的只是一个对象:
object{属性和方法}
. - 匿名对象可以用在本地和私有作用域中声明的类型.如果用在公有函数返回类型或者共有属性的类型,那要么明确指定超类型,没有指定就是
Any
,如果是Any
,你在匿名对象中定义的属性和方法都没法访问了(原因估计是和他的底层实现相关,他是用在本地和私有作用域的,所以如果把它public出去就没法访问了).(示例) - 类似Java匿名内部类,对象表达式中的代码可以访问包含他的作用域的变量,但不仅限final变量(Java是必须final才行)
对象声明(单例模式)
:object Name{方法和属性}
,调用可以直接用名称调用(外表看起来像静态方法和静态属性,但是其实是实例化的).- 对象声明不能在方法内部.但是可以放在其他对象声明或者非内部类中.
伴生对象
: 使用companion object Name{}
,他也是一种特殊的对象声明
(参考后面9.
的异同). 声明在类内部(每个类只能有一个伴生对象
).这样可以直接通过类去调用它的方法.可以省略半生对象的Name,这样默认用Companion
作为Name.虽然伴生对象看着像静态成员,但是他其实还是真实对象的实例成员,例如他还可以实现接口.- 可以通过
@JvmStatic
将伴生对象的成员生成为真正的静态方法和字段(Java互操作). - 对象表达式和对象声明的差异:(1)前者是立即执行,后者是第一次访问时执行的;(3)伴生对象的初始化实在类加载解析时执行的,与Java静态初始化器一致.
//3. 匿名对象
class C {
// 私有函数,所以其返回类型是匿名对象类型
private fun foo() = object {
val x: String = "x"
}
// 公有函数,所以其返回类型是 Any
fun publicFoo() = object {
val x: String = "x"
}
fun bar() {
val x1 = foo().x // 没问题
val x2 = publicFoo().x // 错误:未能解析的引用“x”
}
}
类委托
- 类委托: 委托已经被证明是实现继承的一种很好的替代方案,Kotlin可以零样板代码实现委托.其实就是
多用组合少用继承
的思路(示例). - 当然,如果我们在被委托的类中自己手动实现了某些已经委托给委托对象的方法,他肯定是优先你手动实现的内容.比如下面的例子中,在
Derived
中实现print()方法,那么委托对象b中print()就不会使用了.
//1. 类委托
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
//通过by关键字,这里把Derived本该实现Base的接口方法直接委托给参数b,这不就是组合,但是样板代码都没有了.
class Derived(b: Base) : Base by b
fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print() // 输出 10
}
属性委托
- 属性委托目前可以支持下面几种属性类型:延迟属性,可观察属性,属性存到Map中,不是单个字段中.
- 委托属性也是使用
by
关键字,属性委托委托的是属性的get
和set
到getValue()
和setValue()
中.属性委托不需要实现接口,只要提供了上面这两个方法即可(当然对于val属性时不需要setValue()
的),这两个方法的要求和说明参考(示例) - 1.1起:也可以在函数或代码块中声明一个委托属性,所以他不一定是类成员了.
- 标准委托有三个
- (1)
lazy{}
,第一次调用get()
的时候回执行lazy{}中的lambda表达式并把结果记录,后续调用就直接返回之前记录的结果(也就是不是每次获取值都调用lazy{}中的lambda.);而且这个属性的求值是加了同步锁的,如果你不想加这个可以设置LazyThreadSafetyMode作为lazy方法的第一个参数,模式有:SYNCHRONIZED(默认),PUBLICATION(可能被计算多次,但是还是第一次得出的结果有效),NONE(没有任何锁). - (2)’Delegates.observable(“初始值”){prop,old,new->}’:第一个参数是初始值,第二个参数修改处理程序.每次给属性赋值时都会调用该处理程序,其中prop是被赋值的属性,old是旧值;new是新值;如果想截获赋值操作,可以使用
votoable()
(看看源码很容易明白里面的道理). - (3)把值存储到Map中,
by map
,map是我们主构造方法中传入的map. - 局部属性委托(1.1起)
- 属性委托要求:参考下面的示例.自定义委托类可以实现标准库中提供的
ReadOnlyProperty
和ReadWriteProperty
这两个接口. - 委托属性的实现就是把by后面的对象编译到当前上下文(如类)中成为一个名为
name$delegate
的属性,然后生成委托属性的get和set方法并在其中调用delegate属性的getValue()和setValue()而已. - 提供委托(1.1起): 通过定义
provideDelegate
操作符,可以扩展创建属性实现所委托对象的逻辑.如果by
右侧使用的对象将provideDelegate
定义为成员扩扩展函数,那么会调用该函数来创建属性委托实例. 也就是by后面的对象的类中可以实现一个operator provideDelegate
即可这时的原理就变成了,编译生成prop$delegate
的时候是调用的by后面对象的provideDeleate
方法去拿到委托对象的实例.
//2. 委托对象
class Example {
var p: String by Delegate()
}
class Delegate {
//thisRef是读取`p`的对象(Example);第二个参数保存对`p`的描述
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
//前两个参数同上,第三个参数是要被设置的值
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name} in $thisRef.'")
}
}
解构声明
- 可以把一个对象解构成很多变量;(示例)
- 一个解构声明挥别编译为以下代码(示例)
- 如果需要一个类可以结构,需要实现
operator componentN()
函数(这里的意思是component1(),component2()等等). - 他的应用,比如我们想一个方法返回两个结果,我们可以返回一个数据类,然后结果直接赋给一个解构声明的样式即可.
- 可以把map的entry直接解构成一个(key,value),这在for中经常使用,因为Map.Entry实现了
componentN()
. - 可以用
_
代替不适用的变量;可以在lambda表达式的参数列表中使用_
代表不想使用的变量.lambda表达式的参数列表中可以指定解构参数的类型或者解构参数中每个变量的类型(示例).
//1. 将person解构
val (name,age)=person
//2. 编译解构结果
val name = person.component1()
val age = person.component2()
//6. `_`替代
val (_, status) = getResult()
map.mapValues { (_, value) -> "$value!" }
map.mapValues { (_, value): Map.Entry<Int, String> -> "$value!" }
map.mapValues { (_, value: String) -> "$value!" }
相等性
- 包括:引用相等(指向同一对象),解构相等(equals()),类似Java
- 引用相等用
===
以及!==
判断 - 解构相等用
==
以及!=
判断(和Java不一样),等价于a?.equals(b) ?: (b === null)
a==null
会被自动优化为a===null
- 浮点数的比较: 遵循 IEEE 754 浮点数运算标准
操作符重载
- 将各种操作符对应到一个成员函数或者扩展函数.
- 前面介绍过的属性委托操作符,中缀函数等
异常
- 基本用法类似Java,try-catch-finally,还有throw
- 但是try本身是一个表达式,即他可以有一个返回值,他的返回值是try块中最后一个表达式或者catch块中的最后一个表达式,finally块中的内容不影响结果.(Java中try只是语句)
- Kotlin中没有受检异常(Java中是有的)
- throw也是一个表达式,他的类型时Nothing. 该类型没有值,他代表一个不能达到的位置.(示例)
- 类型推断中,如果Kotlin没法检测出类型,他会认为是一个可空的Nothing(示例).
//4. 返回Nothing
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
//5. 可空的Nothing
val x = null // “x”具有类型 `Nothing?`
val l = listOf(null) // “l”具有类型 `List<Nothing?>
注解
- 注解声明:
annotation class XXX
- 元注解:
@Target
,@Retention
,Repeatable
,MustBeDocumented
,类似Java - 注解可以用在lambda表达式上,他会被应用于生成lambda表达式体的invoke()方法上.
- Kotlin的注解与Java的100%兼容
反射
- 如果要使用反射,请导入单独的jar包(kotlin-reflect.jar)到classpath中.
- 类引用: 类字面值语法:
MyClass::class
,如果要拿到Java的类引用示例,使用MyClass::class.java
::
: 类字面量引用,如String::class
,str::class
(1.1起),::function
(传参时函数引用),::属性名
(属性引用),::Foo
(构造函数引用)- 与Java的互操作,Kotlin提供了与Java反射对象之前的映射,如
A::p.javaGetter
- 绑定的函数引用和属性引用:就是把函数引用和属性引用赋给一个val变量(示例)
//5. 绑定的函数和属性引用
val isNumber = numberRegex::matches
println(isNumber("29")) // 输出“true”
val prop = "abc"::length
println(prop.get()) // 输出“3”
类型安全的构建起
官方说明:类型安全的构建器
这块挺有意思的话说,我们前面的内容举过这样的例子.
1. 构建器在Groovy社区分厂热门,可以以半声明的方式定义数据,很适合生成XML,布局UI(如Anko工具中的代替xml布局文件的代码描述),描述3D场景.
2. 很多情况下Kotlin允许检查类型的构建器,其余情况就和Groovy一样是动态类型的构建器.
这个主要使用了3个Kotlin的特性: 带接收者的lambda表达式和扩展函数,操作符重载.只要弄懂这三个类型,就很容易写这样风格的代码了 .
类型别名
- 可以用来缩短现有较长的名称(如一些泛型名称).使用
typealias
关键字,可以为函数类型提供别名;可以为内部类和嵌套类提供别名. - 类型别名并没有引入新类型,编译器最终还是替换为别名指代的真正类型.
互操作
[TODO 这块内容还挺多,后面做的时候具体看吧]
1. Kotlin中调用Java代码:大部分调用都没有问题.(2) getter和setter的约定方法在Kotlin中表现为属性,Kotlin中目前不支持只写(set-only)属性.(3) 返回void的方法在Kotlin中返回Unit.(4) 如果一些Java中的成员名字是Kotlin的关键字就可以用反引号括起来调用,如foo.is
(bar).(5)空安全的问题:Java中声明的类型为平台类型,会放宽空检查限制.(6)错误信息中可能会使用!
表示平台类型,如T!
表示T
或者T?
. 如果Java类中使用了可空性注解,就可以表示为有空检查的Kotlin类型(编译器支持很多注解,如Lombok的,Android的等).
2. Java调用Kotlin: (1)Kotlin的属性会编译成Java的getter,setter方法和私有字段.(2)包级函数:在org.foo.bar包内的example.kt中所有的包级函数会被编译成org.foo.bar.ExampleKt
的Java静态方法.
泛型
泛型都是编译阶段的,所以理解泛型的相关概念也要从编译阶段去理解.
1. 与Java类似,Kotlin中也有类型参数.调用时如果可以推断出来可以省略类型指定<类型>
.
2. 型变
: Kotlin中没有Java的通配符操作,取而代之的是声明处型变和类型投影. Java中:带extends
限定(上界)的通配符使得类型是协变的.如Collection<E>.addAll(Collection<? extends E> items)
,这样我们可以安全的从列表中取值; 反过来,如果只能往集合中放入项目,就可以用Object的集合放入String. Java中List<? super String>
是List<Object>
的超类(称为逆变性),这个集合你只能接受String作为add()和set()的参数,这时调用get拿到的类型时Object而不是String(逆变性
). 只能读取的为生产者
,只能写入的为消费者
.助记为PECS
.
3. 声明处型变
: 就是在声明的时候就告诉他这个泛型是型变的,如Source<out T>
表示(表示这里的T只会被生产,也就是只在函数返回值中出现),这样我们可以直接把一个Source<String>
赋值给Source<Any>
.另外,编译器要求,如果是out修饰泛型,那么他只能出现在类的输出位置(即返回值),即类是T的生产者而不是消费者; out修饰符称为型变注解
,另外,Kotlin还有一个<in T>
,他使得一个类型参数逆变:只可以被消费而不能被生产,比如Comparable
类(也就是只能出现在参数处,不能出现在返回值中). 消费者in,生产者out.
4. 类型投影
: 使用处型变.将类型参数T声明为out非常方便,但是很多时候又要生产又要消费.我们在函数泛型参加上out
或者in
.Array<in String>
等价于Java的Array<? super String>
.Array<out String>
等价于Java的Array<? extends String>
(示例)
5. 星投影[TODO 这块还不是太明白,后续补充]
6. 泛型函数,类型参数放在函数名称之前;调用的时候放在函数名之后的<>
之中.
7. 泛型约束: 通过在泛型后面加:
和类型来限制上界,如果需要多个,则在函数声明后面使用单独的where子句(示例).
8. Java中的泛型通配符:
(1)无边界的通配符<?>
,如List<?>
:
主要是为了让泛型可以接受未知类型的数据;比如我的一个方法是打印所有的列表元素,如果声明参数类型为List<Object>
,那么他只能接受List的参数而不能接受List<String>
的参数;但是如果设置为List<?>
的话,就可以接受List<String>
,List<Integer>
等等的参数;
但是你不能往List<?>
中添加类型,因为你确定List的类型,所以只能添加null;
从List获取到的类型都是Object类型.
(2)包含上界的通配符<? extends MyType>
;固定上界可以明确生产对象的类型;
固定上界以后,我们拿到的肯定是MyType或者他的子类,所以可以放心的从里面获取MyType类型进行操作,但是消费(即set)的话,我们就不知道对象里面到底要存的是什么;核心是这个?
,因为我们确定了列表中的元素一定是MyType的子类,所以读取的时候就可以拿到MyType,但是这个?具体是哪个子类不知道,所以就不能add
,我之前一直把核心放到了extends后面的类型,所以理解上下界就很糊涂.
(3)包含下界的通配符<? super MyType>
: 固定下界可以明确消费对象;
固定下界指的是规定了元素的最小粒度.比这个粒度小的都可以存,但是往外取时就只能拿到Object. 也就是包这个super保证容器元素至少可以存储MyType
类型,所以你可以很安全的放入MyType,但是存MyType的超类型那不好说了,鬼知道?指的是哪个超类型;因此获取的时候就只能拿到一个Object.
(4)理解为什么这个技巧能够工作的关键相当简单:如果只能从集合中获取项目,那么使用String
的集合, 并且从其中读取Object
也没问题 。反过来,如果只能向集合中 放入 项目,就可以用Object
集合并向其中放入 String
:在 Java 中有List<? super String>
是 List<Object>
的一个超类。
//4. 类型投影
//这里给from加上out,他就只能调用返回类型为T的方法而不能set()
fun copy(from: Array<out Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
//7. 泛型约束
fun <T : Comparable<T>> sort(list: List<T>) {
// ……
}
fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
where T : Comparable,
T : Cloneable {
return list.filter { it > threshold }.map { it.clone() }
}