公众号:字节数组
希望对你有所帮助 🤣🤣
开头先说一下,我不是标题党,本教程真的有两万六千字,以前我也曾写过 Kotlin 的入门教程,但由于隔得比较久了,所以就对原有教程进行了修正,并且补充了 Kotlin 新版本的一些新特性,希望对你有所帮助 🤣🤣
一、Hello World
按照国际惯例,学习一门新的语言通常都是要从打印 Hello World 开始的
1 | package main |
从这个简单的函数就可以列出 kotlin 和 Java 的几个不同点
- 函数可以定义在文件的最外层,不需要把它放在类中
- 用关键字 fun 来声明一个函数
- 可以省略
main
方法的参数 - 参数类型写在变量名之后,这有助于在类型自动推导时省略类型声明
- 使用
println
代替了System.out.println
,这是 kotlin 标准库提供的对 Java 标准库函数的简易封装 - 可以省略代码结尾的分号
二、Package
kotlin 文件都以 package 开头,同个文件中定义的所有声明(类、函数和属性)都会被放到这个包中。同个包中的声明可以直接使用,不同包的声明需要导入后使用
包的声明要放在文件顶部,import 语句紧随其后
1 | package base |
kotlin 不区分导入的是类还是函数,可以使用 import
关键字导入任何种类的声明。此外也可以在包名称后加上 .* 来导入包中定义的所有声明,从而让包中定义的类、顶层函数、和属性都可以直接引用
1 | package learn.package2 |
1 | package learn.package1 |
Java 语言规定类要放到和包结构匹配的文件夹目录结构中,且文件名必须和类名相同。而 kotlin 允许把多个类放到同一个文件中,文件名也可以任意选择。kotlin 也没有文件夹目录施加任何限制,包层级结构不需要遵循同样的目录层级结构 ,但 kotlin 官方还是建议根据包声明把源码文件放到相应的目录中
如果包名出现命名冲突,可以使用 as 关键字在本地重命名冲突项来消歧义
1 | import learn.package1.Point |
kotlin 中也有一个类似的概念可以用来重命名现有类型,叫做类型别名。类型别名用于为现有类型提供一个替代名称,如果类型名称比较长,就可以通过引入一个较短或者更为简略的名称来方便记忆
类型别名不会引入新的类型,依然对应其底层类型,所以在下述代码中输出的 class 类型是一致的
1 | class Base { |
三、变量与数据类型
在 Java 中,大部分的变量是可变的(非 final 的),意味着任何可以访问到这个变量的代码都可以去修改它。而在 kotlin 中,变量可以分为 可变变量(var) 和 不可变变量(val) 两类
声明变量的关键字有两个:
- val(value / varible+final)——不可变引用。使用 val 声明的变量不能在初始化之后再次赋值,对应的是 Java 中的 final 变量
- var(variable)——可变引用。var 变量的值可以被改变,对应的是 Java 中的非 final 变量
不可变变量在赋值之后就不能再去改变它的状态了,因此不可变变量可以说是线程安全的,因为它们无法改变,所有线程访问到的对象都是同一个,因此也不需要去做访问控制。开发者应当尽可能地使用不可变变量,这样可以让代码更加接近函数式编程风格
编程领域也推崇一种开发原则:尽可能使用 val,不可变对象及纯函数来设计程序。这样可以尽量避免副作用带来的影响
1 | fun main() { |
在声明变量时我们通常不需要显式指明变量的类型,这可以由编译器根据上下文自动推导出来。如果只读变量在声明时没有初始的默认值,则必须指明变量类型,且在使用前必须确保在各个分支条件下变量可以被初始化,否则编译器就会报异常
1 | fun main() { |
1、基本数据类型
与 Java 不同,kotlin 并不区分基本数据类型和它的包装类,在 kotlin 中一切都是对象,可以在任何变量上调用其成员函数和属性。kotlin 没有像 Java 中那样的原始基本类型,但 byte、char、integer、float 或者 boolean 等类型仍然有保留,但是全部都作为对象存在
对于基本类型,kotlin 相比 Java 有几点特殊的地方
- 数字、字符和布尔值可以在运行时表示为原生类型值,但对开发者来说,它们看起来就像普通的类
- 对于数字没有隐式拓宽转换,而在 Java 中 int 可以隐式转换为 long
- 所有未超出 Int 最⼤值的整型值初始化的变量都会自动推断为 Int 类型,如果初始值超过了其最⼤值,则会推断为 Long 类型, 如需显式指定 Long 类型值可在该值后追加 L 后缀
- 字符不能视为数字
- 不支持八进制
1 | //在 kotlin 中,int、long、float 等类型仍然存在,但是是作为对象存在的 |
此外,kotlin 的可空类型不能用 Java 的基本数据类型表示,因为 null 只能被存储在 Java 的引用类型的变量中,这意味着只要使用了基本数据类型的可空版本,它就会被编译成对应的包装类型
1 | //基本数据类型 |
如果 intValue_1 的值是100,就会发现 intValue_2 === intValue_3 的比较结果是 true,这就涉及到 Java 对包装类对象的复用问题了
2、字符串
kotlin 与 Java 一样用 String 类型来表示字符串,字符串是不可变的,可以使用索引运算符访问[]
来访问包含的单个字符,也可以用 for 循环来迭代字符串,此外也可以用 + 来连接字符串
1 | val str = "leavesCZY" |
kotlin 支持在字符串字面值中引用局部变量,只需要在变量名前加上字符 $ 即可,此外还可以包含用花括号括起来的表达式,此时会自动求值并把结果合并到字符串中
1 | val intValue = 100 |
如果你需要在原始字符串中表示字面值($)字符(它不支持反斜杠转义),可以用下列语法:
1 | val price = "${'$'}100.99" |
3、数组
kotlin 中的数组是带有类型参数的类,其元素类型被指定为相应的类型参数,使用 Array 类来表示, Array 类定义了 get 与 set 函数(按照运算符重载约定这会转变为 [ ] )以及 size 属性等
创建数组的方法有以下几种:
- 用 arrayOf 函数创建一个数组,包含的元素是指定为该函数的实参
- 用 arrayOfNulls 创建一个给定大小的数组,包含的元素均为 null,只能用来创建包含元素类型可空的数组
- 调用 Array 类的构造方法,传递数组的大小和一个 lambda 表达式,调用 lambda 表达式来创建每一个数组元素
1 | //包含给定元素的字符串数组 |
需要注意的是,数组类型的类型参数始终会变成对象类型,因此声明 Array< Int > 将是一个包含装箱类型(java.lang.Integer)的数组。如果想要创建没有装箱的基本数据类型的数组,必须使用一个基本数据类型数组的特殊类
为了表示基本数据类型的数组,kotlin 为每一种基本数据类型都提供了若干相应的类并做了特殊的优化。例如,有 IntArray、ByteArray、BooleanArray 等类型,这些类型都会被编译成普通的 Java 基本数据类型数组,比如 int[]、byte[]、boolean[] 等,这些数组中的值存储时没有进行装箱,而是使用了可能的最高效的方式。需要注意,IntArray 等并不是 Array 的子类
要创建一个基本数据类型的数组,有以下几种方式:
- 向对应类型的类(如 IntArray)的构造函数传递数组大小,这将返回一个使用对应基本数据类型默认值初始化好的数组
- 向对应类型的类(如 IntArray)的构造函数传递数组大小以及用来初始化每个元素的 lambda
- 向工厂函数(如 charArrayOf)传递变长参数的值,从而得到指定元素值的数组
1 | //指定数组大小,包含的元素将是对应基本数据类型的默认值(int 的默认值是 0) |
4、Any 和 Any?
Any 类型是 kotlin 所有非空类型的超类型,包括像 Int 这样的基本数据类型
如果把基本数据类型的值赋给 Any 类型的变量,则会自动装箱
1 | val any: Any = 100 |
如果想要使变量可以存储包括 null 在内的所有可能的值,则需要使用 Any?
1 | val any: Any? = null |
5、Unit
kotlin 中的 Unit 类型类似于 Java 中的 void,可以用于函数没有返回值时的情况
1 | fun check(): Unit { |
Unit 是一个完备的类型,可以作为类型参数,但 void 不行
1 | interface Test<T> { |
6、Nothing
Nothing 类型没有任何值,只有被当做函数返回值使用,或者被当做泛型函数返回值的类型参数使用时才会有意义,可以用 Nothing 来表示一个函数不会被正常终止,从而帮助编译器对代码进行诊断
编译器知道返回值为 Nothing 类型的函数从不会正常终止,所以编译器会把 name1 的类型推断为非空,因为 name1 在为 null 时的分支处理会始终抛出异常
1 | data class User(val name: String?) |
四、函数
kotlin 中的函数以关键字 fun 作为开头,函数名称紧随其后,再之后是用括号包裹起来的参数列表,如果函数有返回值,则再加上返回值类型,用一个冒号与参数列表隔开
1 | //fun 用于表示声明一个函数,getNameLastChar 是函数名 |
此外,表达式函数体的返回值类型可以省略,返回值类型可以自动推断,这种用单行表达式与等号定义的函数叫做表达式函数体。但对于一般情况下的有返回值的代码块函数体,必须显式地写出返回类型和 return 语句
1 | //getNameLastChar 函数的返回值类型以及 return 关键字是可以省略的 |
如果函数没有有意义的返回值,则可以声明为 Unit ,也可以省略 Unit
以下三种写法都是等价的
1 | fun test(str: String, int: Int): Unit { |
1、命名参数
为了增强代码的可读性,kotlin 允许我们使用命名参数,即在调用某函数的时候,可以将函数参数名一起标明,从而明确地表达该参数的含义与作用,但是在指定了一个参数的名称后,之后的所有参数都需要标明名称
1 | fun main() { |
2、默认参数值
可以在声明函数的时候指定参数的默认值,从而避免创建重载的函数
1 | fun main() { |
对于以上这个例子,如果按照常规的调用语法时,必须按照函数声明定义的参数顺序来给定参数,可以省略的只有排在末尾的参数
1 | fun main() { |
如果使用命名参数,可以省略任何有默认值的参数,而且也可以按照任意顺序传入需要的参数
1 | fun main() { |
3、可变参数
可变参数可以让我们把任意个数的参数打包到数组中传给函数,kotlin 的语法相比 Java 有所不同,改为通过使用 vararg 关键字声明可变参数
例如,以下的几种函数调用方式都是正确的
1 | fun main() { |
在 Java 中,可以直接将数组传递给可变参数,而 kotlin 要求显式地解包数组,以便每个数组元素在函数中能够作为单独的参数来调用,这个功能被称为展开运算符,使用方式就是在数组参数前加一个 *
1 | fun main() { |
4、局部函数
kotlin 支持在函数中嵌套函数,被嵌套的函数称为局部函数
1 | fun main() { |
五、表达式和条件循环
1、语句和表达式
这里需要先区分“语句”和“表达式”这两个概念。语句是可以单独执行,能够产生实际效果的代码,表现为赋值逻辑、打印操作、流程控制等形式,Java 中的流程控制(if,while,for)等都是语句。表达式可以是一个值、变量、常量、操作符、或它们之间的组合,表达式可以看做是包含返回值的语句
例如,以下的赋值操作、流程控制、打印输出都是语句,其是作为一个整体存在的,且不包含返回值
1 | val a = 10 |
再看几个表达式的例子
1 | 1 //字面表达式,返回 1 |
2、If 表达式
if 的分支可以是代码块,最后的表达式作为该块的返回值
1 | val maxValue = if (20 > 10) { |
以下代码可以显示地看出 if 的返回值,完全可以用来替代 Java 中的三元运算符,因此 kotlin 并没有三元运算符
1 | val list = listOf(1, 4, 10, 4, 10, 30) |
如果 if 表达式分支是用于执行某个命令,那么此时的返回值类型就是 Unit ,此时的 if 语句就看起来和 Java 的一样了
1 | val value1 = if (list.size > 0) println("1") else println("2") |
如果将 if 作为表达式而不是语句(例如:返回它的值或者把它赋给变量),该表达式需要有 else 分支
3、when 表达式
when 表达式与 Java 中的 switch/case 类似,但是要强大得多。when 既可以被当做表达式使用也可以被当做语句使用,when 将参数和所有的分支条件顺序比较直到某个分支满足条件,然后它会运行右边的表达式。如果 when 被当做表达式来使用,符合条件的分支的值就是整个表达式的值,如果当做语句使用, 则忽略个别分支的值。与 Java 的 switch/case 不同之处是 when 表达式的参数可以是任何类型,并且分支也可以是一个条件
和 if 一样,when 表达式每一个分支可以是一个代码块,它的值是代码块中最后的表达式的值,如果其它分支都不满足条件将会求值于 else 分支
如果 when 作为一个表达式使用,则必须有 else 分支, 除非编译器能够检测出所有的可能情况都已经覆盖了。如果很多分支需要用相同的方式处理,则可以把多个分支条件放在一起,用逗号分隔
1 | val value = 2 |
1 | fun main() { |
此外,when 语句也可以不带参数来使用
1 | when { |
4、for 循环
和 Java 中的 for 循环最为类似的形式是
1 | val list = listOf(1, 4, 10, 34, 10) |
通过索引来遍历
1 | val items = listOf("H", "e", "l", "l", "o") |
也可以在每次循环中获取当前索引和相应的值
1 | val list = listOf(1, 4, 10, 34, 10) |
也可以自定义循环区间
1 | for (index in 2..10) { |
5、while 和 do/while 循环
while 和 do/while 与 Java 中的区别不大
1 | val list = listOf(1, 4, 15, 2, 4, 10, 0, 9) |
1 | val list = listOf(1, 4, 15, 2, 4, 10, 0, 9) |
6、返回和跳转
kotlin 有三种结构化跳转表达式:
- return 默认从最直接包围它的函数或者匿名函数返回
- break 终止最直接包围它的循环
- continue 继续下一次最直接包围它的循环
在 kotlin 中任何表达式都可以用标签(label)来标记,标签的格式为标识符后跟 @ 符号,例如:abc@ 、fooBar@ 都是有效的标签
1 | fun main() { |
1 | value is 1 |
kotlin 有函数字面量、局部函数和对象表达式。因此 kotlin 的函数可以被嵌套
标签限制的 return 允许我们从外层函数返回,最重要的一个用途就是从 lambda 表达式中返回。通常情况下使用隐式标签更方便,该标签与接受该 lambda 的函数同名
1 | fun main() { |
六、区间
Ranges 表达式使用一个 .. 操作符来声明一个闭区间,它被用于定义实现一个 RangTo 方法
以下三种声明方式都是等价的
1 | var index = 5 |
数字类型的 ranges 在被迭代时,编译器会将它们转换为与 Java 中使用 index 的 for 循环的相同字节码的方式来进行优化
Ranges 默认会自增长,所以像以下的代码就不会被执行
1 | for (index in 10..0) { |
可以改用 downTo 函数来将之改为递减
1 | for (index in 10 downTo 0) { |
可以在 ranges 中使用 step 来定义每次循环递增或递增的长度:
1 | for (index in 1..8 step 2){ |
以上声明的都是闭区间,如果想声明的是开区间,可以使用 until 函数:
1 | for (index in 0 until 4){ |
扩展函数 reversed()
可用于返回将区间反转后的序列
1 | val rangeTo = 1.rangeTo(3) |
七、修饰符
1、final 和 open
kotlin 中的类和方法默认都是 final 的,即不可继承的,如果想允许创建一个类的子类,需要使用 open 修饰符来标识这个类,此外,也需要为每一个希望被重写的属性和方法添加 open 修饰符
1 | open class View { |
如果重写了一个基类或者接口的成员,重写了的成员同样默认是 open 的。例如,如果 Button 类是 open 的,则其子类也可以重写其 click() 方法
1 | open class Button : View() { |
如果想要类的子类重写该方法的实现,可以显式地将重写的成员标记为 final
1 | open class Button : View() { |
2、public
public 修饰符是限制级最低的修饰符,是默认的修饰符。如果一个定义为 public 的成员被包含在一个 private 修饰的类中,那么这个成员在这个类以外也是不可见的
3、protected
protected 修饰符只能被用在类或者接口中的成员上。在 Java 中,可以从同一个包中访问一个 protected 的成员,但对于 kotlin 来说,protected 成员只在该类和它的子类中可见。此外,protected 不适用于顶层声明
4、internal
一个定义为 internal 的包成员,对其所在的整个 module 可见,但对于其它 module 而言就是不可见的了。例如,假设我们想要发布一个开源库,库中包含某个类,我们希望这个类对于库本身是全局可见的,但对于外部使用者来说它不能被引用到,此时就可以选择将其声明为 internal 的来实现这个目的
根据 Jetbrains 的定义,一个 module 应该是一个单独的功能性的单位,可以看做是一起编译的 kotlin 文件的集合,它应该是可以被单独编译、运行、测试、debug 的。相当于在 Android Studio 中主工程引用的 module,Eclipse 中在一个 workspace 中的不同的 project
5、private
private 修饰符是限制级最高的修饰符,kotlin 允许在顶层声明中使用 private 可见性,包括类、函数和属性,这表示只在自己所在的文件中可见,所以如果将一个类声明为 private,就不能在定义这个类之外的地方中使用它。此外,如果在一个类里面使用了 private 修饰符,那访问权限就被限制在这个类里面,继承这个类的子类也不能使用它。所以如果类、对象、接口等被定义为 private,那么它们只对被定义所在的文件可见。如果被定义在了类或者接口中,那它们只对这个类或者接口可见
6、总结
修饰符 | 类成员 | 顶层声明 |
---|---|---|
public(默认) | 所有地方可见 | 所有地方可见 |
internal | 模块中可见 | 模块中可见 |
protected | 子类中可见 | |
private | 类中可见 | 文件中可见 |
八、空安全
1、可空性
在 kotlin 中,类型系统将一个引用分为可以容纳 null (可空引用)或者不能容纳 null(非空引用)两种类型。 例如,String 类型的常规变量不能指向 null
1 | var name: String = "leavesC" |
如果希望一个变量可以储存 null 引用,需要显式地在类型名称后面加上问号
1 | var name: String? = "leavesC" |
问号可以加在任何类型的后面来表示这个类型的变量可以存储 null 引用:Int?、Doubld? 、Long?
等
kotlin 对可空类型的显式支持有助于防止 NullPointerException 导致的异常问题,编译器不允许不对可空变量做 null 检查就直接调用其属性。这个强制规定使得开发者必须在编码初期就考虑好变量的可赋值范围并为其各个情况做好分支处理
1 | fun check(name: String?): Boolean { |
正确的做法是显式地进行 null 检查
1 | fun check(name: String?): Boolean { |
2、安全调用运算符:?.
安全调用运算符:?.
允许把一次 null 检查和一次方法调用合并为一个操作,如果变量值非空,则方法或属性会被调用,否则直接返回 null
例如,以下两种写法是完全等同的:
1 | fun check(name: String?) { |
3、Elvis 运算符:?:
Elvis 运算符:?:
用于替代 ?.
直接返回默认值 null 的情况,Elvis 运算符接收两个运算数,如果第一个运算数不为 null ,运算结果就是其运算结果值,如果第一个运算数为 null ,运算结果就是第二个运算数
例如,以下两种写法是完全等同的:
1 | fun check(name: String?) { |
4、安全转换运算符:as?
安全转换运算符:as?
用于把值转换为指定的类型,如果值不适合该类型则返回 null
1 | fun check(any: Any?) { |
5、非空断言:!!
非空断言用于把任何值转换为非空类型,如果对 null 值做非空断言,则会抛出异常
1 | fun main() { |
6、可空类型的扩展
为可空类型定义扩展函数是一种更强大的处理 null 值的方式,可以允许接收者为 null 的调用,并在该函数中处理 null ,而不是在确保变量不为 null 之后再调用它的方法
例如,如下方法可以被正常调用而不会发生空指针异常
1 | val name: String? = null |
isNullOrEmpty()
的方法签名如下所示,可以看到这是为可空类型 CharSequence? 定义的扩展函数,方法中已经处理了方法调用者为 null 的情况
1 | internal.InlineOnly . |
7、平台类型
平台类型是 kotlin 对 java 所作的一种平衡性设计。kotlin 将对象的类型分为了可空类型和不可空类型两种,但 java 平台的一切对象类型均为可空的,当在 kotlin 中引用 java 变量时,如果将所有变量均归为可空类型,最终将多出许多 null 检查;如果均看成不可空类型,那么就很容易就写出忽略了NPE 风险的代码。为了平衡两者,kotlin 引入了平台类型,即当在 kotlin 中引用 java 变量值时,既可以将之看成可空类型,也可以将之看成不可空类型,由开发者自己来决定是否进行 null 检查
九、类型的检查与转换
1、类型检查
is 与 !is 操作符用于在运行时检查对象是否符合给定类型:
1 | fun main() { |
同时,is 操作符也附带了一个智能转换功能。在许多情况下,不需要在 kotlin 中使用显式转换操作符,因为编译器跟踪不可变值的 is 检查以及显式转换,并在需要时自动插入安全的转换
例如,在上面的例子中,当判断 value 为 String 类型通过时,就可以直接将 value 当做 String 类型变量并调用其内部属性,这个过程就叫做智能转换
编译器会指定根据上下文环境,将变量智能转换为合适的类型
1 | if (value !is String) return |
2、不安全的转换操作符
如果转换是不可能的,转换操作符 as
会抛出一个异常。因此,我们称之为不安全的转换操作符
1 | fun main() { |
需要注意的是,null 不能转换为 String 变量,因为该类型不是可空的
因此如下转换会抛出异常
1 | val x = null |
为了匹配安全,可以转换的类型声明为可空类型
1 | val x = null |
3、安全的转换操作符
可以使用安全转换操作符 as? 来避免在转换时抛出异常,它在失败时返回 null
1 | val x = null |
尽管以上例子 as? 的右边是一个非空类型的 String,但是其转换的结果是可空的
十、类
1、基本概念
类的概念就是把数据和处理数据的代码封装成一个单一的实体。在 Java 中,数据存储在一个私有字段中,通过提供访问器方法:getter 和 setter 来访问或者修改数据
在 Java 中以下的示例代码是很常见的,Point 类包含很多重复的代码:通过构造函数把参数赋值给有着相同名称的字段,通过 getter 来获取属性值
1 | public final class Point { |
使用 kotlin 来声明 Point 类则只需要一行代码,两者完全等同
1 | class Point(val x: Int, val y: Int) |
kotlin 也使用关键字 class 来声明类,类声明由类名、类头(指定其类型参数、主构造函数等)以及由花括号包围的类体构成,类头与类体都是可选的,如果一个类没有类体,可以省略花括号。此外,kotlin 中类默认是 publish(公有的) 且 final (不可继承)的
kotlin 区分了主构造方法(在类体外部声明)和次构造方法(在类体内部声明),一个类可以有一个主构造函数和多个次构造函数,此外也允许在初始化代码块中 init
添加额外的初始化逻辑
2、主构造函数
主构造函数是类头的一部分,跟在类名(和可选的类型参数)后,主构造函数的参数可以是可变的(var)或只读的(val)
1 | class Point constructor(val x: Int, val y: Int) { |
如果主构造函数没有任何注解或者可见性修饰符,可以省略 constructor 关键字
1 | class Point(val x: Int, val y: Int) { |
如果构造函数有注解或可见性修饰符,则 constructor 关键字是必需的,并且这些修饰符在它前面
1 | class Point public constructor(val x: Int, val y: Int) { |
主构造函数不能包含任何的代码,初始化的代码可以放到以 init 关键字作为前缀的初始化块(initializer blocks)中,初始化块包含了在类被创建时执行的代码,主构造函数的参数可以在初始化块中使用。如果需要的话,也可以在一个类中声明多个初始化语句块。需要注意的是,构造函数的参数如果用 val/var 进行修饰,则相当于在类内部声明了一个同名的全局属性。如果不加 val/var 进行修饰,则构造函数只能在 init 函数块和全局属性初始化时进行引用
此外,要创建一个类的实例不需要使用 Java 中的 new 关键字,像普通函数一样调用构造函数即可
1 | class Point(val x: Int, val y: Int) { |
主构造函数的参数也可以在类体内声明的属性初始化器中使用
1 | class Point(val x: Int, val y: Int) { |
3、次构造函数
类也可以声明包含前缀 constructor 的次构造函数。如果类有一个主构造函数,每个次构造函数都需要直接委托给主构造函数或者委托给另一个次构造函数以此进行间接委托,用 this 关键字来进行指定即可
1 | class Point(val x: Int, val y: Int) { |
初始化块中的代码实际上会成为主构造函数的一部分,委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块中的代码都会在次构造函数体之前执行。即使该类没有主构造函数,这种委托仍会隐式发生,并且仍会执行初始化块。如果一个非抽象类没有声明任何(主或次)构造函数,会默认生成一个不带参数的公有主构造函数
4、属性
在 Java 中,字段和其访问器的组合被称作属性。在 kotlin 中,属性是头等的语言特性,完全替代了字段和访问器方法。在类中声明一个属性和声明一个变量一样是使用 val 和 var 关键字。val 变量只有一个 getter ,var 变量既有 getter 也有 setter
1 | fun main() { |
5、自定义访问器
访问器的默认实现逻辑很简单:创建一个存储值的字段,以及返回属性值的 getter 和更新属性值的 setter。如果需要的话,也可以自定义访问器
例如,以下就声明了三个带自定义访问器的属性
1 | class Point(val x: Int, val y: Int) { |
如果仅需要改变一个访问器的可见性或者为其添加注解,那么可以定义访问器而不定义其实现
1 | fun main() { |
6、延迟初始化
一般地,非空类型的属性必须在构造函数中初始化,但像使用了 Dagger2 这种依赖注入框架的项目来说就十分的不方便了,为了应对这种情况,可以用 lateinit 修饰符来标记该属性,用于告诉编译器该属性会在稍后的时间被初始化
用 lateinit 修饰的属性或变量必须为非空类型,并且不能是原生类型
1 | class Point(val x: Int, val y: Int) |
如果访问了一个未经过初始化的 lateinit 变量,则会抛出一个包含具体原因(该变量未初始化)的异常信息
1 | Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property point has not been initialized |
十一、类的分类
1、抽象类
声明为 abstract 的类内部可以包含没有实现体的成员方法,且该成员方法也用 abstract 标记,这种类称为抽象类,没有实现体的方法就称为抽象方法
此外,我们并不需要用 open 标注一个抽象类或者抽象方法,因为这是默认声明的
1 | abstract class BaseClass { |
2、数据类
数据类是一种非常强大的类,可以避免重复创建 Java 中的用于保存状态但又操作非常简单的 POJO 的模版代码,它们通常只提供了用于访问它们属性的简单的 getter 和 setter
定义一个新的数据类非常简单,例如
1 | data class Point(val x: Int, val y: Int) |
数据类默认地为主构造函数中声明的所有属性生成了如下几个方法
- getter、setter(需要是 var)
- componentN()。按主构造函数的属性声明顺序进行对应
- copy()
- toString()
- hashCode()
- equals()
为了确保生成的代码的一致性以及有意义的行为,数据类必须满足以下要求:
- 主构造函数需要包含一个参数
- 主构造函数的所有参数需要标记为 val 或 var
- 数据类不能是抽象、开放、密封或者内部的
可以利用 IDEA 来反编译查看 Point 类的 Java 实现,了解其内部实现
1 | public final class Point { |
通过数据类可以简化很多的通用操作,可以很方便地进行:格式化输出变量值、映射对象到变量、对比变量之间的相等性、复制变量等操作
1 | fun main() { |
需要注意的是,数据类的 toString()、equals()、hashCode()、copy()
等方法只考虑主构造函数中声明的属性,因此在比较两个数据类对象的时候可能会有一些意想不到的结果
1 | data class Point(val x: Int) { |
3、密封类
Sealed 类(密封类)用于对类可能创建的子类进行限制,用 Sealed 修饰的类的直接子类只允许被定义在 Sealed 类所在的文件中(密封类的间接继承者可以定义在其他文件中),这有助于帮助开发者掌握父类与子类之间的变动关系,避免由于代码更迭导致的潜在 bug,且密封类的构造函数只能是 private 的
例如,对于 View 类,其子类只能定义在与之同一个文件里,Sealed 修饰符修饰的类也隐含表示该类为 open 类,因此无需再显式地添加 open 修饰符
1 | sealed class View { |
因为 Sealed 类的子类对于编译器来说是可控的,所以如果在 when 表达式中处理了所有 Sealed 类的子类,那就不需要再提供 else 默认分支。即使以后由于业务变动又新增了 View 子类,编译器也会检测到 check 方法缺少分支检查后报错,所以说 check 方法是类型安全的
1 | fun check(view: View): Boolean { |
4、枚举类
kotlin 也提供了枚举的实现,相比 Java 需要多使用 class 关键字来声明枚举
1 | enum class Day { |
枚举可以声明一些参数
1 | enum class Day(val index: Int) { |
此外,枚举也可以实现接口
1 | interface OnChangedListener { |
枚举也包含有一些共有函数
1 | fun main() { |
5、嵌套类
在 kotlin 中在类里面再定义的类默认是嵌套类,此时嵌套类不会包含对外部类的隐式引用
1 | class Outer { |
以上代码通过 IDEA 反编译后可以看到其内部的 Java 实现方式
可以看到 Nested 其实就是一个静态类,因此 foo2() 不能访问外部类的非静态成员,也不用先声明 Outer 变量再指向 Nested 类,而是直接通过 Outer 类指向 Nested 类
1 | public final class Outer { |
6、内部类
如果需要去访问外部类的成员,需要用 inner 修饰符来标注被嵌套的类,这称为内部类。内部类会隐式持有对外部类的引用
1 | class Outer { |
再来看其内部的 Java 实现方式
使用 inner 来声明 Nested 类后,就相当于将之声明为非静态内部类,因此 foo2() 能访问其外部类的非静态成员,在声明 Nested 变量前也需要通过 Outer 变量来指向其内部的 Nested 类
1 | public final class Outer { |
类A在类B中声明 | 在Java中 | 在kotlin中 |
---|---|---|
嵌套类(不存储外部类的引用) | static class A | class A |
内部类(存储外部类的引用) | class A | inner class A |
7、匿名内部类
可以使用对象表达式来创建匿名内部类实例
1 | interface OnClickListener { |
8、内联类
在有些时候,我们需要对原生类型进行包装以便提升程序的健壮性。例如,对于 sendEmail
方法的入参参数而言,我们无法严格限制入参参数的含义类型,有的开发者可能会将 delay 理解为以毫秒为单位,有的开发者可能又会理解为以分钟为单位
1 | fun sendEmail(delay: Long) { |
为了提升程序的健壮性,我们可以通过声明一个包装类来作为参数类型:
1 | fun sendEmail(delay: Time) { |
这样,在代码源头上就限制了开发者能够传入的参数类型,开发者通过类名就能直接表达出自己希望的时间大小。然而这种方式由于额外的堆内存分配问题,就引入了运行时的性能开销,新的包装类相对原生类型所需要的性能消耗要大得多,可是此时又需要考虑程序的健壮性和可读性,所以包装类也是需要的
内联类(InlineClass)就是为了解决这两者的矛盾而诞生的。上述代码可以改为以下方式来实现
1 | fun sendEmail(delay: Time) { |
使用 inline 修饰的类就称为内联类,内联类必须含有唯一的一个属性在主构造函数中初始化,在运行时将使用这个唯一属性来表示内联类的实例,从而避免了包装类在运行时的额外开销
例如,通过查看字节码可以看到sendEmail
方法会被解释为一个以 long 类型作为入参类型的函数,并不包含任何对象
1 | public static final void sendEmail_G1aXmDY/* $FF was: sendEmail-G1aXmDY*/(long delay) { |
十二、接口
1、抽象方法与默认方法
kotlin 中的接口与 Java 8 中的类似,可以包含抽象方法的定义以及非抽象方法的实现,不需要使用 default 关键字来标注有默认实现的非抽象方法,但在实现接口的抽象方法时需要使用 override 进行标注
1 | fun main() { |
如果一个类实现了多个接口,而接口包含带有默认实现且签名相同的方法,此时编译器就会要求开发者必须显式地实现该方法,可以选择在该方法中调用不同接口的相应实现
1 | class View : Clickable, Clickable2 { |
2、抽象属性
接口中可以包含抽象属性声明,接口不定义该抽象属性是应该存储到一个支持字段还是通过 getter 来获取,接口本身并不包含任何状态,因此只有实现这个接口的类在需要的情况下会存储这个值
看以下例子,Button 类和 TextView 类都实现了 Clickable 接口,并都提供了取得 statusValue 值的方式
Button 类提供了一个自定义的 getter 用于在每次访问时重新获取 statusValue 值,因此在多次获取属性值时其值可能都会不一致,因为每次 getRandom() 方法都会被调用
TextView 类中的 statusValue 属性有一个支持字段来存储在类初始化时得到的数据,因此其值在初始化后是不会再次获取值,即 TextView 类中的 getRandom() 只会被调用一次
1 | fun main() { |
除了可以声明抽象属性外,接口还可以包含具有 getter 和 setter 的属性,只要它们没有引用一个支持字段(支持字段需要在接口中存储状态,而这是不允许的)
1 | interface Clickable { |
十三、SAM 接口
对于以下例子,在 Kotlin 1.4 之前第二种写法是不支持的,我们必须完全实现 SelfRunnable 才可以调用 setRunnable
方法
1 | /** |
而在 Kotlin 1.4 之后,Kotlin 开始支持 SAM 转换。只有一个抽象方法的接口称为函数式接口或 SAM(单一抽象方法)接口,函数式接口可以有多个非抽象成员,但只能有一个抽象成员。SAM 转换即 Single Abstract Method Conversions
,对于只有单个非默认抽象方法的接口,可以直接用 Lambda 来表示,前提是 Lambda 所表示的函数类型能够跟接口中的方法签名相匹配
所以,在 Kotlin 1.4 之后,就支持直接以 Lambda 的方式来声明 SelfRunnable 的实现类,从而使得在方法调用上可以更加简洁,但这也要求 interface 同时使用 fun 关键字修饰
1 | /** |
十四、继承
在 kotlin 中所有类都有一个共同的超类 Any ,对于没有超类声明的类来说它就是默认超类。需要注意的是, Any 并不是 java.lang.Object ,它除了 equals() 、 hashCode() 与 toString() 外没有其他属性或者函数
要声明一个显式的超类,需要把父类名放到类头的冒号之后
1 | open class Base() |
当中,类上的 open 标注与 Java 中的 final 含义相反,用于允许其它类从这个类继承。默认情况下,kotlin 中所有的类都是 final
如果派生类有一个主构造函数,其基类型必须直接或间接调用基类的主构造函数
1 | open class Base(val str: String) |
1、覆盖方法
与 Java 不同,kotlin 需要显式标注可覆盖的成员和覆盖后的成员:
1 | open class Base() { |
用 open 标注的函数才可以被子类重载,子类用 override 表示该函数是要对父类的同签名函数进行覆盖。标记为 override 的成员本身也是开放的,也就是说,它可以被子类覆盖。如果想禁止再次覆盖,可以使用 final 关键字标记
如果父类没有使用 open 对函数进行标注,则子类不允许定义相同签名的函数。对于一个 final 类(没有用 open 标注的类)来说,使用 open 标记属性和方法是无意义的
2、属性覆盖
属性覆盖与方法覆盖类似。在超类中声明为 open 的属性,如果要进行覆盖则必须在派生类中重新声明且以 override 开头,并且它们必须具有兼容的类型
每个声明的属性可以由具有初始化器的属性或者具有 getter 方法的属性覆盖
1 | open class Base { |
此外,也可以用一个 var 属性覆盖一个 val 属性,但反之则不行。因为一个 val 属性本质上声明了一个 getter 方法,而将其覆盖为 var 只是在子类中额外声明一个 setter 方法
可以在主构造函数中使用 override 关键字作为属性声明的一部分
1 | open class Base { |
3、调用超类实现
派生类可以通过 super 关键字调用其超类的函数与属性访问器的实现
1 | open class BaseClass { |
对于内部类来说,其本身就可以直接调用调用外部类的函数
1 | open class BaseClass2 { |
但如果想要在一个内部类中访问外部类的超类,则需要通过由外部类名限定的 super 关键字来实现
1 | open class BaseClass { |
如果一个类从它的直接超类和实现的接口中继承了相同成员的多个实现, 则必须覆盖这个成员并提供其自己的实现来消除歧义
为了表示采用从哪个超类型继承的实现,使用由尖括号中超类型名限定的 super 来指定,如 super< BaseClass >
1 | open class BaseClass { |
十五、集合
1、只读集合与可变集合
kotlin 的集合设计和 Java 不同的另一项特性是:kotlin 把访问数据的接口和修改集合数据的接口分开了,kotlin.collections.Collection
接口提供了遍历集合元素、获取集合大小、判断集合是否包含某元素等操作,但这个接口没有提供添加和移除元素的方法。kotlin.collections.MutableCollection
接口继承于 kotlin.collections.Collection
接口,扩展出了用于添加、移除、清空元素的方法
就像 kotlin 对 val
和 var
的区分一样,只读集合接口与可变集合接口的分离能提高对代码的可控性,如果函数接收 Collection
作为形参,那么就可以知道该函数不会修改集合,而只是对数据进行读取
以下是用来创建不同类型集合的函数
集合元素 | 只读 | 可变 |
---|---|---|
List | listOf | mutableListOf、arrayListOf |
Set | setOf | mutableSetOf、hashSetOf、linkedSetOf、sortedSetOf |
Map | mapOf | mutableMapOf、hashMapOf、linkedMapOf、sortedMapOf |
1 | val list = listOf(10, 20, 30, 40) |
2、集合与 Java
因为 Java 并不会区分只读集合与可变集合,即使 kotlin 中把集合声明为只读的, Java 代码也可以修改这个集合,而 Java 代码中的集合对 kotlin 来说也是可变性未知的,kotlin 代码可以将之视为只读的或者可变的,包含的元素也是可以为 null 或者不为 null 的
例如,在 Java 代码中 names 这么一个 List< String > 类型的变量
1 | public class JavaMain { |
在 kotlin 中可以用以下四种方式来引用变量 names
1 | val list1: List<String?> = JavaMain.names |
3、只读集合的可变性
只读集合不一定就是不可变的。例如,假设存在一个拥有只读类型接口的对象,该对象存在两个不同的引用,一个只读,一个可变,当可变引用修改了该对象后,这对只读引用来说就相当于“只读集合被修改了”,因此只读集合并不总是线程安全的。如果需要在多线程环境下处理数据,需要保证正确地同步了对数据的访问,或者使用支持并发访问的数据结构
例如,list1 和 list1 引用到同一个集合对象, list3 对集合的修改同时会影响到 list1
1 | val list1: List<String> = JavaMain.names |
4、集合与可空性
集合的可空性可以分为三种:
- 可以包含为 null 的集合元素
- 集合本身可以为 null
- 集合本身可以为 null,且可以包含为 null 的集合元素
例如,intList1 可以包含为 null 的集合元素,但集合本身不能指向 null;intList2 不可以包含为 null 的集合元素,但集合本身可以指向 null;intList3 可以包含为 null 的集合元素,且集合本身能指向 null
1 | //List<Int?> 是能持有 Int? 类型值的列表 |
十六、扩展函数和扩展属性
1、扩展函数
扩展函数用于为一个类增加一种新的行为,这是为缺少有用函数的类进行扩展的途径。扩展函数的用途就类似于在 Java 中实现的静态工具方法。而在 kotlin 中使用扩展函数的一个优势就是我们不需要在调用方法的时候把整个对象当作参数传入,扩展函数表现得就像是属于这个类本身的一样,可以使用 this 关键字并直接调用其所有 public 方法
扩展函数并不允许你打破它的封装性,和在类内部定义的方法不同的是,扩展函数不能访问私有的或是受保护的成员
1 | //为 String 类声明一个扩展函数 lastChar() ,用于返回字符串的最后一个字符 |
之后,我们就可以像调用类本身内部声明的方法一样,直接调用扩展函数
1 | fun main() { |
如果需要声明一个静态的扩展函数,则必须将其定义在伴生对象上,这样就可以在没有 Namer 实例的情况下调用其扩展函数,就如同在调用 Java 的静态函数一样
1 | class Namer { |
需要注意的是,如果扩展函数声明于 class 内部,则该扩展函数只能该类和其子类内部调用,因为此时相当于声明了一个非静态函数,外部无法引用到。所以一般都是将扩展函数声明为全局函数
2、扩展属性
扩展函数也可以用于属性
1 | //扩展函数也可以用于属性 |
3、不可重写的扩展函数
看以下例子,子类 Button 重写了父类 View 的 click() 函数,此时如果声明一个 View 变量,并赋值为 Button 类型的对象,调用的 click() 函数将是 Button 类重写的方法
1 | fun main() { |
对于扩展函数来说,与以上的例子却不一样。如果基类和子类都分别定义了一个同名的扩展函数,此时要调用哪个扩展函数是由变量的静态类型来决定的,而非这个变量的运行时类型
1 | fun main() { |
此外,如果一个类的成员函数和扩展函数有相同的签名,成员函数会被优先使用
扩展函数并不是真正地修改了原来的类,其底层其实是以静态导入的方式来实现的。扩展函数可以被声明在任何一个文件中,因此有个通用的实践是把一系列有关的函数放在一个新建的文件里
需要注意的是,扩展函数不会自动地在整个项目范围内生效,如果需要使用到扩展函数,需要进行导入
4、可空接收者
可以为可空的接收者类型定义扩展,即使接受者为 null,使得开发者在调用扩展函数前不必进行判空操作,且可以通过 this == null
来检查接收者是否为空
1 | fun main() { |
十七、Lambda 表达式
Lambda 表达式本质上就是可以传递给其它函数的一小段代码,通过 Lambda 表达式可以把通用的代码结构抽取成库函数,也可以把 Lambda 表达式存储在一个变量中,把这个变量当做普通函数对待
1 | //由于存在类型推导,所以以下三种声明方式都是完全相同的 |
- 一个 Lambda 表达式始终用花括号包围,通过箭头把实参列表和函数体分开
- 如果 Lambda 声明了函数类型,那么就可以省略函数体的类型声明
- 如果 Lambda 声明了参数类型,且返回值支持类型推导,那么就可以省略函数类型声明
虽然说倾向于尽量避免让 Lambda 表达式引用外部变量以避免副作用,但有些情况下让 Lambda 引用外部变量也可以简化计算结构。访问了外部环境变量的 Lambda 表达式称之为闭包,闭包可以被当做参数传递或者直接使用。与 Java 不同,kotlin 中的闭包不仅可以访问外部变量也可以对其进行修改
例如,假设我们需要一个计算总和的方法,每次调用函数时都返回当前的总和大小。方法外部不提供保存当前总和的变量,由 Lambda 表达式内部进行存储
1 | fun main() { |
此外,kotlin 也支持一种自动运行的语法
1 | { va1: Int, va2: Int -> println(va1 + va2) }(10, 20) |
Lambda 表达式最常见的用途就是和集合一起工作,看以下例子
要从一个人员列表中取出年龄最大的一位
1 | data class Person(val name: String, val age: Int) |
当中,库函数 maxBy 可以在任何集合上调用,其需要一个实参:一个函数,用于指定要用来进行比较的函数。花括号中的代码 { it.age }
就是实现了这个逻辑的 Lambda 表达式
上述 maxBy 函数的实参是简化后的写法,这里来看下 maxBy 函数的简化过程
最原始的语法声明应该是这样的,用括号包裹着 Lambda 表达式
1 | println(people.maxBy({ p: Person -> p.age })) |
kotlin 有一种语法约定,如果 Lambda 表达式是函数调用的最后一个实参,可以将之放到括号的外边
1 | println(people.maxBy() { p: Person -> p.age }) |
当 Lamdba 表达式是函数唯一的实参时,可以去掉调用代码中的空括号对
1 | println(people.maxBy { p: Person -> p.age }) |
当 Lambda 表达式的参数类型是可以被推导出来时就可以省略声明参数类型
1 | println(people.maxBy { p -> p.age }) |
如果当前上下文期待的是只有一个参数的 Lambda 表达式且参数类型可以被推断出来,就会为该参数生成一个默认名称:it
1 | println(people.maxBy { it.age }) |
kotlin 和 Java 的一个显著区别就是,在 kotlin 中函数内部的 Lambda 表达式不会仅限于访问函数的参数以及 final 变量,在 Lambda 内部也可以访问并修改非 final 变量
从 Lambda 内部访问外部变量,我们称这些变量被 Lambda 捕捉。当捕捉 final 变量时,变量值和使用这个值的 Lambda 代码一起存储,对非 final 变量来说,其值被封装在一个特殊的包装器中,对这个包装器的引用会和 Lambda 代码一起存储
1 | var number = 0 |
成员引用用于创建一个调用单个方法或者访问单个属性的函数值,通过双冒号把类名称和要引用的成员(一个方法或者一个属性)名称分隔开
成员引用的一个用途就是:如果要当做参数传递的代码块已经被定义成了函数,此时不必专门创建一个调用该函数的 Lambda 表达式,可以直接通过成员引用的方式来传递该函数(也可以传递属性)。此外,成员引用对扩展函数一样适用
1 | data class Person(val name: String, val age: Int) { |
不管引用的是函数还是属性,都不要在成员引用的名称后面加括号
此外,还可以引用顶层函数
1 | fun test() { |
也可以用构造方法引用存储或者延期执行创建类实例的动作
1 | data class Person(val name: String, val age: Int) |
十八、标准库中的扩展函数
kotlin 标准库中提供了几个比较实用的扩展函数,定义在 Standard 文件下
1、run
run 函数接收一个函数参数并以该函数的返回值作为 run 函数的返回值
1 | internal.InlineOnly . |
用例
1 | fun main() { |
2、with
with 函数并不是扩展函数,不过由于作用相近,此处就一起介绍了。with 函数的第一个参数是接受者对象 receiver,第二个参数是在 receiver 对象类型上定义的扩展函数,所以可以在函数内部直接调用 receiver 其公开的方法和属性
1 | internal.InlineOnly . |
with 函数用于对同一个对象执行多次操作而不需要反复把对象的名称写出来
例如,为了构建一个包含指定内容的字符串,需要先后如下调用
1 | fun main() { |
改为通过 with 函数来构建的话会代码会简洁许多
1 | val result = with(StringBuilder()) { |
with 函数是一个接受两个参数的函数,在这个例子中就是一个 StringBuilder 和一个 Lambda 表达式,这里利用了把 Lambda 表达式放在括号外的约定
with 函数的返回值是执行 Lambda 表达式的结果,该结果就是 Lambda 中的最后一个表达式的返回值,因此如果将代码修改为如下所示的话,因为 println() 方法无返回值,所以打印出来的内容将是 kotlin.Unit
1 | val result = with(StringBuilder()) { |
3、apply
apply 函数被声明为类型 T 的扩展函数,它的接收者是作为实参的 Lambda 的接受者,最终函数返回 this 即对象本身
1 | internal.InlineOnly . |
所以apply 函数和 with 函数的唯一区别在于:apply 函数始终会返回作为实参传递给它的对象
1 | val result = StringBuilder().apply { |
4、also
also 函数接收一个函数类型的参数,该参数又以接收者本身作为参数,最终返回接收者对象本身
1 | internal.InlineOnly . |
用例
1 | fun main() { |
5、let
let 函数接收一个函数类型的参数,该参数又以接收者本身作为参数,最终返回函数的求值结果
1 | internal.InlineOnly . |
用例
1 | fun main() { |
6、takeIf
takeIf 接收一个返回值类型为 bool 的函数,当该参数返回值为 true 时返回接受者对象本身,否则返回 null
1 | internal.InlineOnly . |
用例
1 | fun main() { |
7、takeUnless
takeUnless 的判断条件与 takeIf 相反,这里不再赘述
1 | internal.InlineOnly . |
十九、函数操作符
1、总数操作符
1、any
如果至少有一个元素符合给出的判断条件,则返回 true
1 | val list = listOf(1, 3, 5, 7, 9) |
2、all
如果全部的元素符合给出的判断条件,则返回 true
1 | val list = listOf(1, 3, 5, 7, 9) |
3、count
返回符合给出判断条件的元素总数
1 | val list = listOf(1, 3, 5, 7, 9) |
4、fold
在一个初始值的基础上从第一项到最后一项通过一个函数累计所有的元素
1 | fun main() { |
1 | 1 , 2 |
5、foldRight
与 fold 一样,但顺序是从最后一项到第一项
1 | val list = listOf(1, 3, 5, 7, 9) |
1 | 9 , 2 |
6、forEach
1 | val list = listOf(1, 3, 5, 7, 9) |
7、forEachIndexed
类似于 forEach ,同时可以得到元素的索引
1 | val list = listOf(1, 3, 5, 7, 9) |
8、max
返回最大的一项,如果没有则返回null
1 | val list = listOf(1, 3, 5, 7, 9) |
9、maxBy
根据给定的函数返回最大的一项,如果没有则返回 null
1 | val list = listOf(1, 3, 5, 7, 9) |
10、min
返回最小的一项,如果没有则返回null
1 | val list = listOf(1, 3, 5, 7, 9) |
11、minBy
根据给定的函数返回最小的一项,如果没有则返回null
1 | val list = listOf(1, 3, 5, 7, 9) |
12、none
如果没有任何元素与给定的函数匹配,则返回true
1 | val list = listOf(1, 3, 5, 7, 9) |
13、reduce
与 fold 一样,但是没有一个初始值。通过一个函数从第一项到最后一项进行累计
1 | val list = listOf(1, 3, 5, 7, 9) |
14、reduceRight
与 reduce 一样,但是顺序是从最后一项到第一项
1 | val list = listOf(1, 3, 5, 7, 9) |
15、sumBy
返回所有每一项通过函数转换之后的数据的总和
1 | val list = listOf(1, 3, 5, 7, 9) |
2、过滤操作符
1、drop
返回包含去掉前n个元素的所有元素的列表
1 | val list = listOf(1, 3, 5, 7, 9) |
2、dropWhile
返回从第一个开始不符合给定函数的元素起之后的列表
1 | val list = listOf(1, 3, 5, 7, 9, 2) |
3、dropLastWhile
从最后一项开始,返回从开始不符合给定函数的元素起之后的列表
1 | val list = listOf(10, 1, 3, 5, 7, 9) |
4、filter
过滤所有符合给定函数条件的元素
1 | val list = listOf(1, 3, 5, 7, 9, 2) |
5、filterNot
过滤所有不符合给定函数条件的元素
1 | val list = listOf(1, 3, 5, 7, 9, 2) |
6、filterNotNull
过滤所有元素中不是null的元素
1 | val list = listOf(1, 3, 5, 7, 9, 2, null) |
7、slice
过滤一个list中指定index的元素
1 | val list = listOf(1, 3, 5, 7, 9, 2, null) |
8、take
返回从第一个开始的n个元素
1 | val list = listOf(1, 3, 5, 7, 9, 2, null) |
9、takeLast
返回从最后一个开始的n个元素
1 | val list = listOf(1, 3, 5, 7, 9, 2, null) |
10、takeWhile
返回从第一个开始符合给定函数条件的元素。
1 | val list = listOf(1, 3, 5, -1, 7, 9, 2) |
3、映射操作符
1、flatMap
遍历所有的元素,为每一个创建一个集合,最后把所有的集合放在一个集合中
1 | val list = listOf(1, 3, 5, -1, 7, 9, 2) |
2、groupBy
返回一个根据给定函数分组后的map
1 | val list = listOf(1, 3, 5, -1, 7, 9, 2) |
3、map
返回一个每一个元素根据给定的函数转换所组成的List。
1 | val list = listOf(1, 3, 5, -1, 7, 9, 2) |
4、mapIndexed
返回一个每一个元素根据给定的包含元素index的函数转换所组成的List
1 | val list = listOf(1, 3, 5, -1, 7, 9, 2) |
5、mapNotNull
返回一个每一个非null元素根据给定的函数转换所组成的List
1 | val list = listOf(1, 3, 5, -1, 7, 9, null, 2) |
4、元素操作符
1、contains
如果指定元素可以在集合中找到,则返回true
1 | val list = listOf(1, 3, 5, -1, 7, 9, null, 2) |
2、elementAt
返回给定index对应的元素,如果index数组越界则会抛出 IndexOutOfBoundsException
1 | val list = listOf(1, 3, 5, -1, 7, 9, null, 2) |
3、elementAtOrElse
返回给定index对应的元素,如果index数组越界则会根据给定函数返回默认值
1 | val list = listOf(1, 3, 5, -1, 7, 9, null, 2) |
4、elementAtOrNull
返回给定index对应的元素,如果index数组越界则会返回null
1 | val list = listOf(1, 3, 5, -1, 7, 9, null, 2) |
5、first
返回符合给定函数条件的第一个元素
1 | val list = listOf(1, 3, 5, -1, 7, 9, 2) |
6、firstOrNull
返回符合给定函数条件的第一个元素,如果没有符合则返回null
1 | val list = listOf(1, 3, 5, -1, 7, 9, 2) |
7、indexOf
返回指定元素的第一个index,如果不存在,则返回 -1
1 | val list = listOf(1, 3, 5, -1, 7, 9, 2) |
8、indexOfFirst
返回第一个符合给定函数条件的元素的index,如果没有符合则返回 -1
1 | val list = listOf(1, 3, 5, 1, 7, 9, 2) |
9、indexOfLast
返回最后一个符合给定函数条件的元素的index,如果没有符合则返回 -1
1 | val list = listOf(1, 3, 5, 6, 7, 9, 2) |
10、last
返回符合给定函数条件的最后一个元素
1 | val list = listOf(1, 3, 5, 6, 7, 9, 2) |
11、lastIndexOf
返回指定元素的最后一个index,如果不存在,则返回 -1
1 | val list = listOf(1, 3, 2, 6, 7, 9, 2) |
12、lastOrNull
返回符合给定函数条件的最后一个元素,如果没有符合则返回null
1 | val list = listOf(1, 3, 2, 6, 7, 9, 2) |
13、single
返回符合给定函数的单个元素,如果没有符合或者超过一个,则抛出异常
1 | val list = listOf(1, 9, 2, 6, 7, 9, 2) |
14、singleOrNull
返回符合给定函数的单个元素,如果没有符合或者超过一个,则返回null
1 | val list = listOf(1, 9, 2, 6, 7, 9, 2) |
5、生产操作符
1、partition
把一个给定的集合分割成两个,第一个集合是由原集合每一项元素匹配给定函数条 件返回 true 的元素组成,第二个集合是由原集合每一项元素匹配给定函数条件返回 false 的元素组成
1 | val list = listOf(1, 9, 2, 6, 7, 9, 2) |
2、plus
返回一个包含原集合和给定集合中所有元素的集合,因为函数的名字原因,我们可以使用 + 操作符
1 | val list1 = listOf(1, 9, 2, 6, 7, 9, 2) |
3、zip
返回由 pair 组成的List,每个 pair 由两个集合中相同index的元素组成。这个返回的List的大小由最小的那个集合决定
1 | val list1 = listOf(1, 9, 2, 6, 7, 9, 2) |
1 | class java.util.ArrayList |
4、unzip
从包含pair的List中生成包含List的Pair
1 | val list1 = listOf(Pair("leavesC", 1), Pair("leavesC_2", 2), Pair("leavesC_3", 3)) |
1 | class kotlin.Pair |
6、顺序操作符
1、reverse
返回一个与指定list相反顺序的list
1 | val list1 = listOf(Pair("leavesC", 1), Pair("leavesC_2", 2), Pair("leavesC_3", 3)) |
2、sort
返回一个自然排序后的list
1 | val list1 = listOf(2, 4, 1, 9, 5, 10) |
3、sortBy
返回一个根据指定函数排序后的list
1 | val list1 = listOf(2, 4, 1, 9, 5, 10) |
4、sortDescending
返回一个降序排序后的List
1 | val list1 = listOf(2, 4, 1, 9, 5, 10) |
5、sortDescendingBy
返回一个根据指定函数降序排序后的list
1 | val list1 = listOf(2, 4, 1, 9, 5, 10) |
二十、异常
kotlin 中异常处理的基本形式和 Java 类似
1 | fun compute(index: Int): Boolean { |
和 Java 不同的是,kotlin 中 throw 结构是一个表达式,可以作为另一个表达式的一部分来使用
例如下面这个例子,如果条件不满足,则将抛出异常,从而导致 status 变量也不会初始化
1 | val status = if (index in 0..10) index else throw IllegalArgumentException("参数错误") |
此外,在 Java 中对于受检异常必须显式地处理,通过 try/catch 语句捕获异常或者是抛给其调用者来处理。而 kotlin 不区分受检异常和未受检异常,不用指定函数抛出的异常,可以处理也可以不处理异常
在 kotlin 中 ,try 关键字引入了一个表达式,从而可以把表达式的值赋给一个变量。如果一个 try 代码块执行正常,代码块中最后一个表达式就是结果,如果捕获到了一个异常,则相应 catch 代码块中最后一个表达式就是结果
看以下例子,如果 try 表达式包裹的表达式会抛出异常,则返回值为 null ,否则为 true
1 | fun main() { |
但是,如果在 catch 语句中使用 return 结束了 compute 函数,则没有任何输出
1 | fun main() { |
二十一、运算符重载
kotlin 允许为类型提供预定义的操作符实现,这些操作符具有固定的符号表示(例如 + 和 * )和固定的优先级,通过操作符重载可以将操作符的行为映射到指定的方法。为实现这样的操作符,需要为类提供一个固定名字的成员函数或扩展函数,相应的重载操作符的函数需要用 operator 修饰符标记
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.rem(b) |
a..b | a.rangeTo(b) |
a in b | b.contains(a) |
a !in b | !b.contains(a) |
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) |
3、数组操作符
操作符 | 函数 |
---|---|
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) |
4、等于操作符
操作符 | 函数 |
---|---|
a == b | a?.equals(b) ?: b === null |
a != b | !(a?.equals(b) ?: b === null) |
相等操作符有一点不同,为了达到正确合适的相等检查做了更复杂的转换,因为要得到一个确切的函数结构比较,不仅仅是指定的名称
方法必须要如下准确地被实现:
1 | operator fun equals(other: Any?): Boolean |
操作符 === 和 !== 用来做身份检查(它们分别是 Java 中的 == 和 != ),并且它们不能被重载
5、比较操作符
操作符 | 函数 |
---|---|
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 值
6、函数调用
方法 | 调用 |
---|---|
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) |
7、例子
看几个例子
1 | data class Point(val x: Int, val y: Int) { |
1 | fun main() { |
二十二、中缀调用与解构声明
1、中缀调用
可以以以下形式创建一个 Map 变量
1 | fun main() { |
使用 “to” 来声明 map 的 key 与 value 之间的对应关系,这种形式的函数调用被称为中缀调用
kotlin 标准库中对 to 函数的声明如下所示,其作为扩展函数存在,且是一个泛型函数,返回值 Pair 最终再通过解构声明分别将 key 和 value 传给 Map
1 | public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that) |
中缀调用只能与只有一个参数的函数一起使用,无论是普通的函数还是扩展函数。中缀符号需要通过 infix 修饰符来进行标记
1 | fun main() { |
对于 mapOf
函数来说,它可以接收不定数量的 Pair
类型对象,因此我们也可以通过自定义的中缀调用符 test
来创建一个 map 变量
1 | public fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V> = |
2、解构声明
有时会有把一个对象拆解成多个变量的需求,在 kotlin 中这种语法称为解构声明
例如,以下例子将 Person 变量结构为了两个新变量:name 和 age,并且可以独立使用它们
1 | data class Person(val name: String, val age: Int) |
一个解构声明会被编译成以下代码:
1 | val name = person.component1() |
其中的 component1()
和 component2()
函数是在 kotlin 中广泛使用的约定原则的另一个例子。任何表达式都可以出现在解构声明的右侧,只要可以对它调用所需数量的 component
函数即可
需要注意的是,componentN()
函数需要用 operator
关键字标记,以允许在解构声明中使用它们
对于数据类来说,其自动生成了 componentN()
函数,而对非数据类,为了使用解构声明,需要我们自己来手动声明函数
1 | class Point(val x: Int, val y: Int) { |
如果我们需要从一个函数返回两个或者更多的值,这时候使用解构声明就会比较方便了
这里使用的是标准类 Pair 来包装要传递的数据,当然,也可以自定义数据类
1 | fun computer(): Pair<String, Int> { |
此外,解构声明也可以用在 for 循环中
1 | val list = listOf(Person("leavesC", 24), Person("leavesC", 25)) |
对于遍历 map 同样适用
1 | val map = mapOf("leavesC" to 24, "ye" to 25) |
同样也适用于 lambda 表达式
1 | val map = mapOf("leavesC" to 24, "ye" to 25) |
如果在解构声明中不需要某个变量,那么可以用下划线取代其名称,此时不会调用相应的 componentN()
操作符函数
1 | val map = mapOf("leavesC" to 24, "ye" to 25) |
二十三、Object 关键字
1、对象声明
在 kotlin 的世界中,可以通过对象声明这一功能来实现 Java 中的单例模式,将类声明与该类的单一实例声明结合到一起。与类一样,一个对象声明可以包含属性、方法、初始化语句块等的声明,且可以继承类和实现接口,唯一不被允许的是构造方法
与普通类的实例不同,对象声明在定义的时候就被立即创建了,不需要在代码的其它地方调用构造方法,因此为对象声明定义构造方法是没有意义的
1 | interface Fly { |
kotlin 中的对象声明被编译成了通过静态字段来持有它的单一实例的类,这个字段名字始终都是 INSTANCE
例如,对于 kotlin 中的如下两个对象声明
1 | class Test { |
在 Java 代码中来访问这两个对象
1 | public static void main(String[] args) { |
2、伴生对象
如果需要一个可以在没有类实例的情况下调用但是需要访问类内部的函数(类似于 Java 中的静态变量/静态函数),可以将其写成那个类中的对象声明的成员
通过关键字 companion ,就可以获得通过容器类名称来访问这个对象的方法和属性的能力,不再需要显式地指明对象的名称
1 | class Test { |
1、工厂模式
可以利用伴生对象来实现工厂模式
1 | private class User private constructor(val name: String) { |
2、指定名称
伴生对象既可以为其指定名字,也可以直接使用其默认名 Companion,在引用伴生对象时,可以自由选择是否要在类名后加上伴生对象名
如果使用的是其默认名 Companion(没有自定义名称),则以下两种引用方式都是等价的
1 | val user2 = User.Companion.newById(10) |
如果为伴生对象声明了自定义名称,引用方式等同
1 | private class User private constructor(val name: String) { |
3、实现接口
伴生对象也可以实现接口,且可以直接将包含它的类的名字当做实现了该接口的对象实例来使用
1 | private class User private constructor(val name: String) { |
3、对象表达式
object 能用来声明匿名对象,可用于替代 Java 中的匿名内部类,且对象表达式中的代码可以访问并修改其外部的非 final 型的变量
1 | fun newThread(runnable: Runnable) = Thread(runnable) |
二十四、委托
1、委托模式
委托模式是一种基本的设计模式,该模式下有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。kotlin 原生支持委托模式,可以零样板代码来实现,通过关键字 by 实现委托
1 | interface Printer { |
CustomPrinter 的 by 子句表示将会在 CustomPrinter 中存储 printer 变量,并且编译器将为 CustomPrinter 隐式生成 Printer 接口的所有抽象方法,并将这些方法的调用操作转发给 printer
此外,CustomPrinter 也可以决定自己实现部分方法或全部自己实现,但重写的成员不会在委托对象的成员中调用 ,委托对象的成员只能访问其自身对接口成员实现
1 | interface Printer { |
2、属性委托
kotlin 支持通过委托属性将对一个属性的访问操作委托给另外一个对象来完成,对应的语法格式是:
1 | val/var <属性名>: <类型> by <表达式> |
属性的委托不必实现任何的接口,但需要提供一个 getValue() 方法与 setValue()(对于 var 属性),对一个属性的 get 和 set 操作会被委托给属性的委托的这两个方法
1 | class Delegate { |
看以下的小例子,通过输出值就可以看出各个方法的调用时机
1 | package test |
3、延迟属性
lazy() 是接受一个 lambda 并返回一个 Lazy < T > 实例的函数,返回的实例可以作为实现延迟属性的委托,第一次调用 get() 会执行已传递给 lazy() 函数的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果
1 | class Example { |
默认情况下,对于 lazy 属性的求值是带同步锁的(synchronized),即带有 LazyThreadSafetyMode.SYNCHRONIZED 参数,此时该值只允许同一时刻只能有一个线程对其进行初始化,并且所有线程会看到相同的初始化值。如果初始化委托的同步锁不是必需的,即如果允许多个线程同时执行,那么可以将 LazyThreadSafetyMode.PUBLICATION 作为参数传递给 lazy() 函数。 而如果你确定初始化将总是发生在单个线程,那么可以使用 LazyThreadSafetyMode.NONE 模式, 此时不会有任何线程安全的保证以及相关的资源开销
4、可观察属性
Delegates.observable() 接受两个参数:初始值以及修改属性值时的回调函数。当为属性赋值后就会调用该回调函数,该回调函数包含三个参数:被赋值的属性、旧值与新值
1 | fun main() { |
如果想要拦截一个赋值操作并判断是否进行否决,可以使用 vetoable() 函数,通过返回一个布尔值来决定是否进行拦截,该判断逻辑是在属性被赋新值生效之前进行
1 | fun main() { |
5、把属性储存在映射中
可以在一个 map 映射里存储属性的值,然后把属性的存取操作委托给 map 进行管理
1 | fun main() { |
在上述示例中,属性 name 和 age 都是不可变的(val),因此 map 的类型也是 Map 而非 MutableMap(MutableMap 在赋值后可以修改),因此如果为了支持 var 属性,可以将只读的 Map 换成 MutableMap
6、局部委托属性
可以将局部变量声明为委托属性
1 | class Printer { |
委托变量只会在第一次访问时才会进行初始化,因此如果 valid 为 false 的话,getPrinter() 方法就不会被调用
二十五、注解
注解是将元数据附加到代码元素上的一种方式,附件的元数据就可以在编译后的类文件或者运行时被相关的源代码工具访问
注解的语法格式如下所示:
1 | annotation class AnnotationName() |
注解的附加属性可以通过用元注解标注注解类来指定:
- @Target 指定该注解标注的允许范围(类、函数、属性等)
- @Retention 指定该注解是否要存储在编译后的 class 文件中,如果要保存,则在运行时可以通过反射来获取到该注解值
- @Repeatable 标明允许在单个元素上多次使用相同的该注解
- @MustBeDocumented 指定该注解是公有 API 的一部分,并且应该包含在生成的 API 文档中显示的类或方法的签名中
1 |
|
注解可以声明包含有参数的构造函数
1 | annotation class OnClick(val viewId: Long) |
允许的参数类型有:
- 原生数据类型,对应 Java 原生的 int 、long、char 等
- 字符串
- class 对象
- 枚举
- 其他注解
- 以上类型的数组
注解参数不能包含有可空类型,因为 JVM 不支持将 null 作为注解属性的值来存储
看一个在运行时获取注解值的例子
1 |
|