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 函数定义
需要注意的是 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)
1.2.2 Kotlin 中表达式和语句
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 异常
但是我们只处理了 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的了解与使用
文章图片

因为 《Kotlin In Action》 的 3.2.1 章节是讲调用函数的时候为参数命名,所以使用了 Arguments
此外,除了 Parameter 和 Argument ,还有 Type Parameter 和 Type Argument
因为下面还要用到这两个的概念,所以这里我们介绍下 Type Parameter 和 Type Argument
Type Parameter 和 Type Argument 的概念是在泛型类或者泛型函数的时候出现:
kotlin的了解与使用
文章图片

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 扩展函数 何谓 扩展函数 ? 扩展函数是在类的外部定义,但是可以像类成员一样调用该函数
扩展函数的定义格式如下图所示:
kotlin的了解与使用
文章图片

其中 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 也必须是 复杂类型
【kotlin的了解与使用】当我们传递 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 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的了解与使用
文章图片

kotlin的了解与使用
文章图片

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 本身,如下图所示:
kotlin的了解与使用
文章图片

kotlin的了解与使用
文章图片

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,反之返回强转之后的类型,如下图所示:
kotlin的了解与使用
文章图片

kotlin的了解与使用
文章图片

safe-cast 操作符
非空断言:!! 我们知道 Kotlin 中类型有可为空和不可为空两种
比如有一个函数的参数是不可空类型的,然后我们把一个可空的变量当做参数传递给该函数
此时Kotlin编译器肯定会报错的,这个时候可以使用非空断言。非空断言意思就是向编译器保证我这个变量肯定不会为空的
如下面伪代码:
var str:String?

// 参数不可为空
fun test(s: String) {
//...
}

// 非空断言
test(str!!)
注意:对于非空断言要谨慎使用,除非这个变量在实际情况真的不会为null,否则不要使用非空断言。虽然使用了非空断言编译器不报错了,但是如果使用非空断言的变量是空依然会出现空指针异常
非空断言的原理如下图所示:
kotlin的了解与使用
文章图片

kotlin的了解与使用
文章图片

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
需要注意的是,数字字面量当做函数参数或进行算术操作时,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类型 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) // 编译通过
}

    推荐阅读