概述
- Kotlin是面向对象的静态类型语言;
- 在Kotlin中,所有东西都是对象,在这个意义上可以在任意变量上调用成员函数与属性;
- Kotlin优点
- 简洁
- 提供了大量的语法;
- 减少样板代码;
- 安全
- 减少空指针判断;
- 类型支持可空;
- 互操性
- 协程
- 简洁
- 来源于 Kotlin1.3.50 官方文档
基础
- 包
- 包的声明位于源文件顶部;
- 目录与包的结构可以无需匹配;
- 源文件的所有声明(包括类,函数,变量)都包含在包的声明中;
- 以下包会默认导入Kotlin文件
kotlin.*kotlin.annotation.*kotlin.collections.*kotlin.comparisons.*kotlin.io.*kotlin.ranges.*kotlin.sequences.*kotlin.text.*
- 根据目标平台会导入额外的包
- JVM
java.lang.*kotlin.jvm.*
- JS
kotlin.js.*
- JVM
- import
- 可以导入指定Kotlin文件;用来导入类,顶层函数和属性,在对象声明中声明的函数和属性,枚举常量;
- 如果名字冲突,可以用
as起个别名;
- 基本类型
- Kotlin中基本类型有:数字,字符,布尔,数组与字符串;
- 数字
- 对于整数有四类:
ByteShortIntLong,对应字节长度为8163264;不显式指定类型时,未超过Int最大值的整数被推断为Int类型,否则推断为Long,除非后缀L显式指定Long类型; - 无符号整数类型:
UByteUShortUIntULong;字面量整数后缀uU表示无符号类型; - 对于浮点数有两类:
FloatDouble,对应字节长度为3264;默认推断为Double,除非后缀f/F显式指定Float类型; - 在Java平台,数字是物理存储为JVM中的原生类型;
- 两个数字对象保留了相等性(
==),但不保留同一性(===); - 数字之间没有隐式转换(小类型隐式转换为大类型);
-
+-*/%可用于数字间运算; - 位运算
- 没有特殊字符用于位运算;只可用中缀方式调用具名函数;只能用于
IntLong; -
shl(bits)- 有符号左移 -
shr(bits)- 有符号右移 -
ushl(bits)- 无符号左移 -
ushr(bits)- 无符号右移 -
and(bits)- 位与 -
or(bits)- 位或 -
xor(bits)- 位异或 -
inv()-位非
- 没有特殊字符用于位运算;只可用中缀方式调用具名函数;只能用于
- 逻辑运算
-
===== -
<<=>>= -
in a...b!in a...b
-
- 对于整数有四类:
- 字符
- 用
Char表示,字面常量用单引号括起来,支持转义字符和Unicode转义序列;
- 用
- 布尔
- 用
Boolean表示,有两个值truefalse; - 逻辑运算有
||&&!;
- 用
- 数组
- 数组用
Array表示,定义了operator fun get(index: Int): Toperator fun set(index: Int, value: T): Unit函数; - 数组类型不是协变的,不同于Java中的数组类型;
- 有专门的原生数组类型:
ByteArrayIntArray等等;无符号原生数组类UByteArrayUShortArrayUIntArrayULongArray;
- 数组用
- 字符串
- 字符串用
String表示,字符串的元素--字符可以用索引表示s[1]; - 字符串模版
- 字符串用
- 变量
-
val用来定义只读变量,只能为其赋值一次;var用来定义可修改变量;
-
- 控制流
-
if-
if也可以当作表达式;当if当作表达式(返回一个值)而不是语句时,需要else分支;
-
-
whenwhen (x) { 1 -> print("x == 1") 2 -> print("x == 2") else -> { // 注意这个块 print("x is neither 1 nor 2") } } when (x) { 0, 1 -> print("x == 0 or x == 1") else -> print("otherwise") } when (x) { in 1..10 -> print("x is in the range") in validNumbers -> print("x is valid") !in 10..20 -> print("x is outside the range") else -> print("none of the above") }-
when将它的参数与所有的分支条件顺序比较,直到某个分支满足条件;when既可以当作语句,也可以当作表达式;作为表达式,必须有else分支;
fun Request.getBody() = when (val response = executeRequest()) { is Success -> response.body is HttpError -> throw HttpException(response.status) }- 自 Kotlin 1.3 起,可以使用以下语法将
when的主语(subject,译注:指 when 所判断的表达式)捕获到变量中;在when主语中引入的变量的作用域仅限于when主体;
-
-
forfor (item in items) { ... } for (index in 1...100) { ... }- 可以使用
in关键词;
- 可以使用
-
while-
whiledo while和其他语言一样;
-
-
returnbreakcontinue-
return默认从直接包围的函数或者匿名函数返回;支持返回标签函数; -
break默认从直接包围循环跳出;支持跳出标签循环; -
continue默认进入直接包围循环的下一次迭代;支持进入标签循环的下一次迭代;
-
-
- 表达式
- 函数
- Kotlin中函数是头等公民;这表明函数可以存储在变量和数据结构中,作为参数传递给高阶函数或者从高阶函数中返回;
- 函数声明
fun sum(a: Int, b: Int): Int { return a + b }- 定义了函数名为
sum,参数a为Int类型b为Int类型,返回类型为Int的函数; - 如果返回类型为无意义的值,用
Unit表示,可省略返回类型;return语句可推断返回类型时,也可省略显式声明的返回类型;
fun sum(a: Int, b: Int) = a + b- 用表达式表示函数体;
- 定义了函数名为
- 函数参数
- 函数参数可以有默认值;调用时可以不传递有默认值的参数;
- 最后一个参数可以是可变数量参数,用
vararg前缀;
- 函数作用域
- 函数可以在顶部声明,也可以在类中声明,还可以在函数中嵌套声明;
- 局部函数是在一个函数内声明的函数,可以访问外部函数(闭包)的局部变量;
- 扩展函数
- 中缀函数
- 中缀函数用
infix表示; - 中缀函数必须只有一个参数,不能有默认值,不能是可变数量参数;
- 中缀函数用
- 作用域函数
- Kotlin 标准库包含几个函数,它们的唯一目的是在对象的上下文中执行代码块;当对一个对象 调用这样的函数并提供一个 lambda 表达式时,它会形成一个临时作用域;在此作用域中,可 以访问该对象而无需其名称,这些函数称为作用域函数;包括
letrunwithapplyalso;这些函数基本上做了同样的事情:在一个对象上执行一个代码块;不同的是这个对象在块中 如何使用,以及整个表达式的结果是什么; - 每个作用域函数之间 有两个主要区别:
- 上下文对象
thisit- 在作用域函数的 lambda 表达式里,上下文对象可以不使用其实际名称而是使用一个更简短的引用来访问;
- 每个作用域函数都使用以下两种方式之一来访问上下文对象:作为 lambda 表达式的接收者( this )或者作为 lambda 表达式的参数( it );
runwithapply中上下文对象为this,letalso中上下文对象为it;
- 返回值
applyalso返回上下文对象;letrunwith返回lambda表达式结果;
- 上下文对象
- Kotlin 标准库包含几个函数,它们的唯一目的是在对象的上下文中执行代码块;当对一个对象 调用这样的函数并提供一个 lambda 表达式时,它会形成一个临时作用域;在此作用域中,可 以访问该对象而无需其名称,这些函数称为作用域函数;包括
- 高阶函数
- 高阶函数是将函数用作参数或者返回值的函数;
- 函数类型
- 函数类型声明
(A, B, C,...) -> R A.(B, C,...) -> R- 以上表示一个函数类型,其中A,B,C表示函数的参数类型,R表示返回类型;
- 函数类型可以有一个额外的接受者;
- 函数类型实例化
- 使用函数字面值的代码块
- lambda表达式
- 匿名函数
- 使用已有声明的可调用引用
- 顶层,局部,成员,扩展函数:
::isOdd()String::toInt; - 顶层,成员,扩展属性:
List<Int>::size; - 构造函数:
::Regex;
- 顶层,局部,成员,扩展函数:
- 使用实现函数型接口的自定义类的实例
- 使用函数字面值的代码块
- 函数类型实例调用
- 函数类型的值可以直接调通过操作符
invoke(...)调用:f.invoke(x)或者f(x);
- 函数类型的值可以直接调通过操作符
- 函数类型声明
- lambda表达式
- lambda表达式与匿名函数是 函数字面值,即未声明函数,但是可以立即作为表达式传递;
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }-
->前面是函数参数,后面是函数体; - 如果函数的最后一个参数是函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外;也叫尾式 lambda 表达式;如果函数只有一个函数作为参数,圆括号可以省略;
- 如果 lambda 表达式只有一个参数,可以省略 参数声明,直接用
it表示; - lambda 缺少返回类型(虽然可以推断出来),但是匿名函数可以显式指定返回类型;
- lambda 表达式或者匿名函数,以及局部函数和对象表达式,可以访问闭包,即在外部作用域中声明的变量;
- 带有接受者的函数字面值;
- 内联函数
- 使用高阶函数,会带来运行时的效率损失:每个函数都是一个对象,并且会捕获一个闭包,内存分配(对于函数对象和类)和虚拟调用都会引入运行时开销;
- 类与对象
- 类
- 类用
class表示;类声明由类名,类头(类型参数,主构造函数)和类体构成,类头和类体是可选的; - 类的成员包括
- 构造函数与初始化块
- 函数
- 属性
- 嵌套类与内部类
- 对象声明
- 类用
- 主构造函数
class Person constructor(firstName: String) { /*……*/ }- 主构造函数是类头的一部分;如果主构造函数没有任何注解或者可见性修饰符,则可以省略
constructor关键字;主构造函数不能包含任何的代码; - 初始化代码可以放在
init开头的代码块中;在实例初始化期间,初始化块按照在类体中出现的顺序执行,与属性初始化器交织在一起;主构造器的参数可以在初始化块和属性初始化器中使用;
- 主构造函数是类头的一部分;如果主构造函数没有任何注解或者可见性修饰符,则可以省略
- 次构造函数
- 如果有主构造函数,那么次构造函数必须委托给主构造函数或者另一个次构造函数(
:this()); - 委托给主构造函数会作为次构造函数的第一条语句,及时没有主构造函数,初始化块和属性初始化器也会隐式委托;
- 如果有主构造函数,那么次构造函数必须委托给主构造函数或者另一个次构造函数(
- 实例
- 没有
new关键字;
- 没有
- 属性
var <propertyName>[: <PropertyType>] [= <property_initializer>] [<getter>] [<setter>]- 初始化器(
initializer)gettersetter都是可选的;如果可以从初始化器中推断出类型,属性类型也可以省略; -
val不允许setter; - 当一个属性需要幕后字段时,会自动提供,可以使用
field在访问器中使用;如果属性至少一个访问器使用默认实现,或者自定义访问器通过field引用幕后字段,将会 为该属性生成一个幕后字段; - 幕后属性
- 如果只读属性的值在编译期是已知的,那么可以使用 const 修饰符将其标记为编译期常量;
- 延迟初始化
- 属性声明为非空类型,必须在构造函数中初始化;但是可以通过
lateinit延迟初始化;并且不能是原生类型; - 如果在未初始化时访问一个
lateinit属性,会抛出异常;
- 属性声明为非空类型,必须在构造函数中初始化;但是可以通过
- 继承
- 单继承,根类为
Any,有三个方法:hashCode()toString()equals(); - 覆盖方法
- 基类中可覆盖方法用
open修饰;对final类用open则无效; - 子类中覆盖方法用
override修饰;override本身是open的,如果不想再被子类覆盖,需要用final修饰;
- 基类中可覆盖方法用
- 覆盖属性
- 与覆盖方法类似,必须具有兼容的类型;可以用
var属性覆盖val属性,反之不行; - 可以在主构造函数中用
override覆盖属性(可以理解为构造函数的参数);
- 与覆盖方法类似,必须具有兼容的类型;可以用
- 单继承,根类为
- 扩展
- Kotlin 能够扩展一个类的新功能而无需继承该类或者使用像装饰者这样的设计模式;
- 扩展函数
fun MutableList<Int>.swap(index1: Int, index2: Int) { val tmp = this[index1] // “this”对应该列表 this[index1] = this[index2] this[index2] = tmp }- 声明一个扩展函数,我们需要用一个 接收者类型也就是被扩展的类型来作为他的前缀;
-
this表示扩展函数的接受者对象; - 为了在接收者类型表达式中使用泛型,我们要在函数名前声明泛型参数;
- 扩展是静态解析的;扩展不能真正的修改他们所扩展的类。通过定义一个扩展,你并没有在一个类中插入新成 员, 仅仅是可以通过该类型的变量用点表达式去调用这个新函数;
- 扩展属性
- 伴生对象的扩展
- 可以为伴生对象扩展函数与属性;
- 委托
- Java中的委托模式
class Derived(b: Base) : Base by b- Derived 类可以通过将其所有公有成员都委托给指定对象(
by b);
- Derived 类可以通过将其所有公有成员都委托给指定对象(
- 委托属性
val/var <属性名>: <类型> by <表达式>- 延迟属性 Lazy
val lazyValue: String by lazy { println("computed!") "Hello" }- 用于
val属性;
- 用于
- 可观察属性 Observable
var name: String by Delegates.observable("<no name>") { prop, old, new -> println("$old -> $new") }
- 延迟属性 Lazy
- Java中的委托模式
- 数据类
- 数据类用
data class表示; - 主构造函数的参数必须用
varval声明;
- 数据类用
- 密封类
- 密封类用
sealed class表示; - 密封类的子类必须在同一个文件中,其他文件中无法访问;密封类是抽象的;构造函数是
private;
- 密封类用
- 枚举类
- 枚举类用
enum class表示;
- 枚举类用
- 内部类
- 内部类用
inner表示,内部类有外部实例;
- 内部类用
-
is运算符监测一个表达式是否某类型的一个实例;
- 初始化器(
- 对象
-
object表示对象声明,类似于Java中的单例对象; - 类体中
companion object表示伴生对象,类似于Java中的静态变量;
-
- 类
- 集合
- 基础
- 集合包括
ListSetMap; - 只读集合是协变的 ;可变集合不是协变的;
- 集合包括
- 区间
1...10 1 until 10- 第一个表示闭区间
[1, 10],第二个表示开区间[1, 10);
- 第一个表示闭区间
- 数列
- 可以在区间上加
step;
- 可以在区间上加
- 集合操作
- 集合操作一般定义为成员函数和扩展函数;
- 操作不会影响原始集合,生成一个新的集合;
- 转换
-
map;- 参数为元素
- 结果顺序与原始顺序相同;
-
mapIndexed- 参数多了索引;
-
zip- 双路合并是根据两个集合中具有相同位置的元素构建配对;
- 返回
Pair对象的列表;
-
associateWith- 关联允许从集合元素与与其关联的某些值构建Map;
- 以原始集合元素作为Map的键;
-
associateBy- 以原始集合元素作为Map的值;
-
associate- Map的键和值都是生成的,lambda返回
Pair,对应Map的Key和Value;
- Map的键和值都是生成的,lambda返回
-
flatten- 可以在一个集合的集合上调用它;
-
flatMap- 需要一个函数,将一个集合元素映射到另一个集合上;
-
- 过滤
filterfilterIndexpartition- 返回一个 List的
Pair,第一个List包含lambda符合条件的元素,第二个包含不符合lambda的元素;
- 检验谓词
-
any:至少一个匹配成功,返回true; -
none:没有元素匹配成功,返回true; -
all:所有元素都匹配成功,返回true;空集合永远都返回true;
-
- 分组
-
groupBy- 返回Map,key是lambda表达式的值,value为对应元素的list;
-
- 取集合的一部分
-
slice- 返回给定索引的集合元素列表;
-
taketakeLast- 从头部/尾部返回指定数量元素的集合;当数量大于集合大小时,返回整个集合;
-
dropdropLast- 从头部/尾部去掉给定数量的元素;
-
chunked- 将集合分解为给定大小的“块”;
windowed
-
- 取单个元素
elementAtfirstlastrandom
- 排序
- 聚合
minmaxaveragesumcount
- 基础
协程
- 协程后面会单独讲;
习惯用法
- 数据类
data class Customer(val name: String, val email: String)- 会为
Customer提供以下功能:- 所有属性的
getters(对于var还会提供setters); equals()hashCode()toString()copy()- 所有属性的
component1()component2()等等;
- 所有属性的
- 会为
- 单例
object Resource { val name = "Name" } - 函数参数的默认值
fun foo(a: Int = 0, b: String = "") { …… }- 在定义函数时,可以设置参数的默认值;
- 扩展函数
fun String.spaceToCamelCase() { …… } "Convert this to camelcase".spaceToCamelCase() - 单表达式函数
fun theAnswer() = 42 - 一个对象的多个方法调用
class Turtle { fun penDown() fun penUp() fun turn(degrees: Double) fun forward(pixels: Double) } val myTurtle = Turtle() with(myTurtle) { // 画一个 100 像素的正方形 penDown() for(i in 1..4) { forward(100.0) turn(90.0) } penUp() } - 配置对象的属性
val myRectangle = Rectangle().apply { length = 4 breadth = 5 color = 0xFAFAFA } - 字符串模版
println("Name $name") - 使用区间
for (i in 1..100) { …… } // 闭区间:包含 100 for (i in 1 until 100) { …… } // 半开区间:不包含 100 for (x in 2..10 step 2) { …… } for (x in 10 downTo 1) { …… } if (x in 1..10) { …… } - 延迟属性
val p: String by lazy { // 计算该字符串 }
编码规范
- 目录结构
- 包结构与源文件目录保持一致;
- 源文件名称
- 如果源文件只包含单个类(以及可能相关的顶层声明),那么文件名应该和类名一致,并后缀
.kt;如果源文件包含多个类或只包含顶层声明,那么选择一个和该文件内容相关的名称,并以此命名,使用大驼峰风格;
- 如果源文件只包含单个类(以及可能相关的顶层声明),那么文件名应该和类名一致,并后缀
- 源文件布局
- 鼓励多个声明(类,顶层函数或者属性)放在一个Kotlin文件中,只要这些声明在语义上是相关联的;
- 类布局
- 类内容应该按照以下顺序排列
- 属性声明与初始化块
- 次构造函数
- 方法声明
- 伴生对象
- 嵌套类(主要为了外部使用)
- 不要按可见性或者字母顺序对方法进行排序,也不要将常规方法与扩展方法分开;而是把相关方法放在一起,包括相关联的嵌套类;
- 重载方法放在一起;
- 类内容应该按照以下顺序排列