前面的 Kotlin 的文章《从Java角度深刻理解Kotlin》虽然已经对 Kotlin 相关的核心概念有了全面的介绍,可是不少基础入门的知识没有说起。html
这对 Kotlin 新手来讲仍是不是很好理解,因此特意写了这篇《Kotlin从入门到进阶》java
写这篇文章主要有一下几个目的:git
可是出于篇幅的缘由,仍是有不少知识没有介绍到,对其余知识点有兴趣的能够自行查阅github
函数和变量这两个概念是 Kotlin 中最基本的两个元素,在介绍其余概念以前,先介绍下这两个基本概念正则表达式
下面咱们来定义一个函数:express
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
复制代码
对上面的函数作个解释:编程
以下图所示:数组
须要注意的是 Kotlin 中没有像 Java 中的 三元运算符 了安全
在 Java 中上面的 函数体 能够改为这样:bash
return (a > b) ? a : b
复制代码
Kotlin 使用 if 语句来代替 三目运算符
咱们在学习任何编程语言的时候,都会遇到两个概念:
可能有些开发者还搞不清什么是 表达式 ,什么是 语句
在不一样的编程语言中对 表达式和语句的定义 可能会有一些细微的差异
在 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),它包括:
Kotlin 和 Java 中对表达式和语句的定义都是相似的
可是对于有些关键字是语句仍是表达式和 Java 仍是有些区别的
如上所述,在 Java 中全部的 控制流 都是语句
在 Kotlin 的控制流中除了 循环(for/while/do..while) ,其余的都是表达式
既然是表达式,那么它就是表示某种类型的数据,能够把它赋值给变量
val max = if (a > b) a else b
复制代码
在 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) 来得知该表达式的类型
而后把获得的类型当作函数的返回值类型
Kotlin 中对变量的定义和 Java 不同
在 Java 中一般以变量的类型开头,后面跟着变量名称
Kotlin 定义变量的语法为: var/val name:Type
// 定义一个能够被修改的变量
var age : Int = 17
// 定义一个不可修改的变量
val id : Int= "1000"
// 还能够省略变量类型
// Kotlin会类型推导出变量的类型
var age = 17
val id = "1000"
复制代码
须要注意的是,val 表示该变量 引用不可变,可是对象里的内容能够变
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
}
}
复制代码
在 Java 中有 switch 语句,在 Kotlin 中使用 when 来代替 switch
when(parameter){
branch1 -> logic
branch2 -> logic
}
复制代码
when 括号里是参数,参数是可选的。箭头(->) 左边是条件分支,右边是对应的逻辑体
when 不须要向 switch 那样须要加上 break 语句,符合条件自动具备 break 功能
若是逻辑体代码比较多,能够放到花括号({})里:
when(parameter){
branch1 -> {
//...
}
branch1 -> {
//...
}
}
复制代码
若是要组合多个分支,可使用逗号(,)分隔分支:
when(parameter){
branch1,branch1 -> {
//...
}
}
复制代码
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 分支
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")
}
复制代码
上面的 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 类型
在 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)
}
//...
}
复制代码
if 表达式 用于条件判断,在 Kotlin 中 若是判断分支比较多,一般使用 when 来替代 if
fun test(obj: Any) {
when (obj) {
is String -> obj.substring(0, obj.length / 2)
is Type2 -> ignore
is Type3 -> ignore
}
}
复制代码
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 表示区间都是闭区间,包含首尾元素的
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()
}
}
复制代码
可是咱们只处理了 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();
}
}
复制代码
上面咱们已经介绍了函数的定义和组成,下面在继续分析函数的其余方面
假设咱们有以下的函数:
fun <T> joinToString(collection: Collection<T>,
separator: String,
prefix: String,
postfix: String): String
复制代码
而后调用该函数(为参数值指定参数名称):
joinToString(collection, separator = " ", prefix = " ", postfix = ".")
复制代码
咱们能够把 joinToString 定义改为以下形式:
fun <T> joinToString(collection: Collection<T>,
separator: String = ", ",
prefix: String = "",
postfix: String = ""
复制代码
咱们分别为函数的最后三个参数都设置了默认值,咱们能够这样调用该函数:
joinToString(list)
joinToString(list, prefix = "# ")
复制代码
这样也就间接的实现了Java中所谓的重载(overload),代码也更简洁,不用定义多个方法了
看过 《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 的概念是在泛型类或者泛型函数的时候出现:
在 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, ", ", "", "");
复制代码
何谓 扩展函数 ? 扩展函数是在类的外部定义,可是能够像类成员同样调用该函数
扩展函数的定义格式以下图所示:
其中 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()
复制代码
扩展函数本质上是静态函数,如上面的扩展函数 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) 的,由于它本质上是一个静态函数
扩展属性和扩展函数的定义很是类似:
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)
}
复制代码
在 Java 中经过三个点(...)来声明可变参数,如:
public static <T> List<T> listOf(T... items) {
System.out.println(items.getClass()); //数组类型
return Arrays.asList(items);
}
复制代码
Kotlin 和 Java 不同,Kotlin 使用 vararg 关键来定义可变参数:
fun <T> listOf(vararg items: T): List<T> {
println(items.javaClass) //数组类型
return Arrays.asList(*items) // * spread operator
}
复制代码
对于可变参数的函数,调用它的时候能够传递任意个参数
经过上面的两段代码比较咱们发现:Kotlin 须要显示的将可变参数经过 * 展开,而后传递给 asList 函数
这里的 * 就是 展开操做符(spread operator),在 Java 中是没有 展开操做符 的
下面咱们再来看下,展开操做符的方便之处:
val intArr: Array<Int> = arrayOf(1, 2, 3, 4)
Arrays.asList(0, intArr).run {
println("size = $size")
}
//输出结果:
size = 2
复制代码
能够发现,不用展现操做符的话,集合里面只有两个元素
那咱们把它改为使用 展开操做符 的状况:
val intArr: Array<Int> = arrayOf(1, 2, 3, 4)
Arrays.asList(0, *intArr).run {
println("size = $size")
}
//输出结果:
size = 5
复制代码
既然上面用到了 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 <T> List<T> asList(T... a)
复制代码
Java 中的可变参数至关于数组:
public static <T> List<T> asList(T[] a)
复制代码
咱们知道 Java 中的泛型必须是复杂类型,因此这里的泛型 T 也必须是 复杂类型
当咱们传递 int[] 数组的时候,就会出现问题,由于 int 是原始类型,T 是复杂类型
因此 int[] 赋值给 T[] 是非法的,当 一维原始类型的数组 当作给可变参数的时候,编译器会把这个可变参数编译成一个 二维数组
这就是为何会出现上面状况的缘由
咱们再来看下 Arrays.asList 完整源码:
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
private static class ArrayList<E> extends AbstractList<E>
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 <T> void testGeneric(T[] data){
}
int[][] intArray = {{1},{2}};
testGeneric(intArray);
复制代码
讲到这里你可能火烧眉毛的想知道,为何咱们上面的代码使用了展开操做符 Arrays.asList(*intArr) 返回的集合大小就是 5 呢?
val intArr: Array<Int> = 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 了
咱们都知道什么是前缀(prefix),后缀(suffix)。那什么是函数的中缀(infix)调用呢?
使用关键字 infix 修饰的函数都可以 中缀调用
被关键字 infix 修饰的函数只能有一个参数
Kotlin 中的 to 就是一个中缀函数:
public infix fun <A, B> A.to(that: B): Pair<A, B> = 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) {
}
复制代码
局部函数(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 容许在函数内部定义函数,可是不要嵌套太深,不然会致使可读性太差
匿名函数顾名思义就是没有名字的函数:如:
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
复制代码
Kotlin 的 String 字符串和 Java 中的几乎是同样的,Kotlin 在此基础上添加了一系列的扩展函数,方便开发者更好的使用字符串
同时也屏蔽了 Java String 中容易引发开发者困惑的函数,下面咱们从 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<String>
复制代码
若是你想经过正则表达式来分割,你能够调用:
fun CharSequence.split(regex: Regex, limit: Int = 0): List<String>
复制代码
经过不一样的参数类型来减小开发者在使用过程当中出错的概率
假如咱们须要对以下字符串,分割成 路径、文件名和后缀: “/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 内的特殊字符
关于类的定义、对象的建立、接口的原理分析,能够参考以前的文章:《从Java角度深刻理解Kotlin》
关于Lambda表达式及原理分析,能够参考以前的文章:《从Java角度深刻理解Kotlin》
关于 Kotlin 原始数据类型,能够查看以前的文章《从Java角度深刻理解Kotlin》
里面有详细的介绍,下面咱们就来介绍下 Kotlin 中关于 可空类型
可空类型 是 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 都是能够为空的
安全调用操做符(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了
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
复制代码
前面咱们讲到了 Kotlin 的智能强转(smart casts),即经过 is 关键字来判断是否属于某个类型,而后编译器自动帮咱们作强转操做
若是咱们不想判断类型,直接强转呢?在 Java 中可能会出现 ClassCastException 异常
在 Kotlin 中咱们能够经过 as? 操做符来避免相似这样的异常
as? 若是不能强转返回 null,反之返回强转以后的类型,以下图所示:
咱们知道 Kotlin 中类型有可为空和不可为空两种
好比有一个函数的参数是不可空类型的,而后咱们把一个可空的变量当作参数传递给该函数
此时Kotlin编译器确定会报错的,这个时候可使用非空断言。非空断言意思就是向编译器保证我这个变量确定不会为空的
以下面伪代码:
var str:String?
// 参数不可为空
fun test(s: String) {
//...
}
// 非空断言
test(str!!)
复制代码
注意:对于非空断言要谨慎使用,除非这个变量在实际状况真的不会为null,不然不要使用非空断言。虽然使用了非空断言编译器不报错了,可是若是使用非空断言的变量是空依然会出现空指针异常
非空断言的原理以下图所示:
延迟初始化属性(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 加上非空断言了
在前面的章节咱们已经介绍了扩展函数,那什么是 可空类型的扩展函数?
可空类型的扩展函数 就是在 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() 不会空指针吗?
根据上面对扩展函数的讲解,扩展函数编译后会变成静态调用
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
须要注意的是,数字字面量当作函数参数或进行算术操做时,Kotlin会自动进行相应类型的转换
fun foo(l: Long) = println(l)
val y = 0
foo(0) // 数字字面量做为参数
foo(y) // 编译报错
val b: Byte = 1
val l = b + 1L // b 自动转成 long 类型
复制代码
Any 类型 和 Java 中的 Object 相似,是Kotlin中全部类的父类
包括原始类型的包装类:Int、Float 等
Any 在编译后就是 Java 的 Object
Any 类也有 toString() , equals() , and hashCode() 函数
若是想要调用 wait 或 notify,须要把 Any 强转成 Object
Unit 类型和 Java 中的 void 是一个意思
下面介绍它们在使用过程的几个不一样点:
例以下面的函数能够省略 Unit:
fun f(): Unit { ... }
fun f() { ... } //省略 Unit
复制代码
可是在 Java 中则不能省略 void 关键字
例以下面的例子:
interface Processor<T> {
fun process(): T
}
// Unit 做为 Type Arguments
class NoResultProcessor : Processor<Unit> {
override fun process() { // 省略 Unit
// do stuff
}
}
复制代码
若是在 Java 中,则须要使用 Void 类:
class NoResultProcessor implements Processor<Void> {
@Override
public Void process() {
return null; //须要显式的 return null
}
}
复制代码
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) // 编译经过
}
复制代码
能够参考:《从Java角度深刻理解Kotlin》 里关于集合和数组的部分
操做符重载内容有点多,单独写了一篇文章:《Kotlin操做符重载详解》 ,学完这篇文章相信你对 Kotlin 操做符 有全新的理解和掌握
高阶函数的定义及原理分析,能够参考以前的文章:《从Java角度深刻理解Kotlin》
《从Java角度深刻理解Kotlin》中关于高阶函数的部分,咱们说到为了优化高阶函数须要使用 inline 关键字来修饰
可是不是全部的 高阶函数 都能声明为内联(inline)函数
若是高阶函数的 lambda 参数被局部变量保存起来,那么这个高阶函数就不能被 inline 修饰
由于若是Kotlin支持这么作,那么这个局部变量就必须包含lambda代码,可是Java/Kotlin底层并无能保存一段代码的类型
例如高阶函数 Sequence<T>.map 就不能使用 inline 修饰,下面分析其缘由:
public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
return TransformingSequence(this, transform)
}
internal class TransformingSequence<T, R>
constructor(
private val sequence: Sequence<T>,
private val transformer: (T) -> R) : Sequence<R> {
// 省略实现代码
}
复制代码
从中能够发现,Sequence<T>.map 的参数(lambda),被传递给了 TransformingSequence 的构造函数
而后被 TransformingSequence 经过属性的形式将该lambda 保存起来了
因此一个高阶函数可以被内联,要么参数lambda被直接使用,要么直接传递给另外一个内联函数,没有将lambda保存到局部变量或者属性中
另外一方面,若是一个高阶函数接收多个lambda,并且有的lambda参数包含的代码量比较大,能够指定该lambda不内联:
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
// ...
}
复制代码
泛型的类型参数: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<String> = mutableListOf()
//或者
val list = mutableListOf<String>()
复制代码
因此,在Kotlin中要么显式地指定 Type Argument;要么编译器可以推导出 Type Argument 的类型
可是,Java是容许不指定 Type Argument 的,这主要是由于 泛型是 JDK1.5 才出来的新特性
为了兼容之前的代码,因此 Java 容许在使用泛型的时候不指定 Type Argument,这就是所谓的 raw type
例如上面用到的 listOf 就是一个泛型函数
除此以外,泛型还能够用在 扩展函数 上,如:
public fun <T> List<T>.slice(indices: IntRange): List<T>
复制代码
该函数的 type parameter 用在 receiver type 和 return type 中,以下图所示:
一样地,调用泛型扩展函数要么显式指定 type argument,要么编译可以推导出类型:
val letters = ('a'..'z').toList()
letters.slice<Char>(0..2)) // 显式指定 type argument
letters.slice(10..13) // 编译器类型推导出 Char
复制代码
泛型除了能够用在扩展函数上,还能够用到扩展属性上:
val <T> List<T>.penultimate: T
get() = this[size - 2]
println(listOf(1, 2, 3, 4).penultimate)
复制代码
Kotlin 泛型类和 Java 的泛型类差很少,都是将泛型放到 <> 中
// 声明类的时候声明 type parameter
interface List<T> {
//将 type parameter 用于返回类型
operator fun get(index: Int): T {
// ...
}
}
复制代码
继承或者实现这个接口的时候须要提供 type argument
这个 type argument 能够是具体的类,也能够是另外一个泛型:
// type argument 就是 String 类
class StringList: List<String> {
override fun get(index: Int): String = ...
}
//使用另外一个泛型T当作 type argument
class ArrayList<T> : List<T> {
override fun get(index: Int): T = ...
}
复制代码
须要注意的是 ArrayList<T> 这里的 T 和 List<T> 的 T 是两个东西,只是名字同样而已
ArrayList 定义了一个全新的 Type parameter 名为 T,而后将其当作父类List的 Type argument
Type parameter约束 可以限制 Type argument 的类型
例以下面的一段 Java 代码:
<T extends Number> T sum(List<T> list)
复制代码
调用 sum 函数的时候,List 集合里的元素只能是 Number 或 Number 的子类型
也就是说 Type argument 只能是 Number 或 Number的子类型
把这里的 Number 称之为 上界(upper bound)
上面的代码用 Kotlin 来表示就是这样的:
fun <T : Number> List<T>.sum(): T
复制代码
上面只是限制 T 是 Number 或者 Number 的子类,若是还想加上其余限制呢?
例以下面的例子,为 Type parameter 加上了两个类型约束
T 必须同时知足两个条件:
fun <T> 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")
复制代码
泛型的不变性、协变性、逆变性及泛型具体化及原理分析,能够参考以前的文章:《从Java角度深刻理解Kotlin》
Kotlin 有三种结构化跳转表达式:
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
复制代码
continue 、continue label 用法和 break、break 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
复制代码
例如 Kotlin 中的 filter 函数,接收一个lambda 表达式,该lambda返回一个 boolean 值
listOf<Int>().filter { it > 0 }
复制代码
在该例子中,lambda体内只有一行表达式,且该表达式就是 boolean 类型,因此不须要显式的 return
可是若是须要在 lambda body 里进行复杂的判断,而后根据条件来 return 怎么办?
固然上面的代码能够经过 when 来简化,这个例子代码只是来演示 lambda return 值的状况
定义注解的方式很是简单:
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
复制代码
元注解就是描述注解的注解。 元注解主要用于告诉编译器如何处理该注解
@Target(AnnotationTarget.PROPERTY)
annotation class JsonName
复制代码
Target 注解就是元注解,告诉编译器 JsonName 注解只用到类的属性上
AnnotationTarget 是一个枚举类,经常使用的枚举对象有:
@Retention 元注解用于告诉编译器,目标注解保留到哪一个阶段,例如是编译仍是运行时阶段
Java 默认的保留到 class 字节码中,运行时不可见
Kotlin 默认是保留到 runtime,运行时可见
@Retention 经过 AnnotationRetention 来制定注解保留在哪一个阶段:
容许在一个元素上使用屡次该注解,例如:
@JsonName
@JsonName
var firstName: String? = null
复制代码
代表该注解是生成的API文档的一部分,这样在API文档中既能够看到该注解了
Kotlin中的 @Deprecated 比 Java 中的更强大,Kotlin 还能够指定用哪一个 API 来替换废弃的API
@Deprecated("use greeting", ReplaceWith("greeting()",
"annotation.Person.greeting()"))
fun sayHello(){
}
复制代码
还可使用 修复 (Show Intention Actions) 快捷键将废弃的 API 用替换成建议使用的 API:
@Suppress 用来去除代码中的一些 warning
对于编辑器提示的 warning,咱们要尽量的去修复,由于有多是代码的不规范致使的
可是对于一些可有可无的或者暂时没法处理的 warning,能够经过 @Suppress 来去掉 warning
@Suppress("UNCHECKED_CAST")
fun test(source: Any) {
if (source is List<*>) {
val list = source as List<String>
val str: String = list[0]
}
}
复制代码
有些同窗可能会问,@Suppress 注解的参数,具体有哪些呢?
这些参数统必定义在 Errors.java 中
编译器不会为被 @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
@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();
复制代码
@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();
复制代码
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> = Book::class
val kclazz2: KClass<Book> = book.javaClass.kotlin
// Java Class
val jclazz: Class<Book> = Book::class.java
val jclazz2: Class<Book> = book.javaClass
复制代码
上面的获取 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<T,V> 是一个泛型类,该泛型接收两个参数:
介绍完 KProperty 咱们能够写一个函数用来输出任意类的 属性名称 和 属性值
fun <T> printProperty(instance: T, prop: KProperty1<T, *>) {
println("${prop.name} = ${prop.get(instance)}")
}
复制代码
还能够利用 KProperty 来为 var 修饰的属性设置值:
fun <T> changeProperty(instance: T, prop: KMutableProperty1<T, String>) {
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
下面是个人公众号,干货文章不错过,有须要的能够关注下,有任何问题能够联系我: