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 = 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 还是有些区别的

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 <T> joinToString(collection: Collection<T>,

separator: String,

prefix: String,

postfix: String): String

然后调用该函数(为参数值指定参数名称):

joinToString(collection, separator = " ", prefix = " ", postfix = ".")

2.1.2 为函数参数指定默认值

我们可以把 joinToString 定义改成如下形式:

fun <T> joinToString(collection: Collection<T>,

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 <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

}

对于可变参数的函数,调用它的时候可以传递任意个参数

2.6.2 展开操作符

通过上面的两段代码比较我们发现: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

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 <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);

2.6.4 Kotlin 展开操作符的原理分析

讲到这里你可能迫不及待的想知道,为什么我们上面的代码使用了展开操作符 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 了

2.7 中缀调用

我们都知道什么是前缀(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) {

}

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<String>

如果你想通过正则表达式来分割,你可以调用:

fun CharSequence.split(regex: Regex, limit: Int = 0): List<String>

通过不同的参数类型来减少开发者在使用过程中出错的几率

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 == ((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<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 类是一个 标记类

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) // 编译通过

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值