02-Kotlin

Kotlin

JetBrains 公司开发设计的:

  • Java -> .class -> JAVA 虚拟机 -> 二进制
  • Kotlin -> .class -> JAVA 虚拟机 -> 二进制

开发工具:

  • IntelliJ IDEA
  • 在线
  • Android Studio: 在一个 Android 项目中编写一个 Kotlin 的 main() 函数即可独立运行 Kotlin 代码

优势

  • 语法简洁, 代码量少了
  • 语法高级, 开发效率高
  • 语言安全
  • 支持使用 Java 第三方的开源库

变量

变量声明

Kotlin 具有出色的类型推导机制, 因此仅有两种声明变量的关键字:

  • val: value, 声明一个不可变的变量, 在初始赋值之后就再也不能重新赋值(对应 Java 中的 final 变量)
  • var: variable, 声明一个可变的变量

显示类型声明

当变量需要延迟赋值时, 通过 : Type 的形式指定变量的类型

  • Kotlin 完全抛弃了 Java 的基本数据类型, 使用对象数据类型
  • Int, Long, Short, Float, Double, Boolean, Char, Byte
  • Unit 类型表示函数返回无意义的值, 可以省略
val a: Int = 10

定义区间

  • val range = 0..10: 定义 [0, 10] 的区间
  • val range = 0 until 10: 定义 [0, 10) 的区间
  • val range = 10 downTo 0: 定义 [10, 0] 的区间

使用 in 关键字检测目标是否在区间/集合中:

  • if (-1 !in 0..list.lastIndex) { ... }
  • if (list.size !in list.indices) { ... }

模板字符串

  • $value 在字符串中调用变量
  • ${s1.replace("is", "was")} 模板表达式

类型检测

使用 is 关键字(类比 Java 的 instanceof)

1
2
3
4
5
if (obj is String) {
return obj.length
}
// 取反
if (obj !is String) return null

逻辑

if

Kotlin 中的 if 语句存在返回值, 值为条件中最后一行代码的返回值

1
2
3
4
5
6
7
8
fun JCMax2(a: Int, b: Int): Int {
val ret: Int = if ( a > b) {
a
} else {
b
}
return ret
}

when

类似于 switch 语句, 但是功能更加强大:

  • 存在返回值, 返回值为分支的返回值
  • 匹配值支持任意类型的参数
1
2
3
4
5
6
7
8
fun getScore2(name: String) =
when (name) {
"Tom" -> 86
"Jim" -> 77
"Jack" -> 95
"Lily" -> 100
else -> 0
}
  • 支持类型匹配: is (类似 Java 的 instanceof)
1
2
3
4
5
6
7
fun checkNumber(num: Number) {
when (num) {
is Int -> println("number is Int")
is Double -> println("number is Double")
else -> println("number not support")
}
}
  • 支持无参数的更复杂的匹配
1
2
3
4
5
6
7
8
fun getScore3(name: String) =
when {
name.startsWith("Tom") -> 86
name == "Jim" -> 77
name == "Jack" -> 95
name == "Lily" -> 100
else -> 0
}

for

主要使用 for-in 循环:

  • 遍历区间

  • 使用 step 设置遍历步长

  • 遍历区间

1
2
3
4
5
6
fun main() {
val range1 = 0..10
for ( i in range1) {
println(i)
}
}
  • 使用 step 设置遍历步长
1
2
3
4
val range2 = 0 until 10
for ( i in range2 step 2) {
println(i)
}

while

与 Java 的用法一致

1
2
3
4
5
6
7
8
fun main() {
val items = listOf("apple", "banana", "kiwifruit")
var index = 0
while (index < items.size) {
println("item at $index is ${items[index]}")
index ++
}
}

repeat

允许传入一个 n 值, 会把 Lambda 表达式中的内容执行 n 遍:

1
2
3
4
5
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.apple_pic))
fruitList.add(Fruit("Banana", R.drawable.banana_pic))
// 重复加载数据
}

函数

  • 支持默认参数
  • vararg 表示接收任意多个参数: fun max(vararg nums: Int): Int {...}
  • 泛型: fun <T: Comparable<T>> max(vararg nums: T): T {...} 指定泛型 T 是可比较类型的子类型, 即可对求最大值指定泛型了

  • 实例化时取消了 new 关键字
  • Kotlin 中任何一个非抽象类都是不可以被继承的; 如果允许继承, 则在类之前加上 open 修饰
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
var name = ""
var age = 0
fun eat() {
println(name + " is eating. He is " + age + " years old.")
}
}
fun main() {
var p = Person()

p.name = "Coming"
p.age = 23
p.eat()

println(p)
}

继承

使用 : 表示继承, 继承时父类是带有 ()

  • 为什么要有 (): 子类要先调用父类的构造函数, 但是主构造函数的实现默认没有函数体, 因此使用 () 实现父类构造函数的预调用( 当然有参数的构造函数中括号中是要带参数的)
1
2
3
4
5
6
class Student: Person() {
var grade = "1"
fun test() {
println(name + " 正在 " + grade + " 班考试...")
}
}

构造函数

主构造函数: 没有函数体, 直接定义在类后面; 如果想在构造函数中执行一些逻辑, 使用 init 块实现

1
2
3
4
5
6
7
8
9
10
class Person2(val name: String, var age: Int) {
init {
if (name == "Coming" ) {
age += 1
}
}
fun run() {
println(name + "is running..." + "Age = " + age)
}
}

次构造函数, 拥有函数体,

  • 当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)
  • 任何一个类只有一个主构造函数, 但是可以有多个次构造函数
  • 次构造函数也能够用于实例化一个类
  • 没有主构造函数时继承时就不必为父类加括号了, 而是在次构造中的函数体内通过 super 关键字实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
open class Person3(val name: String, val age: Int) {
fun showPersonInfo() {
println("name = " + name + " age = " + age)
}
}

class Student3(val grade: String, val score: Int, name: String, age: Int): Person3(name, age) {
constructor(name: String, age: Int): this("", 0, name, age) {}
constructor() : this("", 0) {} // 没有参数时, 先定义默认值后调用第一个次构造函数
fun showStudentInfo() {
println("name = " + name + " age = " + age + " grade = " + grade + " score = " + score)
}
}

class Student4 : Person3 {
constructor(name: String, age: Int) : super(name, age) { }
}

fun main() {
val student1 = Student3()
val student2 = Student3("Jack", 19)
val student3 = Student3("a123", 5, "Jack", 19)
student1.showStudentInfo()
student2.showStudentInfo()
student3.showStudentInfo()

val student4 = Student4("name", 10)
student4.showPersonInfo()
}

接口

任何一个类只能继承一个父类, 但是可以实现任意多个接口, 可以在接口中定义一系列抽象行为, 然后由具体的类去实现

  • 实现接口中的方法时需要使用 override 关键字
  • kotlin 允许对接口中定义的函数进行默认实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
open class Person5(val name: String) {
constructor(): this("UNKNOWN")
fun showPersonInfo() {
println("Person name = " + name)
}
}

interface Study {
fun readBooks()
fun doHomework() {
println("Default implement")
}
}

class Student5(name: String, val age: Int): Person5(name), Study {
constructor(age: Int): this("UNKNOWN", age) { }
constructor(name: String): this(name, 0) { }
constructor(): this(0)
override fun readBooks() {
println(name + "(age = " + age + ") is reading")
}
override fun doHomework() {
println(name + "(age = " + age + ") is homeworking")
}
}

fun main() {
val s1 = Student5("Coming", 23)
val s2 = Student5("CJC")
val s3 = Student5(24)
val s4 = Student5()

println(s1.readBooks())
println(s2.readBooks())
println(s3.doHomework())
println(s4.doHomework())
}

可见性修饰符

  • private: 类内可见
  • public: 所有类可见, 默认修饰符
  • protected: 当前类和子类可见
  • internal: 对同一模块中的类可见, 比如我们开发了一个模块给别人使用,但是有一些函数只允许在模块内部调用,不想暴露给外部,就可以将这些函数声明成 internal

数据类

数据类用于将服务器端或数据库中的数据映射到内存中,为编程逻辑提供数据模型的支持, Kotlin 中使用 data 关键字进行修饰

  • MVC、MVP、MVVM 之类的架构模式中的 M 指的就是数据类
  • 数据类通常需要重写 equals()、hashCode()、toString() 等方法
    • equals(): 判断两个数据类是否相等
    • hashCode(): equals() 的配套方法
    • toString(): 用于提供更清晰的输入日志(否则一个数据类默认打印出来的就是一行内存地址)
1
2
3
4
// 实现 Cellphone 数据类十分简单
// 当一个类中没有任何代码时, 可以将尾部的大括号省略
data class Cellphone(val brand: String, val price: Double)

单例类

单例模式: 最常用、最基础的设计模式之一,用于避免创建重复的对象

Java 中的单例思想:

  • 首先为了禁止外部创建 Singleton 的实例,需要用 private 关键字将 Singleton 的构造函数私有化
  • 然后给外部提供了一个 getInstance() 静态方法用于获取 Singleton 的实例
  • 在 getInstance() 方法中,如果当前缓存的 Singleton 实例为 null,就创建一个新的实例
  • 否则直接返回缓存的实例即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Singleton
{
private static Singleton instance;
private Singleton() {}
public synchronized static Singleton getInstance()
{
if(instance == null)
{
instance = new Singleton();
}
return instance;
}
public void singletonTest()
{
System.out.println("singletonTest is called.");
}
}

Singleton singleton = Singleton.getInstance();
singleton.singletonTest();

Kotlin 中的单例类依旧隐藏了固定的重复的逻辑, 将 class 改为 object 即可创建单例类

1
2
3
4
5
6
7
8
9
10
11
object Singoton {
val name: String = "Coming"
fun SingotonTest() {
println("called")
}
}

fun main() {
Singoton.SingotonTest() // Kotlin 会先创建 Singoton 的实例然后在调用(并且保证全局只有一个实例)
println(Singoton.name)
}

泛型类

1
2
3
4
5
class MyClass<T> {
fun func(param: T): T {
return param
}
}

泛型函数

1
2
3
4
fun <T> method(param: T): T {
return param
}
method<Int>(123)

Lambda 编程

Lambda 就是一小段可以作为参数传递的代码

语法结构: {参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}

集合

  • listOf(): 初始化不可变集合, 只能读取, 不能添加修改或删除
  • mutableListOf(): 初始化可变集合
  • 同理 setOf()mutableSetOf() 只是 set 中不可以存放重复的元素, 对于重复的元素只会保留一份
  • 方法: .filter, map, maxBy, any, all
1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
val fruitList = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
for (fruit in fruitList) {
println(fruit)
}

val friendList = mutableListOf("Coming", "ZH", "ZCY")
friendList.add("GTY")
for (friend in friendList) {
println(friend)
}
}

遍历索引值

通过集合的属性 indices

1
2
3
4
val items = listOf("apple", "banana", "kiwifruit")
for (index in items.indices) {
println("item at $index is ${items[index]}")
}

Map

  • 支持 Java 中的 put 与 get, 但是不推荐
  • 推荐使用类似于数组下标的语法结构去赋值与获取
  • 也支持 mapOf()mutableMapOf()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
val map1 = HashMap<String, Int>()
map1.put("Coming", 23)

println("Coming's age = " + map1.get("Coming"))

val map2 = HashMap<String, Int>()
map2["Coming"] = 23
println("Coming's age = " + map2["Coming"])

val fruitMap = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5)
for ((fruit, id) in fruitMap) {
println(fruit + " : " + id)
}
}

Java 函数式 API 的使用

Kotlin 中调用 Java 方法时也可以使用函数式 API, 如果我们在 Kotlin 代码中调用了一个 Java 方法,并且该方法接收一个 Java 单抽象方法接口参数,就可以使用函数式API

  • 接口中只有一个待实现方法
  • Java 函数式 API 的使用都限定于从 Kotlin 中调用 Java 方法,并且单抽象方法接口也必须是用 Java 语言定义的

在 Java 中使用函数式 API: 匿名类, 创建了一个 Runnable 接口的匿名类实例,并将它传给了 Thread 类的构造方法

1
2
3
4
5
6
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread is running");
}
}).start();

使用 Kotlin 直观的改写: object 关键字是用于定义单例类, 在这里也直接用于创建了匿名类的实例(因为单例类只有一个实例, 所以类名直接引用是咧)

1
2
3
4
5
6
7
fun main() {
Thread(object : Runnable {
override fun run() {
println("RUNNING")
}
})
}
  • 简化实现代码:因为只有一个待实现的方法, 所以没必要显示的重写 run 方法
Thread(Runnable {
println("RUNING2")
}).start()
  • 继续简化实现代码: 如果一个 Java 方法的参数列表中有且仅有一个 Java 单抽象方法接口参数,我们还可以将接口名进行省略
Thread({
println("RUNING3")
}).start()
  • 最终简化: 当 Lambda 表达式是方法的最后一个参数时,可以将 Lambda 表达式移到方法括号的外面
  • 同时,如果 Lambda 表达式还是方法的唯一一个参数,还可以将方法的括号省略
Thread { println("RUNNING4") }.start()

空指针检查

Kotlin 默认所有的参数和变量都不可为空, 但可以在类型名后面加上 ? 来定义可空类型系统:

  • Int 表示不可为空的整型, Int? 表示可为空的整型

