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 函数定义
需要注意的是 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 = https://www.it610.com/article/8933.234
// 自增语句
aValue++;
// 方法调用语句
System.out.println("Hello World!");
// 创建对象语句
Bicycle myBike = new Bicycle();
除此之外,还有 声明语句(declaration statements),如:
// declaration statement
double aValue = https://www.it610.com/article/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)
Kotlin 和 Java 中对表达式和语句的定义都是类似的
但是对于有些关键字是语句还是表达式和 Java 还是有些区别的
1. if/when
如上所述,在 Java 中所有的 控制流 都是语句
在 Kotlin 的控制流中除了 循环(for/while/do..while) ,其他的都是表达式
既然是表达式,那么它就是表示某种类型的数据,可以把它赋值给变量
val max = if (a > b) a else b
2. 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
1) when 的基本语法
when(parameter){
branch1 -> logic
branch2 -> logic
}
when 括号里是参数,参数是可选的。箭头(->) 左边是条件分支,右边是对应的逻辑体
when 不需要向 switch 那样需要加上 break 语句,符合条件自动具有 break 功能
如果逻辑体代码比较多,可以放到花括号({})里:
when(parameter){
branch1 -> {
//...
}
branch1 -> {
//...
}
}
如果要组合多个分支,可以使用逗号(,)分隔分支:
when(parameter){
branch1,branch1 -> {
//...
}
}
2) 枚举类对象作为 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 分支
3) 任意对象作为 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")
}
4) 无参数的 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 类型
5) 智能类型转换(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 异常
如果是在 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
separator: String,
prefix: String,
postfix: String): String
然后调用该函数(为参数值指定参数名称):
joinToString(collection, separator = " ", prefix = " ", postfix = ".")
2.1.2 为函数参数指定默认值
我们可以把 joinToString 定义改成如下形式:
fun
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 的概念是在泛型类或者泛型函数的时候出现:
文章图片
type-parameters VS type arguments
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
System.out.println(items.getClass()); //数组类型
return Arrays.asList(items);
}
Kotlin 和 Java 不一样,Kotlin 使用 vararg 关键来定义可变参数:
fun
println(items.javaClass) //数组类型
return Arrays.asList(*items) // * spread operator
}
对于可变参数的函数,调用它的时候可以传递任意个参数
2.6.2 展开操作符
通过上面的两段代码比较我们发现:Kotlin 需要显示的将可变参数通过 * 展开,然后传递给 asList 函数
这里的 * 就是 展开操作符(spread operator),在 Java 中是没有 展开操作符 的
下面我们再来看下,展开操作符的方便之处:
val intArr: Array
Arrays.asList(0, intArr).run {
println("size = $size")
}
//输出结果:
size = 2
可以发现,不用展示操作符的话,集合里面只有两个元素
那我们把它改成使用 展开操作符 的情况:
val intArr: Array
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
Java 中的可变参数相当于数组:
public static
我们知道 Java 中的泛型必须是复杂类型,所以这里的泛型 T 也必须是 复杂类型
【kotlin的了解与使用】当我们传递 int[] 数组的时候,就会出现问题,因为 int 是原始类型,T 是复杂类型
所以 int[] 赋值给 T[] 是非法的,当 一维原始类型的数组 当做给可变参数的时候,编译器会把这个可变参数编译成一个 二维数组
这就是为什么会出现上面情况的原因
我们再来看下 Arrays.asList 完整源码:
public static
return new ArrayList<>(a);
}
private static class ArrayList
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
}
int[][] intArray = {{1},{2}};
testGeneric(intArray);
2.6.4 Kotlin 展开操作符的原理分析
讲到这里你可能迫不及待的想知道,为什么我们上面的代码使用了展开操作符 Arrays.asList(*intArr) 返回的集合大小就是 5 呢?
val intArr: Array
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 funA.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 内的特殊字符
可空类型 可空类型 是 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 那就正常调用即可
如下图所示:
文章图片
文章图片
kotlin-safe-call
所以上面的代码不会出现空指针异常
安全调用操作符 ?.,不仅可以调用函数,还可以调用属性。
需要注意的是,使用了 ?. 需要注意其返回值类型:
val length = str?.length
if(length == 0){
//do something
}
这个时候如果 str == null 的话,那么 length 就是 null,它永远不等于0了
Elvis操作符: ?: Elvis操作符 用来为null提供默认值的,例如:
fun foo(s: String?) {
val t: String = s ?: ""
}
如果 s == null 则返回 "",否则返回 s 本身,如下图所示:
文章图片
文章图片
elvis操作符
上面介绍 可空性 时候的例子可以通过 Elvis操作符改造成更简洁:
fun strLenSafe(s: String?) = if (s != null) s.length else 0
//改成如下形式:
fun strLenSafe(s: String?) = s.length ?: 0
安全强转操作符:as? 前面我们讲到了 Kotlin 的智能强转(smart casts),即通过 is 关键字来判断是否属于某个类型,然后编译器自动帮我们做强转操作
如果我们不想判断类型,直接强转呢?在 Java 中可能会出现 ClassCastException 异常
在 Kotlin 中我们可以通过 as? 操作符来避免类似这样的异常
as? 如果不能强转返回 null,反之返回强转之后的类型,如下图所示:
文章图片
文章图片
safe-cast 操作符
非空断言:!! 我们知道 Kotlin 中类型有可为空和不可为空两种
比如有一个函数的参数是不可空类型的,然后我们把一个可空的变量当做参数传递给该函数
此时Kotlin编译器肯定会报错的,这个时候可以使用非空断言。非空断言意思就是向编译器保证我这个变量肯定不会为空的
如下面伪代码:
var str:String?
// 参数不可为空
fun test(s: String) {
//...
}
// 非空断言
test(str!!)
注意:对于非空断言要谨慎使用,除非这个变量在实际情况真的不会为null,否则不要使用非空断言。虽然使用了非空断言编译器不报错了,但是如果使用非空断言的变量是空依然会出现空指针异常非空断言的原理如下图所示:
文章图片
文章图片
NotNullAssert.png
延迟初始化属性 延迟初始化属性(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 =https://www.it610.com/article/= ((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
fun foo(l: Long) = println(l)
val y = 0
foo(0) // 数字字面量作为参数
foo(y) // 编译报错
val b: Byte = 1
val l = b + 1L // b 自动转成 long 类型
Any类型 Any 类型 和 Java 中的 Object 类似,是Kotlin中所有类的父类
包括原始类型的包装类:Int、Float 等
Any 在编译后就是 Java 的 Object
Any 类也有 toString() , equals() , and hashCode() 函数
如果想要调用 wait 或 notify,需要把 Any 强转成 Object
Unit 类型 Unit 类型和 Java 中的 void 是一个意思
下面介绍它们在使用过程的几个不同点:
1). 函数没有返回值,Unit可以省略
例如下面的函数可以省略 Unit:
fun f(): Unit { ... }
fun f() { ... } //省略 Unit
但是在 Java 中则不能省略 void 关键字
2) 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
}
}
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) // 编译通过
}
推荐阅读
- 热闹中的孤独
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 放屁有这三个特征的,请注意啦!这说明你的身体毒素太多
- 一个人的旅行,三亚
- 布丽吉特,人生绝对的赢家
- 慢慢的美丽
- 尽力
- 一个小故事,我的思考。
- 家乡的那条小河
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量