0. 前言
前面的 Kotlin 的文章《从Java角度深入理解Kotlin》虽然已经对 Kotlin 相关的核心概念有了全面的介绍,但是很多基础入门的知识没有提及。
这对 Kotlin 新手来说还是不是很好理解,所以特地写了这篇《Kotlin从入门到进阶》
写这篇文章主要有一下几个目的:
- 对于 Kotlin 新手来说理解这篇《从Java角度深入理解Kotlin》还是有些吃力,毕竟还有许多基础知识点没有介绍
- 对自己来说也有一个查缺补漏的作用
但是出于篇幅的原因,还是有很多知识没有介绍到,对其他知识点有兴趣的可以自行查阅
1. Kotlin 基础知识
1.1 Kotlin 函数和变量的定义
函数和变量这两个概念是 Kotlin 中最基本的两个元素,在介绍其他概念之前,先介绍下这两个基本概念
下面我们来定义一个函数:
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
复制代码
对上面的函数做个解释:
- fun 关键字用来定义一个函数
- fun 关键字后面是函数名(max)
- 括号中间是函数参数
- 冒号后面是返回值类型
- 语句可以不用分号结尾
如下图所示:
需要注意的是 Kotlin 中没有像 Java 中的 三元运算符 了
在 Java 中上面的 函数体 可以改成这样:
return (a > b) ? a : b
复制代码
Kotlin 使用 if 语句来代替 三目运算符
1.2 表达式和语句
我们在学习任何编程语言的时候,都会遇到两个概念:
- 表达式(expressions)
- 语句(statements)
可能有些开发者还搞不清什么是 表达式 ,什么是 语句
在不同的编程语言中对 表达式和语句的定义 可能会有一些细微的差别
1.2.1 Java 中表达式和语句
在 Java 中 一个 表达式 是由 变量
、操作符
和 方法
调用组成, 用来得到某种类型的返回值
比如下面的 Java 官方文档的代码示例:
// cadence = 0 是表达式
int cadence = 0;
// anArray[0] = 100 是表达式
anArray[0] = 100;
// "Element 1 at index 0: " + anArray[0] 是表达式
System.out.println("Element 1 at index 0: " + anArray[0]);
// result = 1 + 2 是表达式
int result = 1 + 2; // result is now 3
// value1 == value2 是表达式
if (value1 == value2)
//"value1 == value2" 是表达式
System.out.println("value1 == value2");
复制代码
我们从中可以看出 表达式 会返回某种类型的值
Java 中的 语句 和人类自然语言的句子差不多,一个 Java 语句 形成一个完整的执行单元,语句以分号(;)结尾
有的表达式在末尾加上分号就变成语句了,如下面几种类型的表达式:
- 赋值表达式
- 任何使用了 ++ 或 -- 的表达式
- 方法调用
- 创建对象表达式
如:
// 赋值语句
aValue = 8933.234
// 自增语句
aValue++;
// 方法调用语句
System.out.println("Hello World!");
// 创建对象语句
Bicycle myBike = new Bicycle();
复制代码
除此之外,还有 声明语句(declaration statements),如:
// declaration statement
double aValue = 8933.234;
复制代码
还有 控制流语句(control flow statements),它包括:
- 选择语句 decision-making statements (if-then, if-then-else, switch)
- 循环语句 looping statements (for, while, do-while)
- 分支语句 branching statements (break, continue, return)
1.2.2 Kotlin 中表达式和语句
Kotlin 和 Java 中对表达式和语句的定义都是类似的
但是对于有些关键字是语句还是表达式和 Java 还是有些区别的
- if/when
如上所述,在 Java 中所有的 控制流 都是语句
在 Kotlin 的控制流中除了 循环(for/while/do..while) ,其他的都是表达式
既然是表达式,那么它就是表示某种类型的数据,可以把它赋值给变量
val max = if (a > b) a else b
复制代码
- try
在 Java 中 try 异常处理是语句
在 Kotlin 中它是表达式:
fun readNumber(reader: BufferedReader) {
//将 try 赋值给 number 变量
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException){
return
}
println(number)
}
复制代码
3 表达式体
上面的 max 函数,因为函数体只有一个表达式,我们可以改写成如下形式:
fun max (a:Int, b:Int) = if (a > b) a else b
复制代码
可以看出我们把一个表达式赋值给一个函数,表达式的返回值就是函数的返回值
如果一个函数的函数体放在花括号({})中,我们说该函数有一个 区块体(block body)
如果一个函数直接返回一个表达式,我们说该函数有一个 表达式体(expression body)
为什么上面的 max 函数可以省略 return 关键字呢?
实际上任何一个变量和表达式都有一个类型;
Kotlin 每个函数都会有返回类型,这个后面介绍的函数的时候回继续讲解
表达式的类型,Kotlin 会通过 类型推导(type inference) 来得知该表达式的类型
然后把得到的类型当做函数的返回值类型
1.2.3 变量的定义
Kotlin 中对变量的定义和 Java 不一样
在 Java 中通常以变量的类型开头,后面跟着变量名称
Kotlin 定义变量的语法为: var/val name:Type
- var 关键字是 variable 的简称,表示该变量可以被修改
- val 关键字是 value 的简称,表示该变量一旦赋值后不能被修改
// 定义一个可以被修改的变量
var age : Int = 17
// 定义一个不可修改的变量
val id : Int= "1000"
// 还可以省略变量类型
// Kotlin会类型推导出变量的类型
var age = 17
val id = "1000"
复制代码
需要注意的是,val 表示该变量 引用不可变,但是对象里的内容可以变
1.2 Kotlin 类、枚举和属性
Kotlin 类的定义可以参考之前的文章:《从Java角度深入理解Kotlin》
在 Java 中使用 enum 关键定义枚举类
Kotlin 使用 enume class 来定义枚举类,如:
enum class Color(val r: Int, val g: Int, val b: Int ){ //枚举常量属性
// 定义枚举常量对象
RED(255, 0, 0), ORANGE(255, 165, 0),
YELLOW(255, 255, 0), GREEN(0, 255, 0),
BLUE(0, 0, 255), INDIGO(75, 0, 130),
VIOLET(238, 130, 238); //最后一个枚举对象需要分号结尾
// 在枚举类中定义函数
fun rgb() = (r * 256 + g) * 256 + b
}
复制代码
关于类的属性,在介绍如何创建类的时候已经有过详细的讲解,这里再做一些补充
如何自定义类属性的访问?
我们知道通过 val 关键声明的公有属性,只会生成它对应的 getter 函数
如果我们需要在这个 getter 函数里添加逻辑怎么做呢?如下所示:
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() {// 自定义 getter 方法
return height == width
}
}
复制代码
1.3 when、if 和循环语句
13.1. when
在 Java 中有 switch 语句,在 Kotlin 中使用 when 来代替 switch
- when 的基本语法
when(parameter){
branch1 -> logic
branch2 -> logic
}
复制代码
when 括号里是参数,参数是可选的。箭头(->) 左边是条件分支,右边是对应的逻辑体
when 不需要向 switch 那样需要加上 break 语句,符合条件自动具有 break 功能
如果逻辑体代码比较多,可以放到花括号({})里:
when(parameter){
branch1 -> {
//...
}
branch1 -> {
//...
}
}
复制代码
如果要组合多个分支,可以使用逗号(,)分隔分支:
when(parameter){
branch1,branch1 -> {
//...
}
}
复制代码
- 枚举类对象作为 when 参数
fun getMnemonic(color: Color) = when (color) {
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
复制代码
需要注意的是,when 使用枚举对象作为参数,需要把该枚举类的所有对象列举完
所以 枚举对象作为 when 参数不需要 else 分支
- 任意对象作为 when 参数
Kotlin 中的 when 比 Java 中的 switch 功能更强大
Java 的 switch 参数只能是 枚举常量、字符串、整型或整型的包装类型(浮点型不可以)
Kotlin 的 when 可以是任意对象:
fun mix(c1: Color, c2: Color) = when (setOf(c1, c2)) {
setOf(RED, YELLOW) -> ORANGE
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
//需要处理 其他 情况
else -> throw Exception("Dirty color")
}
复制代码
- 无参数的 when 表达式
上面的 mix 函数比较低效,因为每次比较的时候都会创建一个或多个 set 集合
如果该函数调用频繁,会创建很多临时对象
可以使用无参的 when 表达式来改造下:
fun mixOptimized(c1: Color, c2: Color) = when {
(c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) ->
ORANGE
(c1 == YELLOW && c2 == BLUE) || (c1 == BLUE && c2 == YELLOW) ->
GREEN
(c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) ->
INDIGO
else -> throw Exception("Dirty color")
}
复制代码
无参数的 when 表达式的条件分支必须是 boolean 类型
- 智能类型转换(smart casts)
在 Java 中对某个对象进行类型转换的时候时候,需要通过 instanceof 来判断是否可以被强转
void test(Object obj) {
if (obj instanceof String) {
String str = (String) obj;
str.substring(0, str.length() / 2);
}
//...
}
复制代码
Kotlin 通过 is 关键字来判断类型,并且编译器会自动帮你做类型转换
fun test(obj: Any) {
if (obj is String) {
// 不需要手动做类型转换操作
obj.substring(0, obj.length / 2)
}
//...
}
复制代码
1.3.2. if
if 表达式 用于条件判断,在 Kotlin 中 如果判断分支比较多,通常使用 when 来替代 if
fun test(obj: Any) {
when (obj) {
is String -> obj.substring(0, obj.length / 2)
is Type2 -> ignore
is Type3 -> ignore
}
}
复制代码
1.3.3. 循环
Kotlin 中的 while 和 do...while 循环和 Java 没有什么区别
while (condition) {
/*...*/
}
do {
/*...*/
} while (condition)
复制代码
for 循环的语法和 Java 中的循环还是有些区别
// Java for 循环
for (int i = 0; i <= 100; i++) {
System.out.println(i);
}
// 对应 Kotlin 版本
for(i in 0..100){
println(i)
}
复制代码
使用 .. 操作符 表示一个区间,该区间是闭区间,包含开始和结束的元素
然后使用 in 操作符来遍历这个区间
这个区间是从小到大的,如果开始的数字比结尾的还要大,则没有意义
如果想要表示 半闭区间 ,即只包含头部元素,不包含尾部
可以使用 until 操作符:
for(i in 0 until 100){
println(i)
}
复制代码
如果想要倒序遍历,可以使用 downStep 关键字:
for(i in 100 downTo 0){
println(i)
}
复制代码
遍历的时候 步长(step) 默认是 1,可以通过 step 关键字来指定步长
for( i in 100 downTo 0 step 2){
println(i)
}
复制代码
操作符 .. 和 downTo 表示区间都是闭区间,包含首尾元素的
1.4 Kotlin 异常处理
Kotlin 中的异常处理和 Java 的非常类似,但是也有一些用法上的区别
throw 关键字在 Kotlin 中是 表达式:
val percentage = if (number in 0..100)
number
else
throw IllegalArgumentException(
"A percentage value must be between 0 and 100: $number")
复制代码
另一个不同点是在 Kotlin 中可以选择性地处理 checked exception
fun readNumber(reader: BufferedReader): Int? {
try {
// throws IOException
val line = reader.readLine()
// throws NumberFormatException
return Integer.parseInt(line)
} catch (e: NumberFormatException) {
return null
} finally {
// throws IOException
reader.close()
}
}
复制代码
- reader.readLine() 会抛出 IOException 异常
- Integer.parseInt(line) 会抛出 NumberFormatException 异常
- reader.close() 会抛出 IOException 异常
但是我们只处理了 NumberFormatException 并没有对 IOException 进行处理
如果是在 Java 中则需要在声明函数的时候 throws IOException 如:
int readNumber( BufferedReader reader) throws IOException {
try {
String line = reader.readLine(); // throws IOException
return Integer.parseInt(line);
} catch (NumberFormatException e) {
return -1;
} finally {
reader.close(); // throws IOException
}
}
复制代码
当然我们也可以对 Integer.parseInt(line) 抛出的异常不做处理
因为 NumberFormatException 并不是 checked exception 而是 runtime exception
在 Java 中,对于 checked exception 是一定要显示的处理的,否则会编译报错;而对于runtime exception 则不会
对于上面的 Java 代码,还可以通过 Java7 的 try-with-resources 改造下:
int readNumber( BufferedReader reader) throws IOException {
try (reader) { //把需要管理的资源作为try的参数
String line = reader.readLine();
return Integer.parseInt(line);
} catch (NumberFormatException e) {
return -1;
}
// 省略 reader.close();
}
复制代码
在 Kotlin 中可以使用 use 函数来实现该功能:
fun readNumber(reader: BufferedReader): Int? {
reader.use {
val line = reader.readLine()
try {
return Integer.parseInt(line)
} catch (e: NumberFormatException) {
return null
}
// 省略 reader.close();
}
}
复制代码
2. 再谈 Kotlin 函数
上面我们已经介绍了函数的定义和组成,下面在继续分析函数的其他方面
2.1 更方便的函数调用
2.1.1 调用函数时指定参数的名字
假设我们有如下的函数:
fun joinToString(collection: Collection,
separator: String,
prefix: String,
postfix: String): String
复制代码
然后调用该函数(为参数值指定参数名称):
joinToString(collection, separator = " ", prefix = " ", postfix = ".")
复制代码
2.1.2 为函数参数指定默认值
我们可以把 joinToString 定义改成如下形式:
fun joinToString(collection: Collection,
separator: String = ", ",
prefix: String = "",
postfix: String = ""
复制代码
我们分别为函数的最后三个参数都设置了默认值,我们可以这样调用该函数:
joinToString(list)
joinToString(list, prefix = "# ")
复制代码
这样也就间接的实现了Java中所谓的重载(overload),代码也更简洁,不用定义多个方法了
2.1.3 Parameter和Argument的区别
看过 《Kotlin In Action》 的英文原版细心的同学可能会发现:书中的 3.2.1 章节是 Named Arguments
直译过来是:为参数命名。作者为什么没有写成 Named Parameters 呢?
下面我们就来看下 Parameter 和 Argument 的区别
简而言之,就是在定义函数时候的参数称之为 Parameter;调用函数传入的参数称之为 Argument
如下图所示:
因为 《Kotlin In Action》 的 3.2.1 章节是讲调用函数的时候为参数命名,所以使用了 Arguments
此外,除了 Parameter 和 Argument ,还有 Type Parameter 和 Type Argument
因为下面还要用到这两个的概念,所以这里我们介绍下 Type Parameter 和 Type Argument
Type Parameter 和 Type Argument 的概念是在泛型类或者泛型函数的时候出现:
2.2 顶级函数和属性
在 Java 中我们需要把函数和属性放在一个类中
在 Kotlin 中我们可以把某个函数或属性直接放到某个 Kotlin 文件中
把这样的函数或属性称之为 顶级(top level)函数或属性
例如在 join.kt 文件中:
package strings
fun joinToString(...): String {
...
}
复制代码
在 Java 代码中如何调用该方法呢?因为 JVM 虚拟机只能执行类中的代码
所以 Kotlin 会生成一个名叫 JoinKt 的类,并且顶级函数是静态的
所以可以在 Java 中这样调用顶级函数:
JoinKt.joinToString(...)
复制代码
在Kotlin中如何调用,如果在不同的包,需要把这个顶级函数导入才能调用
//相当于 import strings.JoinKt.joinToString
import strings.joinToString
//相当于 import strings.JoinKt.*
import strings.*
复制代码
所有的工具类都可以使用这样的方式来定义
顶级属性 同样也是 static 静态的
如果使用 var 来定义会生成对应的静态setter、getter函数
如果使用 val 来定义只会生成对应的静态getter函数
我们知道顶级函数和属性,最终还是会编译放在一个类里面,这个类名就是顶级函数或属性的 Kotlin文件名称+Kt
如果所在的Kotlin文件名被修改,编译生成的类名也会被修改,可以通过注解的方式来固定编译生成的类名:
@file:JvmName("StringFunctions")
package strings
fun joinToString(...): String {
...
}
复制代码
调用的时候就可以这样来调用:
import strings.StringFunctions;
StringFunctions.joinToString(list, ", ", "", "");
复制代码
2.3 扩展函数
何谓 扩展函数 ? 扩展函数是在类的外部定义,但是可以像类成员一样调用该函数
扩展函数的定义格式如下图所示:
其中 receiver type 就是我们扩展的目标类,receiver object 就是目标类的对象(哪个对象调用该扩展函数,这个this就是哪个对象)
lastChar 就是我们为 String 类扩展的函数
package strings
fun String.lastChar(): Char = this.get(this.length - 1)
复制代码
然后我们这样来调用该扩展函数:
println("Kotlin".lastChar())
复制代码
如果扩展函数所在的包名和使用地方的包名不一样的话,需要导入扩展函数
import strings.*
//或者
import strings.lastChar
val c = "Kotlin".lastChar()
复制代码
2.4 扩展函数原理分析
扩展函数本质上是静态函数,如上面的扩展函数 lastChar 反编译后对应的 Java 代码:
public static final char lastChar(@NotNull String $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
return $receiver.charAt($receiver.length() - 1);
}
复制代码
编译的时候,会在调用的该扩展函数的地方使用 StringUtilsKt.lastChar("") 代替
所以,如果要在 Java 中使用 Kotlin 定义的扩展函数,也是直接调用该静态方法即可
并且扩展函数是不能被覆写(override) 的,因为它本质上是一个静态函数
2.5 扩展属性
扩展属性和扩展函数的定义非常相似:
val String.lastChar: Char
get() = this.get(length - 1)
复制代码
我们必须为这个扩展属性定义 getter 函数,因为扩展属性没有 backing field
扩展属性在定义的时候,也会生成静态方法:
public static final char getLastChar(@NotNull String $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
return $receiver.charAt($receiver.length() - 1);
}
复制代码
如果扩展属性的 receiver object 可以被修改,可以把扩展属性定义成 var
var StringBuilder.lastChar: Char
get() = get(length - 1)
set(value: Char) {
this.setCharAt(length - 1, value)
}
复制代码
2.6 函数的可变参数和展开操作符
2.6.1 可变参数
在 Java 中通过三个点(...)来声明可变参数,如:
public static List listOf(T... items) {
System.out.println(items.getClass()); //数组类型
return Arrays.asList(items);
}
复制代码
Kotlin 和 Java 不一样,Kotlin 使用 vararg 关键来定义可变参数:
fun listOf(vararg items: T): List {
println(items.javaClass) //数组类型
return Arrays.asList(*items) // * spread operator
}
复制代码
对于可变参数的函数,调用它的时候可以传递任意个参数
2.6.2 展开操作符
通过上面的两段代码比较我们发现:Kotlin 需要显示的将可变参数通过 * 展开,然后传递给 asList 函数
这里的 * 就是 展开操作符(spread operator),在 Java 中是没有 展开操作符 的
下面我们再来看下,展开操作符的方便之处:
val intArr: Array = arrayOf(1, 2, 3, 4)
Arrays.asList(0, intArr).run {
println("size = $size")
}
//输出结果:
size = 2
复制代码
可以发现,不用展示操作符的话,集合里面只有两个元素
那我们把它改成使用 展开操作符 的情况:
val intArr: Array = arrayOf(1, 2, 3, 4)
Arrays.asList(0, *intArr).run {
println("size = $size")
}
//输出结果:
size = 5
复制代码
2.6.3 Java中的Arrays.asList()的坑和原理分析
既然上面用到了 Java 中的 Arrays.asList() 函数,下面来讲下该函数的容易遇到的坑及原理分析:
public static void testArrays() {
int[] intArr = {1, 2, 3};
List list = Arrays.asList(intArr);
println(list.size()); //size = 1
}
public static void testArrays2() {
Integer[] intArr ={1, 2, 3};
List list = Arrays.asList(intArr);
println(list.size()); //size = 3
}
复制代码
上面的 testArrays 和 testArrays2 函数非常相似,只不过是数组的类型不同,导致 Arrays.asList(arr) 返回的集合大小不一样
只要是 原始类型数组 Arrays.asList 返回的集合大小为 1,如果是 复杂类型的数组,Arrays.asList 返回的集合大小为数组的大小
为什么会产生这种情况呢?下面来分析下:
首先看下 Arrays.asList 是怎么定义的:
public static List asList(T... a)
复制代码
Java 中的可变参数相当于数组:
public static List asList(T[] a)
复制代码
我们知道 Java 中的泛型必须是复杂类型,所以这里的泛型 T 也必须是 复杂类型
当我们传递 int[] 数组的时候,就会出现问题,因为 int 是原始类型,T 是复杂类型
所以 int[] 赋值给 T[] 是非法的,当 一维原始类型的数组 当做给可变参数的时候,编译器会把这个可变参数编译成一个 二维数组
这就是为什么会出现上面情况的原因
我们再来看下 Arrays.asList 完整源码:
public static List asList(T... a) {
return new ArrayList<>(a);
}
private static class ArrayList extends AbstractList
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
//省略其他...
}
复制代码
经过上面的分析我们知道,如果是一维原始类型的数组传递给可变参数,这个可变参数就是 二维数组
然后把二维数组传递给内部ArrayList的构造方法,通过 E[] 保存下来。这里的泛型 E 就相当于 int[],E[] 相当于 int[][]
需要注意是 Java 不允许 将个二维数组 直接赋值 给一维的泛型数组:
int[][] intArray = {{1},{2}};
T[] t = intArray; //非法
复制代码
但是 Java 允许 把二维数组传递给参数是一维的泛型数组的函数,如:
public static void testGeneric(T[] data){
}
int[][] intArray = {{1},{2}};
testGeneric(intArray);
复制代码
2.6.4 Kotlin 展开操作符的原理分析
讲到这里你可能迫不及待的想知道,为什么我们上面的代码使用了展开操作符 Arrays.asList(*intArr) 返回的集合大小就是 5 呢?
val intArr: Array = arrayOf(1, 2, 3, 4)
Arrays.asList(0, *intArr).run {
println("size = $size")
}
//输出结果:
size = 5
复制代码
反编译后对应的 Java 代码如下:
Integer[] intArr2 = new Integer[]{1, 2, 3, 4};
SpreadBuilder var10000 = new SpreadBuilder(2);
var10000.add(0); //第1个元素
var10000.addSpread(intArr2); //数组里的4个元素
List var2 = Arrays.asList((Integer[])var10000.toArray(new Integer[var10000.size()]));
int var7 = false;
String var5 = "size = " + var2.size();
System.out.println(var5);
复制代码
原来会通过 SpreadBuilder 来处理展开操作符,SpreadBuilder 里面维护了一个ArrayList
所有的元素都会保存到这个 ArrayList 中,然后把这个集合转成 元素为复杂类型数组,再传给 Arrays.asList(arr) 函数
根据上面我们对 Arrays.asList(arr) 的分析,我们就知道返回的集合大小是 5 了
2.7 中缀调用
我们都知道什么是前缀(prefix),后缀(suffix)。那什么是函数的中缀(infix)调用呢?
使用关键字 infix 修饰的函数都能够 中缀调用
被关键字 infix 修饰的函数只能有一个参数
Kotlin 中的 to 就是一个中缀函数:
public infix fun A.to(that: B): Pair = Pair(this, that)
复制代码
下面我们来对比下 to 函数的常规调用和中缀调用:
1.to("one") //普通的函数调用
1 to "one" //函数的中缀调用
复制代码
除了 to 函数,还有我们介绍 循环 的时候讲到的 until、downTo、step 也是中缀函数:
public infix fun Int.until(to: Int): IntRange {
if (to <= Int.MIN_VALUE) return IntRange.EMPTY
return this .. (to - 1).toInt()
}
public infix fun Int.downTo(to: Int): IntProgression {
return IntProgression.fromClosedRange(this, to, -1)
}
public infix fun IntProgression.step(step: Int): IntProgression {
checkStepIsPositive(step > 0, step)
return IntProgression.fromClosedRange(first, last, if (this.step > 0) step else -step)
}
//使用示例:
for(i in 0 until 100){
}
for (i in 100 downTo 0 step 2) {
}
复制代码
2.8 局部函数
局部函数(local function) 是在函数里面定义函数,局部函数只能在函数内部使用
局部函数说白了就是函数嵌套。什么时候使用局部函数?当一个函数里的逻辑很多重复的逻辑,可以把这些逻辑抽取到一个局部函数。以《Kotlin In Action》的代码为例:
fun saveUser(user: User) {
if (user.name.isEmpty()) {
throw IllegalArgumentException("Cannot save user ${user.id}: Name is empty")
}
if (user.address.isEmpty()) {
throw IllegalArgumentException("Cannot save user ${user.id}: Address is empty")
}
// Save user to the database
}
复制代码
这个 saveUser 函数里面有些重复逻辑,如果 name 或 address 为空都会抛出异常
可以使用局部函数优化下:
fun saveUser(user: User) {
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: " + "$fieldName is empty")
}
}
validate(user.name, "Name")
validate(user.address, "Address")
// Save user to the database
}
复制代码
局部函数避免了模板代码的出现。如果不使用局部函数,我们需要把 validate函数 定义到外面去,但是这个函数只会被 saveUser函数 使用到,从而污染了外面的全局作用域。通过局部函数使得代码更加清晰,可读性更高。
需要注意的是,虽然 Kotlin 允许在函数内部定义函数,但是不要嵌套太深,否则会导致可读性太差
2.9 匿名函数
匿名函数顾名思义就是没有名字的函数:如:
fun(x: Int, y: Int): Int {
return x + y
}
复制代码
匿名函数的返回类型的推导机制和普通函数一样:
fun(x: Int, y: Int) = x + y
复制代码
如果声明了一个匿名函数 ,如何调用呢?
(fun(x: Int, y: Int): Int {
val result = x + y
println("sum:$result")
return result
})(1, 9)
输出结果:
sum:10
复制代码
3. 字符串
Kotlin 的 String 字符串和 Java 中的几乎是一样的,Kotlin 在此基础上添加了一系列的扩展函数,方便开发者更好的使用字符串
同时也屏蔽了 Java String 中容易引起开发者困惑的函数,下面我们从 String 的 split 函数开始说起
3.1. String.split()
在 Java 中的 split 函数接收一个字符串参数:
public String[] split(String regex)
复制代码
开发者可能会这样来使用它:
public static void main(String[] args) {
String[] arr = "www.chiclaim.com".split(".");
System.out.println(arr.length); // length = 0
}
复制代码
我们想通过字符 . 来分割字符串 www.chiclaim.com 但是返回的是数组大小是 0
因为 split 函数接收的是一个正则字符串,而字符 . 在正则中表示所有字符串
为了避免开发开发者的困惑,Kotlin 对 CharSequence 扩展了 split 函数
如果你想通过字符串来分割,你可以调用:
public fun CharSequence.split(
vararg delimiters: String,
ignoreCase: Boolean = false,
limit: Int = 0): List
复制代码
如果你想通过正则表达式来分割,你可以调用:
fun CharSequence.split(regex: Regex, limit: Int = 0): List
复制代码
通过不同的参数类型来减少开发者在使用过程中出错的几率
3.2. 三引号字符串
假如我们需要对如下字符串,分割成 路径、文件名和后缀: “/Users/chiclaim/kotlin-book/kotlin-in-action.doc”
fun parsePathRegexp(path: String) {
val regex = "(.+)/(.+)\\.(.+)".toRegex()
val matchResult = regex.matchEntire(path)
if (matchResult != null) {
val (directory, filename, extension) = matchResult.destructured
println("Dir: $directory, name: $filename, ext: $extension")
}
}
复制代码
我们从中可以看出 (.+)/(.+)\.(.+) 我们使用了两个反斜杠
不用反斜杠的话字符 . 表示任意字符,所以需要用反斜杠转义(escape)
但是如果使用一个反斜杠,编译器会包错:非法转义符
在 Java 中两个反斜杠表示一个反斜杠
这个时候可以使用三引号字符串,这样就不要只需要一个反斜杠
val regex = """(.+)/(.+)\.(.+)""".toRegex()
复制代码
在三引号字符串中,不需要对任何字符串转义,包括反斜杠
上面的例子,除了可以使用正则来实现,还可以通过 Kotlin 中内置的一些函数来实现:
fun parsePath(path: String) {
val directory = path.substringBeforeLast("/")
val fullName = path.substringAfterLast("/")
val fileName = fullName.substringBeforeLast(".")
val extension = fullName.substringAfterLast(".")
println("Dir: $directory, name: $fileName, ext: $extension")
}
复制代码
三引号字符串除了可以避免字符转义,三引号字符串还可以包含任意字符串,包括换行
然后输出时候可以原样输出 多行三引号字符串 格式:
val kotlinLogo = """
| //
.|//
.|/ \"""
println(kotlinLogo)
输出结果:
| //
.|//
.|/ \
复制代码
所以可以将 JSON 字符串很方便的放在 三引号字符串 中,不用管 JSON 内的特殊字符
4. 类、对象和接口
关于类的定义、对象的创建、接口的原理分析,可以参考之前的文章:《从Java角度深入理解Kotlin》
5. Lambda表达式
关于Lambda表达式及原理分析,可以参考之前的文章:《从Java角度深入理解Kotlin》
6. Kotlin 类型体系
6.1 原始数据类型
关于 Kotlin 原始数据类型,可以查看之前的文章《从Java角度深入理解Kotlin》
里面有详细的介绍,下面我们就来介绍下 Kotlin 中关于 可空类型
6.2 可空类型
可空类型 是 Kotlin 用来避免 NullPointException 异常的
例如下面的 Java 代码就可能会出现 空指针异常:
/*Java*/
int strLen(String s){
return s.length();
}
strLen(null); // throw NullPointException
复制代码
如果上面的代码想要在 Kotlin 中避免空指针,可改成如下:
fun strLen(s: String) = s.length
strLen(null); // 编译报错
复制代码
上面的函数参数声明表示参数不可为null,调用的时候杜绝了参数为空的情况
如果允许 strLen 函数可以传 null 怎么办呢?可以这样定义该函数:
fun strLenSafe(s: String?) = if (s != null) s.length else 0
复制代码
在参数类型后面加上 ? ,表示该参数可以为 null
需要注意的是,可为空的变量不能赋值给不可为空的变量,如:
val x: String? = null
var y: String = x //编译报错
//ERROR: Type mismatch: inferred type is String? but String was expected
复制代码
在为空性上,Kotlin 中有两种情况:可为空和不可为空;而 Java 都是可以为空的
6.3 安全调用操作符:?.
安全调用操作符(safe call operator): ?.
安全调用操作符 结合了 null 判断和函数调用,如:
fun test(s:String?){
s?.toUpperCase()
}
复制代码
如果 s == null 那么 s?.toUpperCase() 返回 null,如果 s!=null 那就正常调用即可
如下图所示:
所以上面的代码不会出现空指针异常
安全调用操作符 ?.,不仅可以调用函数,还可以调用属性。
需要注意的是,使用了 ?. 需要注意其返回值类型:
val length = str?.length
if(length == 0){
//do something
}
复制代码
这个时候如果 str == null 的话,那么 length 就是 null,它永远不等于0了
6.4 Elvis操作符: ?:
Elvis操作符 用来为null提供默认值的,例如:
fun foo(s: String?) {
val t: String = s ?: ""
}
复制代码
如果 s == null 则返回 "",否则返回 s 本身,如下图所示:
上面介绍 可空性 时候的例子可以通过 Elvis操作符改造成更简洁:
fun strLenSafe(s: String?) = if (s != null) s.length else 0
//改成如下形式:
fun strLenSafe(s: String?) = s.length ?: 0
复制代码
6.5 安全强转操作符:as?
前面我们讲到了 Kotlin 的智能强转(smart casts),即通过 is 关键字来判断是否属于某个类型,然后编译器自动帮我们做强转操作
如果我们不想判断类型,直接强转呢?在 Java 中可能会出现 ClassCastException 异常
在 Kotlin 中我们可以通过 as? 操作符来避免类似这样的异常
as? 如果不能强转返回 null,反之返回强转之后的类型,如下图所示:
6.6 非空断言:!!
我们知道 Kotlin 中类型有可为空和不可为空两种
比如有一个函数的参数是不可空类型的,然后我们把一个可空的变量当做参数传递给该函数
此时Kotlin编译器肯定会报错的,这个时候可以使用非空断言。非空断言意思就是向编译器保证我这个变量肯定不会为空的
如下面伪代码:
var str:String?
// 参数不可为空
fun test(s: String) {
//...
}
// 非空断言
test(str!!)
复制代码
注意:对于非空断言要谨慎使用,除非这个变量在实际情况真的不会为null,否则不要使用非空断言。虽然使用了非空断言编译器不报错了,但是如果使用非空断言的变量是空依然会出现空指针异常
非空断言的原理如下图所示:
6.7 延迟初始化属性
延迟初始化属性(Late-initialized properties),主要为了解决没必要的 非空断言 的出现
例如下面的代码:
class MyService {
fun performAction(): String = "foo"
}
class MyTest {
private var myService: MyService? = null
@Before fun setUp(){
myService = MyService()
}
@Test fun testAction(){
Assert.assertEquals("foo",myService!!.performAction())
}
}
复制代码
我们知道属性 myService 肯定不会为空的,但是我们不得不为它加上 非空断言
这个时候可以使用 lateinit 关键字来对 myService 进行延迟初始化了
class MyTest {
private lateinit var myService: MyService
@Before fun setUp(){
myService = MyService()
}
@Test fun testAction(){
Assert.assertEquals("foo", myService.performAction())
}
}
复制代码
这样就无需为 myService 加上非空断言了
6.8 可空类型的扩展函数
在前面的章节我们已经介绍了扩展函数,那什么是 可空类型的扩展函数?
可空类型的扩展函数 就是在 Receive Type 后面加上问号(?)
如 Kotlin 内置的函数 isNullOrBlank:
public inline fun CharSequence?.isNullOrBlank(): Boolean
复制代码
Kotlin 为我们提供了一些常用的 可空类型的扩展函数
如:isNullOrBlank、isNullOrEmpty
fun verifyUserInput(input: String?){
if (input.isNullOrBlank()) {
println("Please fill in the required fields")
}
}
verifyUserInput(null)
复制代码
有些人可能会问 input==null,input.isNullOrBlank() 不会空指针吗?
根据上面对扩展函数的讲解,扩展函数编译后会变成静态调用
6.9 数字类型转换
Kotlin 和 Java 另一个重要的不同点就是数字类型的转换上。
Kotlin 不会自动将数字从一个类型转换到另一个类型,例如:
val i = 1
val l: Long = i // 编译报错 Type mismatch
复制代码
需要显示的将 Int 转成 Long:
val i = 1
val l: Long = i.toLong()
复制代码
这些显式类型转换函数定义在每个原始类型上,除了 Boolean 类型
Kotlin 之所以在数字类型的转换上使用显示转换,是为了避免一些奇怪的问题。
例如,下面的 Java 例子 返回 false:
new Integer(42).equals(new Long(42)) //false
复制代码
Integer 和 Long 使用 equals 函数比较,底层是先判断参数的类型:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
复制代码
如果 Kotlin 也支持隐式类型转换的话,下面的代码也会返回 false ,因为底层也是通过 equals 函数来判断的:
val x = 1 // Int
val list = listOf(1L, 2L, 3L)
x in list
复制代码
但是在Kotlin中上面的代码会编译报错,因为类型不匹配
上面的 val x = 1,没有写变量类型,Kotlin编译器会推导出它是个 Int
- 如果字面量是整数,那么类型就是 Int
- 如果字面量是小数,那么类型就是 Double
- 如果字面量是以 f 或 F 结尾,那么类型就是 Float
- 如果字面量是 L 结尾,那么类型就是 Long
- 如果字面量是十六进制(前缀是0x或0X),那么类型是 Long
- 如果字面量是二进制(前缀是0b或0B),那么类型是 Int
- 如果字面量是单引号中,那么类型就是 Char
需要注意的是,数字字面量当做函数参数或进行算术操作时,Kotlin会自动进行相应类型的转换
fun foo(l: Long) = println(l)
val y = 0
foo(0) // 数字字面量作为参数
foo(y) // 编译报错
val b: Byte = 1
val l = b + 1L // b 自动转成 long 类型
复制代码
6.10 Any类型
Any 类型 和 Java 中的 Object 类似,是Kotlin中所有类的父类
包括原始类型的包装类:Int、Float 等
Any 在编译后就是 Java 的 Object
Any 类也有 toString() , equals() , and hashCode() 函数
如果想要调用 wait 或 notify,需要把 Any 强转成 Object
6.11 Unit 类型
Unit 类型和 Java 中的 void 是一个意思
下面介绍它们在使用过程的几个不同点:
- 函数没有返回值,Unit可以省略
例如下面的函数可以省略 Unit:
fun f(): Unit { ... }
fun f() { ... } //省略 Unit
复制代码
但是在 Java 中则不能省略 void 关键字
- Unit 作为 Type Arguments
例如下面的例子:
interface Processor {
fun process(): T
}
// Unit 作为 Type Arguments
class NoResultProcessor : Processor {
override fun process() { // 省略 Unit
// do stuff
}
}
复制代码
如果在 Java 中,则需要使用 Void 类:
class NoResultProcessor implements Processor {
@Override
public Void process() {
return null; //需要显式的 return null
}
}
复制代码
6.12 Nothing 类型
Nothing 类是一个 标记类
Nothing 不包含任何值,它是一个空类
public class Nothing private constructor()
复制代码
Nothing 主要用于 函数的返回类型 或者 Type Argument
关于 Type Argument 的概念已经在前面的 Parameter和Argument的区别 章节介绍过了
下面介绍下 Nothing 用于函数的返回类型
对于有些 Kotlin 函数的返回值没有什么实际意义,特别是在程序异常中断的时候,例如:
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
复制代码
你可能会问,既然返回值没有意义,使用Unit不就可以了吗?
但是如果使用Unit,当与 Elvis 操作符 结合使用的时候就不太方便:
fun fail(message: String) { // return Unit
throw IllegalStateException(message)
}
fun main() {
var address: String? = null
val result = address ?: fail("No address")
//编译器报错,因为result是Unit类型,所以result没有length属性
println(result.length)
}
复制代码
这个时候使用 Nothing 类型作为 fail 函数的返回类型 就可以解决这个问题:
fun fail(message: String) : Nothing {
throw IllegalStateException(message)
}
fun main() {
var address: String? = null
val result = address ?: fail("No address")
println(result.length) // 编译通过
}
复制代码
7. 集合与数组
可以参考:《从Java角度深入理解Kotlin》 里关于集合和数组的部分
8. Kotlin 操作符重载
操作符重载内容有点多,单独写了一篇文章:《Kotlin操作符重载详解》 ,学完这篇文章相信你对 Kotlin 操作符 有全新的理解和掌握
9. 高阶函数
高阶函数的定义及原理分析,可以参考之前的文章:《从Java角度深入理解Kotlin》
10. 内联函数
《从Java角度深入理解Kotlin》中关于高阶函数的部分,我们说到为了优化高阶函数需要使用 inline 关键字来修饰
但是不是所有的 高阶函数 都能声明为内联(inline)函数
如果高阶函数的 lambda 参数被局部变量保存起来,那么这个高阶函数就不能被 inline 修饰
因为如果Kotlin支持这么做,那么这个局部变量就必须包含lambda代码,但是Java/Kotlin底层并没有能保存一段代码的类型
例如高阶函数 Sequence
public fun Sequence.map(transform: (T) -> R): Sequence {
return TransformingSequence(this, transform)
}
internal class TransformingSequence
constructor(
private val sequence: Sequence,
private val transformer: (T) -> R) : Sequence {
// 省略实现代码
}
复制代码
从中可以发现,Sequence
然后被 TransformingSequence 通过属性的形式将该lambda 保存起来了
所以一个高阶函数能够被内联,要么参数lambda被直接使用,要么直接传递给另一个内联函数,没有将lambda保存到局部变量或者属性中
另一方面,如果一个高阶函数接收多个lambda,而且有的lambda参数包含的代码量比较大,可以指定该lambda不内联:
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
// ...
}
复制代码
11. 泛型
11.1 泛型的类型参数
泛型的类型参数:Type Parameter 和 Type Argument 已经在前面介绍过了,这里就不赘述了
就像一般类型一样,Type Argument 通常能够被 Kotlin 编译推导出具体的类型
例如,通过 listOf 函数创建集合:
val list = listOf("Chiclaim", "Kotlin")
复制代码
此时Kotlin编译推导出 Type Argument 的类型是 String,所以创建的集合是 String 类型的
如果你创建的是一个空集合,那么你就必须指定 Type Argument
因为空集合 Kotlin 编译器无法推导出 Type Argument 的类型:
val list: MutableList = mutableListOf()
//或者
val list = mutableListOf()
复制代码
所以,在Kotlin中要么显式地指定 Type Argument;要么编译器能够推导出 Type Argument 的类型
但是,Java是允许不指定 Type Argument 的,这主要是因为 泛型是 JDK1.5 才出来的新特性
为了兼容以前的代码,所以 Java 允许在使用泛型的时候不指定 Type Argument,这就是所谓的 raw type
11.2 泛型函数和属性
例如上面用到的 listOf 就是一个泛型函数
除此之外,泛型还可以用在 扩展函数 上,如:
public fun List.slice(indices: IntRange): List
复制代码
该函数的 type parameter 用在 receiver type 和 return type 中,如下图所示:
同样地,调用泛型扩展函数要么显式指定 type argument,要么编译能够推导出类型:
val letters = ('a'..'z').toList()
letters.slice(0..2)) // 显式指定 type argument
letters.slice(10..13) // 编译器类型推导出 Char
复制代码
泛型除了可以用在扩展函数上,还可以用到扩展属性上:
val List.penultimate: T
get() = this[size - 2]
println(listOf(1, 2, 3, 4).penultimate)
复制代码
11.3 泛型类
Kotlin 泛型类和 Java 的泛型类差不多,都是将泛型放到 <> 中
// 声明类的时候声明 type parameter
interface List {
//将 type parameter 用于返回类型
operator fun get(index: Int): T {
// ...
}
}
复制代码
继承或者实现这个接口的时候需要提供 type argument
这个 type argument 可以是具体的类,也可以是另一个泛型:
// type argument 就是 String 类
class StringList: List {
override fun get(index: Int): String = ...
}
//使用另一个泛型T当做 type argument
class ArrayList : List {
override fun get(index: Int): T = ...
}
复制代码
需要注意的是 ArrayList
ArrayList 定义了一个全新的 Type parameter 名为 T,然后将其当做父类List的 Type argument
11.4 Type parameter 约束
Type parameter约束 能够限制 Type argument 的类型
例如下面的一段 Java 代码:
T sum(List list)
复制代码
调用 sum 函数的时候,List 集合里的元素只能是 Number 或 Number 的子类型
也就是说 Type argument 只能是 Number 或 Number的子类型
把这里的 Number 称之为 上界(upper bound)
上面的代码用 Kotlin 来表示就是这样的:
fun List.sum(): T
复制代码
上面只是限制 T 是 Number 或者 Number 的子类,如果还想加上其他限制呢?
例如下面的例子,为 Type parameter 加上了两个类型约束
T 必须同时满足两个条件:
- T 是 CharSequence 或者 CharSequence 子类
- T 是 Appendable 或者 Appendable 子类
fun ensureTrailingPeriod(seq: T)
where T : CharSequence, T : Appendable {
if (!seq.endsWith('.')) {
seq.append('.')
}
}
val helloWorld = StringBuilder("Hello World")
ensureTrailingPeriod(helloWorld)
//下面代码 编译报错,因为String是CharSequence的子类,但不是Appendable子类
ensureTrailingPeriod("Hello Word")
复制代码
11.5 泛型的不变性、协变性、逆变性及泛型具体化
泛型的不变性、协变性、逆变性及泛型具体化及原理分析,可以参考之前的文章:《从Java角度深入理解Kotlin》
12. Kotlin 中的 break、return 和 label
12.1 返回和跳转
Kotlin 有三种结构化跳转表达式:
- break。默认终止最直接包围它的循环。
- continue。默认继续下一次最直接包围它的循环。
- return。默认从最直接包围它的函数或者匿名函数返回。
12.1.1 break 和 break label
break默认是终止离它最近的循环,这个和 Java 是一样的:
fun breakLabel() {
for (i in 5..10) {
for (j in 1..10) {
//当 i==j 中断最里面的循环
if (i == j) break
println("$i-$j")
}
}
}
复制代码
如果要跳出最外层循环,则需要和 label,结合使用
label 的语法为:labelName@
fun breakLabel() {
//最外层循环处定义了一个名为loop的label
loop@ for (i in 5..10) {
for (j in 1..10) {
if (i == j) break@loop //跳出最外层循环
println("$i-$j")
}
}
}
//输出结果:
5-1
5-2
5-3
5-4
复制代码
12.1.2 continue 和 continue label
continue 、continue label 用法和 break、break label 是类似的,就不做赘述了
12.1.3 return 和 return label
return 默认从最直接包围它的函数或者匿名函数返回。如:
fun foo() {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return // 跳出整个函数
print(it)
}
println("this point is unreachable")
}
复制代码
有可能有人会疑问:离 return 表达式最近的不是 forEach 函数吗?不应该是 return forEach 函数吗?
其实 forEach 是一个高阶函数,该函数接收一个 lambda表达式,更准确的说 return 表达式是 在lambda 表达式内
而lambda表达式不是函数,所以 最直接包围 return 表达式的 是 foo 函数
如果想 return forEach 处怎么办呢?有三种方式
第一种方式,配合 label 来实现:
fun foo() {
listOf(1, 2, 3, 4, 5).forEach lit@{ // 声明 名为 lit 的 label
if (it == 3) return@lit
print(it)
}
print(" done with explicit label")
}
//输出结果
1245 done with explicit label
复制代码
第二种是方式:使用 隐式label
每个高阶函数都有和它名字一样的隐式label:
fun foo() {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return@forEach
print(it)
}
print(" done with implicit label")
}
//输出结果
1245 done with explicit label
复制代码
第三种方式:使用 匿名函数
fun foo() {
listOf(1, 2, 3, 4, 5).forEach(fun(value: Int) {
if (value == 3) return
print(value)
})
print(" done with anonymous function")
}
//输出结果
1245 done with explicit label
复制代码
可以看出当 value==3 的时候被过滤掉了,但是还会继续下一次循环
如果想要 value==3 中断循环,怎么办?
fun foo() {
run loop@{
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return@loop
print(it)
}
}
print(" done with nested loop")
}
//输出结果
12 done with nested loop
复制代码
12.2 lambda表达式中return一个值
例如 Kotlin 中的 filter 函数,接收一个lambda 表达式,该lambda返回一个 boolean 值
listOf().filter { it > 0 }
复制代码
在该例子中,lambda体内只有一行表达式,且该表达式就是 boolean 类型,所以不需要显式的 return
但是如果需要在 lambda body 里进行复杂的判断,然后根据条件来 return 怎么办?
当然上面的代码可以通过 when 来简化,这个例子代码只是来演示 lambda return 值的情况
13. Kotlin反射和注解
13.1 定义注解
定义注解的方式非常简单:
annotation class AnnotationName
复制代码
定义注解的语法和定义类的语法非常相似,只需要在后者的基础上加上 annotation 关键字
为注解加上参数:
annotation class JsonName(val name: String)
复制代码
相比之下,Java定义的方式是这样的:
public @interface JsonName {
String name();
}
复制代码
在使用 JsonName 注解的时候,Kotlin 注解是允许省略 name 参数的,而 Java 不允许:
@JsonName("Chiclaim") //Kotlin允许省略参数名name
@JJsonName(name = "Chiclaim") // Java不允许省略参数名name
var username: String? = null
复制代码
13.2 元注解(Meta-Annotation)
元注解就是描述注解的注解。 元注解主要用于告诉编译器如何处理该注解
13.2.1 @Target元注解
@Target(AnnotationTarget.PROPERTY)
annotation class JsonName
复制代码
Target 注解就是元注解,告诉编译器 JsonName 注解只用到类的属性上
AnnotationTarget 是一个枚举类,常用的枚举对象有:
- CLASS:用于类、接口、注解类
- ANNOTATION_CLASS:只用于注解类
- TYPE_PARAMETER:泛型参数
- PROPERTY:属性
- FIELD:字段,包括back-field
- LOCAL_VARIABLE:局部变量
- VALUE_PARAMETER:用于函数或构造函数的参数
- CONSTRUCTOR:仅用于构造函数
- FUNCTION:用于函数,不包括构造函数
- PROPERTY_GETTER:仅用于属性的getter函数
- PROPERTY_SETTER:仅用于属性的setter函数
- EXPRESSION:用于表达式
13.2.2 @Retention元注解
@Retention 元注解用于告诉编译器,目标注解保留到哪个阶段,例如是编译还是运行时阶段
Java 默认的保留到 class 字节码中,运行时不可见
Kotlin 默认是保留到 runtime,运行时可见
@Retention 通过 AnnotationRetention 来制定注解保留在哪个阶段:
- SOURCE:注解只保留在源文件中,编译后的二进制文件不存在该注解
- BINARY:注解保留在编译后的二进制文件中
- RUNTIME:注解保留到运行时,反射可见
13.2.3 @Repeatable元注解
允许在一个元素上使用多次该注解,例如:
@JsonName
@JsonName
var firstName: String? = null
复制代码
13.2.4 @MustBeDocumented元注解
表明该注解是生成的API文档的一部分,这样在API文档中既可以看到该注解了
13.3 Kotlin 中常用的注解
13.3.1 @Deprecated
Kotlin中的 @Deprecated 比 Java 中的更强大,Kotlin 还可以指定用哪个 API 来替换废弃的API
@Deprecated("use greeting", ReplaceWith("greeting()",
"annotation.Person.greeting()"))
fun sayHello(){
}
复制代码
还可以使用 修复 (Show Intention Actions) 快捷键将废弃的 API 用替换成建议使用的 API:
13.3.2 @Suppress
@Suppress 用来去除代码中的一些 warning
对于编辑器提示的 warning,我们要尽可能的去修复,因为有可能是代码的不规范导致的
但是对于一些无关紧要的或者暂时无法处理的 warning,可以通过 @Suppress 来去掉 warning
@Suppress("UNCHECKED_CAST")
fun test(source: Any) {
if (source is List<*>) {
val list = source as List
val str: String = list[0]
}
}
复制代码
有些同学可能会问,@Suppress 注解的参数,具体有哪些呢?
这些参数统一定义在 Errors.java 中
13.3.3 @JvmField
编译器不会为被 @JvmField 修饰的属性生成getter、setter方法:
class Person(
@JvmField
var name: String,
var age: Int
)
//生成代码:
public final class Person {
@JvmField
@NotNull
public String name;
private int age;
public final int getAge() {
return this.age;
}
public final void setAge(int var1) {
this.age = var1;
}
public Person(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.name = name;
this.age = age;
}
}
复制代码
name 字段没有生成 getter、setter 方法 并且该属性是 public 的
也就是说 Kotlin 编译器把该属性当做 字段 暴露给外面,可以通过 private 关键显式指定为 private
13.3.4 @JvmName
@JvmName 指定编译器生成的 class 字节码中该元素对应的名称
用于解决 Kotlin 和 Java 交互 Kotlin 命名发生变化的兼容问题
//指定编译生成的class字节码文件的名称
@file:JvmName("JavaClassName")
package annotation.kotlin_annoation
//指定 javaMethodName 编译成 class 字节码后,对应的名称
@JvmName("java_method_name")
fun javaMethodName(){
println("java method name")
}
//在Kotlin中调用
javaMethodName()
//在Java中调用(Kotlin函数名发生变化也不影响Java调用)
JavaClassName.java_method_name();
复制代码
13.3.5 @JvmStatic
@JvmStatic 用于告诉编译器在 class字节码 中生成一个静态方法
这样 Java 在调用这个 Kotlin 的 companion 函数是就不需要 Companion 内部类了
class JvmStaticTest {
companion object {
fun greeting() {
println("hello...")
}
@JvmStatic
fun sayHello() {
println("hello...")
}
}
}
//在 Java 中调用
JvmStaticTest.Companion.greeting();
JvmStaticTest.sayHello();
复制代码
13.4. Kotlin 反射
13.4.1 Kotlin反射的基本概念
Java 中的反射 API 主要集中在 com.java.reflect 包中
Kotlin 中的反射 API 主要集中在 kotlin.reflect 包中
为什么 Kotlin 还要搞一套反射的 API 呢?
因为 Kotlin 有很多 Java 没有的语言特性,
例如 Java 是通过 Class 来描述一个类,Kotlin 通过 KClass 来表述一个类
对于 Kotlin 一个类,它可能是 sealed class,而 Java 中的类没有这个概念
所以 Kotlin 的反射 API 是针对语言本身特性设计的
同时,Kotlin 在编译后也是 class字节码,所以 Java 的反射API是兼容所有 Kotlin 代码的
下面我们来看下常用常用的 Kotlin 和 Java API的对应关系:
Java | Kotlin |
---|---|
java.lang.Class | kotlin.reflect.KClass |
java.lang.reflect.Field | kotlin.reflect.KProperty |
java.lang.reflect.Method | kotlin.reflect.KFunction |
java.lang.reflect.Parameter | kotlin.reflect.KParameter |
我们知道 Kotlin 中的 KClass 对应 Java 中的 Class,那么在 Kotlin 代码中如何获取 Class 和 KClass:
// Kotlin Class
val kclazz: KClass = Book::class
val kclazz2: KClass = book.javaClass.kotlin
// Java Class
val jclazz: Class = Book::class.java
val jclazz2: Class = book.javaClass
复制代码
13.4.2 成员引用和反射
上面的获取 Class 和 KClass 代码中使用到了成员引用操作符(::),所以下面介绍成员引用和反射的关系
成员引用(Member references)包括:属性引用(Property references)和函数引用(Function references)
- 属性引用
如果成员引用操作符后面是属性,那么这就是属性引用
class Book(val name: String, var author: String) {
fun present() = "book's name = $name, author = $author "
}
// 属性引用
val pro = Book::name
复制代码
我们上面定义的属性引用到底是什么呢?看下反编译后的Java代码:
KProperty1 prop = MemberReferenceTestKt$main$prop$1.INSTANCE;
final class MemberReferenceTestKt$main$prop$1 extends PropertyReference1 {
public static final KProperty1 INSTANCE = new MemberReferenceTestKt$main$prop$1();
public String getName() {
return "name";
}
public String getSignature() {
return "getName()Ljava/lang/String;";
}
public KDeclarationContainer getOwner() {
return Reflection.getOrCreateKotlinClass(Book.class);
}
@Nullable
public Object get(@Nullable Object receiver) {
return ((Book)receiver).getName();
}
}
复制代码
发现我们上面的定义的属性引用,内部生成了一个继承自 PropertyReference1 的内部类
而这个 PropertyReference1 实际上最终实现了 KProperty 接口
所以 Book::name 返回的值实际上是 KProperty 类型
由于属性 name 使用 val 关键字修饰,如果是 var 关键字修饰,
Book::name 返回的值实际上是 KMutableProperty 类型
KProperty
- 第一个泛型参数是 receiver 的类型,receiver 就是属性的所有者
- 第二个泛型参数是该属性的类型
介绍完 KProperty 我们可以写一个函数用来输出任意类的 属性名称 和 属性值
fun printProperty(instance: T, prop: KProperty1) {
println("${prop.name} = ${prop.get(instance)}")
}
复制代码
还可以利用 KProperty 来为 var 修饰的属性设置值:
fun changeProperty(instance: T, prop: KMutableProperty1) {
val value = prop.get(instance)
prop.set(instance, "$value, Johnny")
}
复制代码
下面演示下这两个函数的使用:
val book = Book("Kotlin从入门到放弃", "Chiclaim")
// 打印 name 属性的值
printProperty(book, Book::name)
// 打印 author 属性的值
printProperty(book, Book::author)
// 修改 author 属性的值
changeProperty(book, Book::author)
println("book's author is [${book.author}]")
输出结果:
name = Kotlin从入门到放弃
author = Chiclaim
book's author is [Chiclaim, Johnny]
复制代码
- 函数引用
在 成员引用操作符 后面是函数名的就是函数引用,例如:
// 函数引用
val fk = Book::present
复制代码
属性引用 会生成一个内部类实现了 KProperty 接口,同理,函数引用 内部也会生成一个内部类,只不过这个类实现了 KFunction 接口
所以 Book::present 返回值是 KFunction 类型
可以用过 KFunction 来打印函数名称和函数调用:
val fk = Book::present
// 函数名称
println("function name is ${fk.name}")
// 函数调用
println(fk.call(book))
输出结果:
function name is present
book's name = Kotlin从入门到放弃, author = Chiclaim, Johnny
复制代码
对于顶级函数可以这样来定义函数引用:::foo
联系我
下面是我的公众号,干货文章不错过,有需要的可以关注下,有任何问题可以联系我:
Reference
《Kotlin in action》
Java expressions