但使用可空类型系统时就需要对参数进行判空处理, 常用的符号有 ?. !!. ?:

  • a?.doSomething(): a 为 null 时不再执行, 直接返回 null, a 不为 null 时正常执行
  • a ?: b: 左右两边都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果
  • !!.: 函数内部进行了非空判断, 但是函数返回后 Kotlin 不能知道已经做了非空判断, 因此使用 !!. 进行非空断言, 表明这个对象绝对不为空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun getTextLength(text: String?): Int {
println(text?.length)
if (text != null) {
return text.length
}
return 0
}

// text 不为 null 则 text.length 返回值不为空, ?: 操作符返回左边的结果
// text 为 null 则 text.length 返回值为空, ?: 操作符返回右边的结果
fun JCGetTextLength(text: String?) = text?.length ?: 0

fun main() {
getTextLength("WWW") // text?.length = 3
getTextLength(null) // text?.length = null
println(JCGetTextLength(null))
println(JCGetTextLength("Hello World"))
}

let 函数

这个函数提供了函数式 API 的编程接口,并将原始调用对象作为参数传递到 Lambda 表达式中, 配合进行非空判断十分方便:

  • obj.let { obj_ -> ... }: obj_obj 是相同的对象, 只不过为了不重名
  • 如下例, 如果 study 为 null 则不会执行 let 函数; 如果 study 不为 null 则执行 let 函数并实现了非空判断
  • if 不同的是, let 函数是可以处理全局变量的判空问题的: if 中如果存在全局变量, 其值随时都有可能被其他线程所修改,即使做了判空处理,仍然无法保证 if 语句中的 study 变量没有空指针风险
1
2
3
4
5
6
7
8
9
10
11
12
13
fun doStudy(study: Study?) {
study?.let { stu ->
stu.readBooks()
stu.doHomework()
}
}
// lambda 只有一个参数时可以简化改参数为 it
fun doStudy(study: Study?) {
study?.let {
it.readBooks()
it.doHomework()
}
}

常用工具类

LocalDateTime

格式化输出

  • 日期时间格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 输出日期时间格式
* dateSplit = "-", timeSplit = ":", minTime = "min": 2022-10-31 16:52
* dateSplit = "chinese", timeSplit = "chinese", minTime = "min": 2022年10月31日 16时52分
*/
fun LocalDateTime.dateTimeFormatter(dateSplit: String = "-", timeSplit: String = ":", minTime: String = "min"): String {
val datePattern: String = if (dateSplit == "chinese") "yyyy年MM月dd日" else "yyyy${dateSplit}MM${dateSplit}dd"
val timePattern: String = if (timeSplit == "chinese") "HH时mm分ss秒" else "HH${timeSplit}mm${timeSplit}ss"
val datetimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern(datePattern + " " + timePattern)
var cutIndex: Int = 0
when (minTime) {
"hour" -> cutIndex = 6
"min" -> cutIndex = 3
else -> cutIndex = 0
}
val dateTimeString = datetimeFormatter.format(this).toString()
return dateTimeString.substring(0, dateTimeString.length - cutIndex)
}

  • 日期格式
fun LocalDateTime.dateFormatter(dateSplit: String = "-"): String {
return this.dateTimeFormatter(dateSplit = dateSplit).split(" ").first()
}
  • 时间格式
fun LocalDateTime.timeFormatter(timeSplit: String = ":", minTime: String = "min"): String {
return this.dateTimeFormatter(timeSplit = timeSplit, minTime = minTime).split(" ").last()
}

类型转换

  • LocalDateTime 2 Long
fun LocalDateTime.toLong(): Long {
return this.toInstant(ZoneOffset.of("+8")).toEpochMilli()
}
  • Long 2 LocalDateTime: 损失了毫秒的精度
fun Long.toLocalDateTime(): LocalDateTime {
return LocalDateTime.ofEpochSecond(this/1000, 0, ZoneOffset.ofHours(8))
}

计算距离

  • 两个 LocalDateTime 之间的秒级距离
fun LocalDateTime.distence(targetLocalDateTime: LocalDateTime): Long {
return abs(this.toLong() - targetLocalDateTime.toLong()) / 1000
}

Float

格式化输出

保留小数点后 n 位的同时, 如果小数点后 n 位为 0 则转为整数

1
2
3
4
5
6
7
8
9
fun Float.toTypedString(keepLength: Int): String {
val checkValue = 10.0.pow(keepLength).toInt()
// 后 keepLength 位为 0 转为整数
if((this * checkValue).toInt() % checkValue == 0) {
return this.toInt().toString()
} else {
return String.format("%.${keepLength}f", this)
}
}