【Kotlin】从Java转向Kotlin,耐心看完这篇博客就够了。Kotlin快速入门教程分享

导读

适合群体:Java已经入门的人,如果是零基础,不要勉强!虽然没有深奥的术语,即使有也尽可能通俗易懂 。

Kotlin和Java都是Jvm语言,相同的部分能省则省(篇幅有限),重点是Kotlin。

示例代码的注释很重要。最好可以使用IDEA等开发工具运行一下。

最后创作不易,全部都是自己亲手码出来的5万多字的笔记分享,如果觉得对您学习Kotlin有帮助,还请三连(点赞,关注,打赏),这是我的创作动力。

build.gradle.kts

//version_gradle:7.4.2
//version_jdk:17.0.6
plugins {
    kotlin("jvm") version "1.8.0"
    application
}

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    testImplementation(kotlin("test"))
    implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlin.coreLibrariesVersion}")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<JavaCompile> {
    options.encoding = "UTF-8"
}

kotlin {
    jvmToolchain(17)
}

application {
    mainClass.set("MainKt")
}

目录

基础篇

启动入口

main

fun main(args: Array<String>) {
	println("Hello World")
}

无参数的main方法与有参数传入的main方法都是kotlin程序启动的入口方法。
启动时先调用有参的main方法,然后调用无参的main方法。

fun main() {
	println("Hello World")
}

虽然两个main方法可以同时存在,无形参的main方法不会执行。
在这里插入图片描述

注释

Kotlin的注释与Java一致

// 单行注释
/*
多行注释
*/
/**
文档注释,KDoc,详见:dokka
 */

方法

方法默认为不可继承的静态方法,参数列表传入的值不能为null

//关键字 fun
//形参名: 类型
//fun 方法名(参数列表): 返回值类型 {方法体}
fun add(n1: Int, n2: Int): Int {
	return n1 + n2
}

add方法对应Java的方法如下。

public static final int add(int n1, int n2) {
	return n1 + n2;
}

如果需要去掉final(子类继承和访问),可以使用关键字open修饰fun

class Main {
    open fun add(n1: Int, n2: Int): Int {
        return n1 + n2
    }
}

方法只有一行时,可以使用=替代{...}

fun add(n1: Int, n2: Int): Int = n1 + n2

数据类型

变量、常量

//变量
var i: Int = 1
//常量
val n: Int = 2

i = i + 1 //OK
n = n + 1 //ERROR, Val cannot be reassigned

原始数据类型

Kotlin没有基本数据类型,只有封装Java基本类型后的原始数据类型。

编译前都是包装的类型,编译时会进行判断(null的可能性检查),选择基本类型还是包装类型。

整数
类型位宽最小值最大值
Byte8-128127
Short16-3276832767
Int32-2,147,483,6482,147,483,647
Long64-9,223,372,036,854,775,8089,223,372,036,854,775,807
  • 整型默认为Int

  • Long类型的整数需要加尾缀L(必须大写)

浮点
类型位宽有效位数指数位数最小值最大值
Float322481.4e-453.4028235e38
Double6453114.9e-3241.7976931348623157e308
  • 浮点型默认为Double
  • Float需要加尾缀F
  • e标记的前后必须为10进制数,2e3表示2*10^3,不支持Java的p标记
字符
字符位宽精度范围
Char16\u0000~\uFFFF

Char类型废弃了所有转换为数字的方法。

var a: Char
var b: Int

a = 'A'
//Int -> Char
a = (65).toChar()
a = Char(65)
//Char -> Int
a = 'A'
b = a.code
布尔
布尔类型范围
Booleanfalse, true
无符号数字
类型位宽最大值
UByte8255
UShort1665535
UInt324,294,967,295
ULong6418,446,744,073,709,551,615
var a: ULong
var b: UByte

a = 200UL
//Sign -> UnSign
b = (-10).toUByte() //246
//UnSign -> Sign
println(b.toByte()) //-10
//UnSign Calc
var c = a * b
println(c)//200*246=49200
  • 无符号类型需要加尾缀U
  • 无符号类型的变量不能直接与有符号类型的变量运算
进制

默认为10进制,16进制前缀为0x,2进制前缀为0b,不支持8进制。

数字和数字之间可以使用下划线_分隔。

数组

ArrayIntArrayUIntArray

字符串

String"字符串""""原始字符串"""

数值类型的自动转换

原数据类型的精度范围在目标数据类型的范围内,可以自动转换。否则会因为丢失精度转换为错误的数值。

//Long转Int不能强制转换
var a: Int = 129L; //ERROR
//Int转Long自动转换
var b: Long = 129;
//超过最大值,最高位的进位丢失
var c = b.toByte() //-127

不同类型进行运算时,先自动转换为范围较大的类型,然后再运算(相同的类型才能进行运算)。

var a: Int = 129;
var b: Long = 129L;
var c = a * b //Long
println(c) //16641

表达式和语句

//这是条语句,其中 n = 1 是表达式
var n = 1

表达式有返回值,语句没有(等价于Java的Void)。

语句是程序的执行最小单元。

var n //声明语句
n = 1 //赋值语句(表达式加换行符)
n++ //自增语句
println(n) //方法调用语句

还有创建对象语句、控制流语句。

控制流语句

不支持goto

if

用法

var a = 1
var b = 2
var max = a
//单行写法
if (a < b) max = b
//完整的写法
if (a > b) {
    max = a
} else if (a < b) {
    max = b
} else {
    TODO("一样大,什么也不做")
}
//表达式写法
max = if (a < b) b //ERROR
max = if (a > b) a else b //OK
max = if (a > b) { //OK
    println("Choose a")
    a //缺省 return 的返回值
} else {
    println("Choose b")
    b
}
println(max)

when

类似switch的用法,Java14、17、18都进行了加强,现在Java的switch和Kotlin的when用法相似。

val n = when (val s = readlnOrNull()) {
    null -> 0
    else -> s.length
}
println("输入了${n}个字符")

使用Range进行匹配。

fun main() {
    test(2)
}

fun test(v: Int) {
    when (v) {
        in 1..5 -> {
            println("工作日")
        } //不用担心break
        in setOf(6, 7) -> println("休息日")
        !in 1..7 -> {
            println("非法输入")
        }
    }
}

使用Set集合进行匹配,原理是比较HashCode值,所以Set集合是无序。

fun main() {
    test("绿", "红") //黄
}

fun test(a: String, b: String) {
    when (setOf(a, b)) {
        setOf("红", "绿") -> {
            println("黄")
        }
        setOf("红", "蓝") -> {
            println("紫")
        }
        setOf("蓝", "绿") -> {
            println("青")
        }
        else -> {
            println("非法输入")
        }
    }
}

数据类型匹配

fun main() {
    test(2)
}

fun test(v: Any) = when (v) {
    is String -> println("字符串")
    is Int -> println("数字")
    else -> println("未知")
}

for

//.. 正序:[0, 10]
for (i in 0..10) {
    //0 1 2 3 4 5 6 7 8 9 10
    print("$i ")
    //$ 字符串模板,格式化输出变量的值
}
//until 正序:[0, 10)
for (i in 0 until 10) {
    //0 1 2 3 4 5 6 7 8 9
    print("$i ")
}
//downTo 逆序:[10, 0]
//step 步进值
for (i in 10 downTo 0 step 3) {
    //10 7 4 1
    print("$i ")
}

forEach

val a: IntProgression = (10 downTo 0 step 3)
a.forEach { i -> //这里不写,默认形参为it
    //10 7 4 1
    print("$i ")
}

dowhile

用法

while (/*布尔表达式*/true) {
    TODO("循环内容")
}
do {
    TODO("循环内容")
} while (/*布尔表达式*/true)

label@

跳出到指定代码块的标签

val gcd = lambda@{
    w1@ while (true) {
        while (true) {
            break
            continue@w1
            return@lambda
        }
    }
}

null

Type?

任何数据类型后面加?,表示这个类型的值可以为null

var a: Int? //可以为null
var b: Int //不可以为null

可以为null的类型不可以赋值给不能为null的类型

fun main() {
    var a: Int? = 1
    test1(a) //Int? 不可以向 Int 传参
    var b: Int = 1
    test2(b) //Int 可以向 Int? 传参
}

fun test1(a: Int) {
    println(a)
}

fun test2(a: Int?) {
    println(a)
}

可能为null的方法调用

var str: String? = null
//必须加?,返回值类型为Int?
var len = str?.length
println(len) //null

?:

可以为null的数据类型,可以使用Elvis操作符。

var str: String? = null
//左边为null时,执行右边
var len = str?.length ?: -1
println(len) //-1

as?

var str = "123"
//左边不为null时,强制转换为右边的类型
//不支持从父类强制转换为子类
//转换失败,结果为null
var b = str as? String
println(b) //123

!!

var str = "123"
//str必须为非null,否则抛出
//java.lang.NullPointerException
var b = str!!.length
println(b) //3

尽量避免使用非null断言(!!),会破坏kotlin对null的设计初衷。

var str: String? = "123"
str?.let {
//let:只有在str非null时执行
//避免直接调用str.length出现异常
//也避免了执行str?.length,出现不正常的结果
//it是默认传入的参数,不为null
    println(it.length) //3
}

或者直接退出,不执行后续代码,保证不产生不正常的结果和执行多余的步骤。

var str: String? = "123"
//如果为null,执行右边的代码退出(或者抛异常)
var s = str ?: return
println(s?.length) //3
var str: String? = "123"
//使用?:避免null引发后续代码的执行
//如果str为null,会出现非法值-1,不会进入执行
if ((str?.length ?: -1) >= 0) {
    println(str?.length) //3
}

包装类型和基本类型

编译时如果参数有?修饰,并且有null的可能,编译后为装箱的包装类型,否则会自动优化,都编译为基本数据类型。

//int
var a: Int = 1
//Integer
var b: Int? = null

集合和数组默认使用Java的包装类型,如果使用特定类型的arrayOf方法,则是Java的基本类型。

//List<Integer>
var list: List<Int> = listOf(1, 2, 3)
//Integer[]
//Array<T>的arrayOf方法
var arr1: Array<Int> = arrayOf(1, 2, 3)
//int[]
var arr2 = intArrayOf(1, 2, 3)

访问修饰符

修饰类和顶层成员

修饰符Java最大访问范围Kotlin最大访问范围
public全局全局
protected相同包和所有子包不支持
internal不支持相同模块
default、缺省相同包全局
private源文件源文件

修饰类的成员

修饰符Java最大访问范围Kotlin最大访问范围
public全局全局
protected相同包和所有子包所有子类
internal不支持相同模块
default、缺省相同包全局
private

访问权限的传递

继承访问权限时,访问修饰符的访问范围不能被放大,可以更严格(缩小可以访问的范围)。

数组

创建

fun main() {
    //Integer[]
    val arr1: Array<Int> = Array(3) { 1 }
    println(arr1.contentToString())//[1, 1, 1]

    val arr2: Array<Int> = arrayOf(1, 2, 3)
    println(arr2.contentToString())//[1, 2, 3]
    //int[]
    val arr3: IntArray = IntArray(3) { 1 }
    println(arr3.contentToString())//[1, 1, 1]
    val arr4: IntArray = intArrayOf(1, 2, 3)
    println(arr4.contentToString())//[1, 2, 3]
    //Integer[][]
    //IntArray只能是一维数组
    val arr5 = Array(3) { intArrayOf(0, 0, 0) }
    println(arr5.contentDeepToString())//[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
}

常用方法

map

将每一个元素操作后放回。

val arr = intArrayOf(1, 2, 3)
arr.map {
    it * 2
}.also {
    println(it) //[2, 4, 6]
}
flatMap

将每一个元素操作后放入一个集合,然后操作下一个元素时,将这个集合中的所有元素整合在一起,最后返回整合后的集合。

val arr = intArrayOf(1, 2, 3)
arr.flatMap {
    listOf(it * 2, 0)
}.also {
    println(it) //[2, 0, 4, 0, 6, 0]
}
fold

设置初始值,然后进行累计操作。

val arr = intArrayOf(1, 2, 3)
//初始值1,累加
arr.fold(1) { sum, i ->
    sum + i
}.also {
    println(it) //7
}
associate

将数组的元素转换为键值对,键相同时会不存放。

val arr = intArrayOf(1, 2, 3)
arr.associate {
    Pair(it, it * 2) //键值对
}.also {
    println(it) //{1=2, 2=4, 3=6}
}

//另一种写法,以数组的元素为键,存放特定的值,键相同时会不存放。
intArrayOf(1, 2, 3).associateWith { it * 2 }.also(::println)

将数组的元素按照特定的键进行存放,键相同时会不存放。

val arr = intArrayOf(1, 2, 3)
arr.associateBy {
    it * 2
}.also {
    println(it) //{2=1, 4=2, 6=3}
}
distinct

去重。已经存在时不存放。

val arr = intArrayOf(1, 2, 2)
arr.distinct().also {
    println(it) //[1, 2]
}
val arr = intArrayOf(1, 2, 3)
arr.distinctBy {
    it % 2
}.also {
    println(it) //[1, 2]
}

AnyUnitNothing

KotlinJava
AnyObject
UnitVoid
Nothing没有对应的类型,编译后为Void

Any是所有类型的父类,Unit是表达式没有返回值时用来替代的一种类型。

Nothing

不可以实例化,是所有类型的子类,常用在抛出异常的方法,作为返回值类型(只抛出异常,没有什么可以返回)。

fun main() {
    var str = "123"
    var len = size(str)
    println(len)
}

fun size(str: String?): Int {
    return str?.length ?: error("不能为null")
}

//不加Nothing,size方法会报错,error方法返回的是Unit类型,与size方法返回类型不兼容
//加Nothing,编译器会检测出异常出现的位置,判断出死代码的范围并提醒
//Nothing是所有类型的子类,与size方法的Int返回类型兼容
fun error(msg: String): Nothing {
    throw RuntimeException(msg)
}

集合

包路径:kotlin.collections.*

不可变集合

只能查询集合中的元素,不能修改。

优点:

  • 被不可信任的库调用时,不可变集合更安全。
  • 在多线程环境下,没有并发的安全性问题。
  • 不可变集合节省内存空间,不需要扩容
val list: List<Int> = listOf(1, 2, 3)
println(list.javaClass)//java.util.Arrays$ArrayList
println(list)//[1, 2, 3]

val set: Set<Int> = setOf(1, 2, 3)
println(set.javaClass)//java.util.LinkedHashSet
println(set)//[1, 2, 3]

val map: Map<Int, String> = mapOf(
	1 to "01", 2 to "02", 3 to "03"
)
println(map.javaClass)//java.util.LinkedHashMap
println(map)//{1=01, 2=02, 3=03}

可变集合

可以对集合中的元素进行增删改查。

val list: List<Int> = mutableListOf(1, 2, 3)
println(list.javaClass)//java.util.ArrayList
println(list)//[1, 2, 3]

val set: Set<Int> = mutableSetOf(1, 2, 3)
println(set.javaClass)//java.util.LinkedHashSet
println(set)//[1, 2, 3]

val map: Map<Int, String> = mutableMapOf(
	1 to "01", 2 to "02", 3 to "03"
)
println(map.javaClass)//java.util.LinkedHashMap
println(map)//{1=01, 2=02, 3=03}
val list: List<Int> = arrayListOf(1, 2, 3)
println(list.javaClass)//java.util.ArrayList
println(list)//[1, 2, 3]

val set: Set<Int> = hashSetOf(1, 2, 3)
println(set.javaClass)//java.util.HashSet
println(set)//[1, 2, 3]

val map: Map<Int, String> = hashMapOf(
	1 to "01", 2 to "02", 3 to "03"
)
println(map.javaClass)//java.util.HashMap
println(map)//{1=01, 2=02, 3=03}
val set: Set<Int> = linkedSetOf(1, 2, 3)
println(set.javaClass)//java.util.LinkedHashSet
println(set)//[1, 2, 3]

val map: Map<Int, String> = linkedMapOf(
	1 to "01", 2 to "02", 3 to "03"
)
println(map.javaClass)//java.util.LinkedHashMap
println(map)//{1=01, 2=02, 3=03}
val set: Set<Int> = sortedSetOf(1, 2, 3)
println(set.javaClass)//java.util.TreeSet
println(set)//[1, 2, 3]

val map: Map<Int, String> = sortedMapOf(
	1 to "01", 2 to "02", 3 to "03"
)
println(map.javaClass)//java.util.TreeHashMap
println(map)//{1=01, 2=02, 3=03}

常用方法

val list: List<Int> = listOf(1, 2, 3)
//有一个不满足,返回false
list.all { n -> n == 3 }
	.apply { println(this) }//false
//有一个满足条件,返回true
list.any { n -> n != 3 }
	.apply { println(this) }//true
//统计满足条件的元素个数
list.count { it >= 2 }
	.apply { println(this) }//2
//寻找第一个满足条件的元素
list.find { n -> n >= 2 }
	.apply { println(this) }//2
list.firstOrNull { n -> n >= 2 }
	.apply { println(this) }//2
//按条件分组
list.groupBy { n -> n % 2 }
	.apply { println(this) }//{1=[1, 3], 0=[2]}
//过滤不符合条件的元素
list.filter { n -> n % 2 == 0 }
	.apply { println(this) }//[2]
//寻找最大值
listOf("asx", "s", "qza", "amount")
	.maxByOrNull(String::length)
	.apply { println(this) }//amount

序列

集合调用filter方法和map等处理元素的方法时,会创建中间临时集合对象进行封装。为了避免这个过程,引入sequence序列。

var list: List<Int> = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
list = list.asSequence()
    //[5,6,7,8,9,a],不执行
    .filter { n -> n > 4 }
    //[a,c,e,10,12,14],不执行
    .map { n -> n * 2 }
    //归集方法
    .toList()
    //开始执行序列设计好的上述过程
    //每个元素都会经过上述操作,避免中间创建临时对象
println(
    //序列格式化输出
    list.joinToString(
        "[", //左边界
        ",", //分隔符
        "]", //右边界
        10, //最多显示的元素个数
        "...", //超出限制数量不显示的元素,使用填充
        { it.toString(16) } //转换元素
    )
)

字符串

比较

fun main() {
    //创建
    val str = "abcDefgHi"
    val str1 = "$str" + " " + "${str.length}"
    val str2 = buildString {
        append("abcdefghi").append(' ').append(str.length)
    }
    
    //内容相同(引用可能不同),false
    (str1 == str2).ptl()
    //引用相同,false
    (str1 === str2).ptl()
    //忽略大小写比较,true
    (str1.equals(str2, true)).ptl()
    //匹配前缀,true
    (str1.startsWith("AB", 0, true)).ptl()
    //匹配后缀,false
    (str1.endsWith("hi", true)).ptl()
}

//扩展方法:为特定类型添加方法(并未修改源文件)
//作用域受这个方法的修饰词控制
fun Any?.ptl() {
    println(this)
}

处理

fun main() {
    //获取第一个字符
    "abcDefgHi"[0].ptl() //a
    //长度为0的字符串=>NoSuchElementException
    "abcDefgHi".first().ptl() //a
    //长度为0的字符串=>null
    "abcDefgHi".firstOrNull().ptl() //a
    
    //获取最后一个字符
    "abcDefgHi".last().ptl() //i
    "abcDefgHi".lastOrNull().ptl() //i
    "abcDefgHi".lastOrNull { it == 'c' }.ptl() //c
    
    //截取
    "abcDefgHi".drop(3).ptl() //DefgHi
    "abcDefgHi".dropLast(3).ptl() //abcDef
    
    //删除前后特定字符
    "_1_234_567_".removeSurrounding("_").ptl() //1_234_567
    
    //分组
    "1,23_4,567".split("_", ",").ptl() //[1, 23, 4, 567]
    
    //设置默认值
    " \n \r \t".ifBlank { "空字符串" }.ptl()
    "".ifEmpty { "0长度字符串" }.ptl()
    
    //资源路径截取
    val path = "C:\\Windows\\System32\\cmd.exe"
    //文件名
    path.substring(path.lastIndexOf('\\') + 1).ptl()
    path.removeRange(0..path.lastIndexOf('\\')).ptl()
    path.substringAfterLast('\\').ptl()//推荐
    //批量去除后缀名
    listOf("1.jpg", "2.jpg", "3.jpg").map { it.removeSuffix(".jpg") }.forEach(::println)
    listOf("1.jpg", "2.png", "3.tiff").map { it.substringBefore(".") }.forEach(::println)
    
    //首字母大写
    "system".capitalize().ptl()
    "system".replaceFirstChar {
        if (it.isLowerCase()) {
            it.titlecase(Locale.getDefault())
        } else {
            it.toString()
        }
    }.ptl()
}

fun Any?.ptl() {
    println(this)
}

原始字符串

fun main() {
    //转义符失效
    """
    asdf \t \n \r 123
    """.ptl()
    //保留每一行和前面的空格
    
    """
    asdf \t \n \r 123
    """.trimIndent().ptl()
    //删除前后的空行和左边的空格
    
    """
    |public static void main(String[] args) {
    |System.out.println("Hello World!");
    |}
    """.trimMargin("|").ptl()
    //默认以”|“字符为左边界,删除左边的字符和多余的空行
    
    //支持拼接
    ("""123""" + """sada""" + "125").ptl()
}

fun Any?.ptl() {
    println(this)
}

泛型

型变、约束

open class Animal //动物
open class Dog : Animal() //狗
open class Shiba : Dog()//柴犬

open class Shop<T> {
    fun run(t: T?): T? = t
}

fun main() {
    //out:协变,<? extends Dog>,子类泛型可以赋值给父类泛型
    val s1: Shop<out Dog> = Shop<Animal>() //error
    val s2: Shop<out Dog> = Shop<Dog>()
    val s3: Shop<out Dog> = Shop<Shiba>()
    val s2run: Dog? = s2.run(/*Nothing*/null)//能获取,不能传入
    //in:逆变,<? super Dog>,父类泛型可以赋值给子类泛型
    val s4: Shop<in Dog> = Shop<Animal>()
    val s5: Shop<in Dog> = Shop<Dog>()
    val s6: Shop<in Dog> = Shop<Shiba>() //error
    val s5run: Any? = s5.run(Dog())//能传入,不能获取
    //不变,<Dog>,类型被限定
    val s7: Shop<Dog> = Shop<Animal>() //error
    val s8: Shop<Dog> = Shop<Dog>()
    val s9: Shop<Dog> = Shop<Shiba>() //error
    val s8run: Dog? = s8.run(Dog())//能获取,能传入
    //投影,<in Nothing>,<out Any>,类型不被限定
    val s10: Shop<*> = Shop<Animal>()
    val s11: Shop<*> = Shop<Dog>()
    val s12: Shop<*> = Shop<Shiba>()
    val s11run: Dog? = s8.run(Dog())//能获取,能传入
}

方法

可选参数

import kotlin.math.pow

//参数拥有默认值后,变为可选参数,不需要赋值也可以使用
fun pow(
    a: Double = .0,
    base: Double = 10.0,
    c: Double = 1.0
) = a + base.pow(c) //表达式方法

fun main() {
//跨参数赋值可以使用参数名指定
    println(pow(1.0, c = 3.0)) //1001.0
}

vararg

参数的数量可以不固定,使用vararg修饰的参数为可变长度的参数,本质是一个数组。

位置不受限制,放在固定参数的前边是数组,放在后边是Java的T...

fun add(vararg arr: Int, last: Int): IntArray {
    val temp = arr.copyOf(arr.size + 1)
    temp[arr.size] = last
    return temp
}

fun main() {
    //println(add(last = 0))//ERROR

    //[0]
    println(add(last = 0).contentToString())

    //[1, 2, 0]
    println(add(1, 2, last = 0).contentToString())

    //[1, 2, 3, 4, 0]
    println(add(1, 2, 3, 4, last = 0).contentToString())
}

*

展开参数的操作符。避免将int[]传入T...时,int[]被当作一个元素的T

fun main() {
    val arr = arrayOf(1, 2, 3)
    //识别为一个对象
    //public fun <T> listOf(element: T): List<T>
    listOf(arr).run { println("$size") } //1
    //数组也是一个对象,使用*后,匹配vararg形参
    //public fun <T> listOf(vararg elements: T): List<T>
    listOf(*arr).run { println("$size") } //3
}

infix

两个相同类型的形参方法,使用infix修饰方法,可以放在两个相同的参数之间,不需要使用点和括号的方法,构成中缀表达式的语法。

infix fun Int.max(that: Int) = this.coerceAtLeast(that)

fun main() {
    //相当于 5.max(6)
    println(5 max 6)//6
}

for循环的untildownTostep使用了中缀表达式。

Map的键值对使用key to value的方式生成Pair对象。

扩展方法

在方法名前使用类型名进行指定扩展的方法。并没有在指定类型的源文件中进行扩展。

//为所有类型扩展ptl方法
//在哪里扩展,从哪里来调用
fun Any?.ptl() {
    println(this)
}

局部方法

方法里面嵌套的方法。

fun main() {
    fun max(a: Int, b: Int) = a.coerceAtLeast(b)
    max(5, 6).ptl()//6
}
fun Any?.ptl() = println(this)

匿名方法

fun main() {
    (fun(a: Int, b: Int): Int {
        return a.coerceAtLeast(b)
    })(5, 6).ptl()//6
}
fun Any?.ptl() = println(this)

进阶篇

构造方法

创建对象时用于初始化类属性的方法,返回创建的对象。

在类名后为一级构造方法,在类内为二级构造方法。

//一级构造方法的参数如果没有被var、val标记
//不会生成属性、get、set方法
//constructor可以省略
open class Person constructor(var name: String) {
    var age: Int = 0

    //二级构造方法的参数不可以被var、val标记
    constructor(name: String, age: Int) : this(name) {
        this.age = age
    }
}

//顶级成员类的构造方法私有化
//object 类名
object Man

伴生对象

伴生对象:静态类成员。每个类只能有一个伴生对象

open class Math {
    //companion object name
	//name缺省时,默认名为:Companion
    companion object Const{
        //公共常量
        const val INT_MAX: Int = Int.MAX_VALUE
        //私有常量
        val INT_MIN: Int = Int.MIN_VALUE
        //静态变量
        var INT_BITS: Int = Int.SIZE_BITS
        //静态方法
        fun max(x: Int, y: Int): Int {
            return x.coerceAtLeast(y)
        }
    }
}

fun main() {
    Math.INT_MAX //Kotlin直接调用
    Math.Const.INT_MAX //兼容Java的调用
}

单例模式

(主要是了解构造方法怎么私有化。)

饿汉
//将构造方法私有化
object Person

fun main() {
    val p1 = Person
    val p2 = Person
    println(p1 === p2)//true
}
懒汉
//将构造方法私有化
class Person private constructor() {
    companion object {
        private var INSTANCE: Person? = null
        fun getInstance(): Person {
            if (INSTANCE === null) {
                //创建并保存单例对象
                INSTANCE = Person()
            }
            return INSTANCE!!
        }
    }
}

fun main() {
    val p1 = Person.getInstance()
    val p2 = Person.getInstance()
    println(p1 === p2)
}
线程安全的懒汉
class Person private constructor() {
    companion object {
        private var INSTANCE: Person? = null
        @Synchronized //这个方法将被加上同步锁关键字
        fun getInstance(): Person {
            if (INSTANCE === null) {
                INSTANCE = Person()
            }
            return INSTANCE!!
        }
    }
}

fun main() {
    val p1 = Person.getInstance()
    val p2 = Person.getInstance()
    println(p1 === p2)
}
双重校验锁
class Person private constructor() {
    companion object {
        private val INSTANCE: Person by lazy(
            //懒加载的同步锁
            mode = LazyThreadSafetyMode.SYNCHRONIZED
        ) {
            return@lazy Person()
        }
        fun getInstance(): Person {
            return INSTANCE
        }
    }
}

fun main() {
    val p1 = Person.getInstance()
    val p2 = Person.getInstance()
    println(p1 === p2)
}
静态内部类模式
class Person private constructor() {
    companion object {
        private object PersonSingleton {
            val singleton = Person()
        }

        private val INSTANCE: Person = PersonSingleton.singleton
        fun getInstance(): Person {
            return INSTANCE
        }
    }
}

fun main() {
    val p1 = Person.getInstance()
    val p2 = Person.getInstance()
    println(p1 === p2)
}

匿名内部类对象

class Person {
    fun main() {
        //抽象类不能创建对象,但是实现全部方法后
        //就可以创建内部类的匿名对象
        //然后直接调用对象的方法
        object : AbstractCollection<Int>() {
            override val size: Int
                get() = TODO()

            override fun iterator(): Iterator<Int> {
                TODO()
            }
        }.stream().forEach(::println)
    }
}

init

初始化代码块。创建对象,初始化类时,最先被执行的代码。

class Person {
    //会添加到一级构造方法中,每次创建对象时最先执行
    //如果没有一级构造方法,则添加到每一个二级构造方法中
    //如果在companion中,自动转换为static{ }
    init {
        TODO()
    }
}

inner

创建的内部类默认为静态内部类,如果需要去掉静态,使用关键字inner

open class Person {
    //public static final
    class Man {
    }

    //public final
    inner class Woman {
    }
}

sealed

密封类:枚举的扩展。被标记后是抽象类。

sealed class Person {
    class Man() : Person()
    class Woman() : Person()
}

fun func(p: Person) {
    when (p) {
        is Person.Man -> println("男")
        is Person.Woman -> println("女")
        else -> println("中性") //多余的分支
    }
}

data class

数据类:自动为一级构造方法的属性创建一些特殊的方法。只对一级构造方法的参数有用。

//equals() / hashCode()
//toString() 格式如 "Person(name=John, age=42)"
//componentN() functions 对应于属性,按声明顺序排列
//copy() 函数
data class Person(var id: Int)

interface

接口类。

interface IPerson {
    //接口的属性不允许赋值,实现时必须重写,get和set方法为抽象方法
    var id: Int

    //如果没有方法体,为抽象方法
    //如果有方法体,除了抽象方法,还将实现封装在DefaultImpls类中,成为默认方法
    fun idPlus() {
        id++
    }
}

class Person : IPerson {
    //override 重写接口中的成员
    override var id: Int = 0
    override fun idPlus() {
        //调用默认方法
        super.idPlus()
        println(id)
    }
}

类的成员

类由多个字段和方法构成。

字段、属性

属性的声明由valvar标记,一级构造方法也是如此。

abstract修饰的抽象成员可以不用初始化赋值。

  • private标记,没有setget方法
  • val标记,为属性,只有get方法
  • var标记,为属性,setget方法都有
class Person {
    //Field 类的字段。
    private val id = 0 //字段

    //Property 字段和对应的get、set方法组成类的属性。
    val age = 0 //age属性
    var name = "" //name属性
}

fun main() {
    val p = Person()
    val fields = p.javaClass.declaredFields
    for (field in fields) {
        println(field.name)
    }
}
field

手动为属性添加getset方法

class Person {
    val age: Int = 18
        //field指代这个属性的值,使用this.age会出现无限的嵌套调用
        get() = field
    var name: String = "无名氏"
        //get方法不可以私有化,属性必须可读
        get() {
            return field
        }
        //set方法可以私有化,只供内部调用
        private set(name) {
            if (name.isNotEmpty()) field = name
        }
}

const

常量,只能修饰原始类型和字符串。

class Person {
    //常量必须放在伴生对象中
    companion object {
        //public static final
        const val MAN = 1
        const val WOMAN = 2
    }
}

lateinit

懒加载的属性不可以为原始数据类型(不能为null)

class Person {
    //属性只能为使用var修饰,可以不用初始化
    lateinit var name: String
    override fun toString(): String {
        if (this::name.isInitialized.not()) this.name = ""
        //如果调用没有初始化的属性,将抛出异常
        //UninitializedPropertyAccessException
        return "Person(name='$name')"
    }
}

fun main() {
    val p = Person()
    //Person(name='')
    println(p.toString())
    p.name = "张三"
    //Person(name='张三')
    println(p.toString())
}

委托

by

属性的get和set方法实现,可以委托给别的类的对象进行重载。

import kotlin.reflect.KProperty

class Name {
    //使用委托对象后,这个委托对象没有name字段,只有name字段的get和set方法的重载
    //operator 重载
    operator fun getValue(p: Person, pro: KProperty<*>): String {
        println("getValue: 将 ${pro.name} 委托到 ${this.toString()} 来处理")
        return pro.name
    }

    operator fun setValue(p: Person, pro: KProperty<*>, s: String) {
        println("setValue: 将 $s 委托到 ${this.toString()} 来处理,然后交给 ${pro.name}")
    }
}

class Person {
    //委托Name对象完成get和set方法的实现
    var name: String by Name()
    override fun toString(): String {
        return "Person(name='$name')"
    }
}

fun main() {
    val p = Person()
    p.name = "123"
    println(p.toString())
}

by lazy

import kotlin.concurrent.thread

class Person {
    //延迟加载的属性只能使用val修饰
    private val name: String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
        println("${Thread.currentThread().id.toString()} lazy")
        "123"
    }

    override fun toString(): String {
        Thread.sleep(10)
        return "${Thread.currentThread().id.toString()} Person(name='$name')"
    }
}

fun main(args: Array<String>) {
    val p = Person()
    repeat(10) {
        thread(name = "线程:${it}") {
            println(p.toString())
        }
    }
}

LazyThreadSafetyMode

  • SYNCHRONIZED:默认,多线程环境下,只会执行一次,然后任何线程就不会再执行
  • PUBLICATION:多个线程都可能会执行一次,直到检测到已经初始化后不再执行
  • NONE:没有线程安全的保障,只能在单线程中安全的使用

by Delegates.observable

属性的值发生变化时的监听处理。

import kotlin.properties.Delegates

class Person {
    //设置初始值<init>
    var name: String by Delegates.observable("<init>") { pro, old, new ->
        //然后每次变化前会执行这个方法
        println("属性名:${pro.name} , ${old} -> ${new}")
    }

    override fun toString(): String {
        return "Person(name='$name')"
    }
}

fun main() {
    val p = Person()
    p.name = "A"
    p.name = "B"
    println(p.toString())
}

by map

使用Map对象初始化对象的属性值。键是属性名,值是属性值。

class Person(val map: Map<String, Any>) {
    private val id: Int by map
    private val name: String by map
    override fun toString(): String {
        return "Person(id=$id, name='$name')"
    }
}

fun main() {
    val p = Person(hashMapOf("id" to 1, "name" to "a"))
    println(p.toString())
}

委托模式

当两个子类(继承同一个类)的实现完全一致,可以只实现一个子类,另一个子类交给这个类去实现。一处修改,另一个自动修改。

interface People {
    fun talk()
}

class Man : People {
    override fun talk() {
        println("talk")
    }
}

//Woman使用代理对象man实现接口,实现和Man类完全一致
class Woman(private val man: Man = Man()) : People by man

fun main() {
    val w = Woman()
    w.talk()
}

枚举

可以约束传参时的值在合理的范围区间。

enum class Color(var color: Int) {
    RED(0xFF0000),
    WHITE(0xFFFFFF),
    BLACK(0x000000),
    GREEN(0x00FF00)
}

fun toString(color: Color): String {
    //when判断枚举
    return when (color) {
        Color.RED -> "红"
        Color.BLACK -> "黑"
        Color.GREEN -> "绿"
        Color.WHITE -> "白"
    }
}

fun main() {
    println(toString(Color.WHITE))
}

密封类和枚举对比

密封类比枚举类更灵活。常用来表示受限制的类继承结构(密封类中的内部类都必须继承这个密封类)。

枚举中,每个枚举值都是枚举的单例,密封类中的子类可以不是单例(更自由灵活)。

密封类的子类都必须在这个密封类中,但是密封类子类的子类不受限制。

密封类是抽象类,不能实例化,内部的子类可以实例化。

常用方法

import java.io.Serializable

//枚举可以实现接口,不能继承类
enum class Country : Serializable {
    USA {
        //匿名内部类,重载方法
        override fun toString(): String = "美国"
    },
    KR,
    JP;
}

fun main() {
    //名称
    Country.USA.ptl()//美国
    Country.USA.name.ptl()//USA
    //序号,when索引时底层使用
    Country.JP.ordinal.ptl()//2
    //使用name获取枚举值
    //不存在抛出异常java.lang.IllegalArgumentException
    Country.valueOf("USA").ordinal.ptl()//0
    //输出全部枚举值
    Country.values().toList().ptl()//[美国, KR, JP]
}

fun Any?.ptl() = println(this)

实例域

使用枚举时不推荐使用ordinalname(序列化和反序列化出现差异时无法避免null),不推荐作为接口的返回类型。

//使用实例域代替ordinal序数,避免枚举发生变化时,无法对应
enum class Country(val id: Int) {
    USA(101),
    KR(201),
    JP(202);
}

fun main() {
    Country.USA.id.ptl()
}

fun Any?.ptl() = println(this)

操作符重载

https://github.com/JetBrains/kotlin/blob/1.8.0/spec-docs/operator-conventions.md

import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
//分数类
class Fraction private constructor() {
    private var p: Int = 0 //分子
    private var q: Int = 0 //分母

    constructor(p: Int, q: Int) : this() {
        if (q == 0) throw Exception("q != 0")
        if (q < 0) {
            this.q = -q
            this.p = -p
        }
        val g = gcd(abs(p), q)
        this.p = p / g
        this.q = q / g
    }

    //this + a
    operator fun plus(a: Fraction): Fraction {
        return Fraction(a.p * this.q + a.q * this.p, a.q * this.q)
    }

    //this += a
    operator fun plusAssign(a: Fraction) {
        val f = this + a
        this.p = f.p
        this.q = f.q
    }

    //-this
    operator fun unaryMinus(): Fraction {
        return Fraction(-this.p, this.q)
    }

    //this - a
    operator fun minus(a: Fraction): Fraction {
        return this + -a
    }

    //this * a
    operator fun times(a: Fraction): Fraction {
        return Fraction(this.p * a.p, this.q * a.q)
    }

    //this / a
    operator fun div(a: Fraction): Fraction {
        return Fraction(this.p * a.q, this.q * a.p)
    }

    //this % a
    //operator fun rem(other: Fraction): Fraction
    companion object {
        fun gcd(a: Int, b: Int): Int {
            if (a == 0 || b == 0) return 0
            var t: Int
            var max: Int = max(a, b)
            var min: Int = min(a, b)
            while (max % min != 0) {
                t = max % min
                max = min
                min = t
            }
            return min
        }
    }

    private fun toDouble(): Double {
        return p.toDouble() / q
    }

    override fun toString(): String {
        return toDouble().toString()
    }
}

fun main() {
    val f_2_3 = Fraction(2, 3)
    val f_3_7 = Fraction(3, 7)
    println(f_2_3 - f_3_7) //0.23809523809523808
    println(f_2_3 * f_3_7) //0.2857142857142857
    println(f_2_3 / f_3_7) //1.5555555555555556
    f_2_3 += f_3_7
    println(f_2_3) //1.0952380952380953
}

一元操作符

操作符函数名接口方法
+ aunaryPlusoperator fun unaryPlus(): T
- aunaryMinusoperator fun unaryMinus(): T
! anotoperator fun not(): Boolean
++aincoperator fun inc(): T
a++incoperator fun inc(): T
--adecoperator fun dec(): T
a--decoperator fun dec(): T

基本运算符

操作符函数名接口方法示例
a + bplusoperator fun plus(t: T): T
a - bminusoperator fun minus(t: T): T
a * btimesoperator fun times(t: T): T
a / bdivoperator fun div(t: T): T
a % bremoperator fun rem(t: T): T

复合赋值操作运算符

操作符函数名接口方法示例
a += bplusAssignoperator fun plusAssign(t: T)
a -= bminusAssignoperator fun minusAssign(t: T)
a *= btimesAssignoperator fun timesAssign(t: T)
a /= bdivAssignoperator fun divAssign(t: T)
a %= bremAssignoperator fun remAssign(t: T)

如果只重载了plus运算符,执行a+=b时,自动展开为a=a+b,然后调用plus方法。

如果同时重载了plusAssignplus运算符,执行a+=b时,直接调用plusAssign方法。

按位操作符

Kotlin没有Java的按位操作符,通过中缀方法实现。

操作符函数名接口方法示例
a & ba and binfix fun and(t: T): T
a | ba or binfix fun or(t: T): T
~ aa inv boperator fun inv(): T
a ^ ba xor binfix fun xor(t: T): T
a << ba shl binfix fun shl(t: T): T
a >> ba shr binfix fun shr(t: T): T
a >>> ba ushr binfix fun ushr(t: T): T

比较操作符

操作符函数名接口方法示例
a == bequalsoverride fun equals(other: Any?): Boolean
a != b!(equals)override fun equals(other: Any?): Boolean
a < bcompareTo < 0operator fun compareTo(other: BigUInt): Int
a <= bcompareTo <= 0operator fun compareTo(t: T): Int
a > bcompareTo > 0operator fun compareTo(t: T): Int
a >= bcompareTo >= 0operator fun compareTo(t: T): Int

===!==运算符不支持扩展函数的操作符重载。

索引操作符

操作符函数名接口方法示例
v = a[b]get(b)operator fun get(n: Int): T
v = a[x, y]get(x, y)operator fun get(x: Int, y: Int): T
a[b] = vset(b, v)operator fun set(n: Int, v: T)
a[x, y] = vset(x, y, v)operator fun set(x: Int, y: Int, v: T)

可以为多个参数。

调用操作符

操作符函数名接口方法示例
a(b)a.invoke(b)operator fun invoke(b: Int)
a(x, y)a.invoke(x, y)operator fun invoke(x: Int, y: Int)

可以为多个参数。

in操作符

判断存在于某个范围内。遍历数组或集合。

操作符函数名接口方法示例
a in bb.contains(a)operator fun contains(t: T): Boolean
a !in b!b.contains(a)operator fun contains(t: T): Boolean
for(a in b)b.iterator(){a}operator fun iterator(): Iterator<T>

区间操作符

操作符函数名接口方法示例
a..ba.rangeTo(b)operator fun rangeTo(t: T): ClosedRange<out Comparable<T>>

解构操作符

componentN

class Fraction(private var a: Int, private var b: Int) {
    operator fun component1(): Int {
        return this.a
    }
    operator fun component2(): Int {
        return this.b
    }
}

fun main() {
    val f = Fraction(1, 3)
    //a=>component1,b=>component2
    val (a, b) = f
}
  • 两个返回值元组类:kotlin.Pair
  • 三个返回值元组类:kotlin.Triple

`(反引号)

反引号可以解决关键字冲突的问题,强行将一个不合法的字符变为合法。

fun main(args: Array<String>) {
    val r = 2 `**` 3
    `This is a println function`(r)
}

//自定义运算符
infix fun Number.`**`(b: Number): Double {
    return Math.pow(this.toDouble(), b.toDouble())
}

//自定义方法名,空格不受命名规则限制
fun `This is a println function`(text: Any) {
    println(text)
}

文件

读写

import java.io.File
import kotlin.random.Random

fun main() {
    //文件不存在会自动创建
    val write = File("""D:\1.txt""")
    if (write.exists().not()) write.createNewFile()
    //覆盖
    write.writeBytes("123".toByteArray(Charsets.UTF_8))
    write.writeText("123") //默认使用UTF-8编码
    //追加
    write.appendBytes("12".toByteArray())
    write.appendText("12")
    //使用Java的IO流,完全覆盖
    write.outputStream().use {
        val sb = StringBuilder()
        for (i in 0..10) {
            sb.append(Random.nextInt(0x4E00, 0x9FA5).toChar())
        }
        //默认使用UTF-8编码
        it.write(sb.toString().toByteArray())
    }

    //文件不存在,抛出FileNotFoundException
    val read = File("""D:\1.txt""")
    //按行读取
    ptl { sb ->
        read.forEachLine { sb.append(it) }
    }
    //按数组块大小读取
    ptl { sb ->
        read.forEachBlock { buffer: ByteArray, n: Int ->
            //默认的块大小是4096,块大小不能小于512
            sb.append(buffer.decodeToString(0, n))
        }
    }
    //按字节读取
    ptl { sb ->
        read.inputStream().use {
            var value: Int = it.read()
            while (value != -1) {
                sb.append(value.toString(16)).append(" ")
                value = it.read()
            }
        }
    }
    //使用缓冲区
    ptl { sb ->
        read.inputStream().use {
            val buffer = ByteArray(1024)
            var n: Int
            do {
                n = it.read(buffer, 0, buffer.size)
                if (n == -1) break
                sb.append(buffer.decodeToString(0, n))
            } while (true)
        }
    }

    //使用缓冲流
    ptl { sb ->
        read.bufferedReader().use {
            it.readLine().forEach { sb.append(it.toString()) }
        }
    }
}

//高阶函数,内联函数
inline fun ptl(f: (sb: StringBuilder) -> Unit) {
    val str = StringBuilder()
    f(str)
    println(str)
}

遍历

import java.io.File

fun main() {
    val file: File = File("C:\\Windows\\System32\\cmd.exe")
    println(file.name) //cmd.exe
    println(file.nameWithoutExtension) //cmd
    println(file.extension) //exe

    File("C:\\")
        //父目录
        //.parentFile
        //访问
        .walk()
        //遍历深度
        .maxDepth(Int.MAX_VALUE)
        //过滤后缀
        .filter { it.extension.equals("log", true) }
        .forEach {
            println(it.absolutePath)
            it.delete()
        }
}

拷贝

import java.io.File

fun main() {
    val file = File("""D:\1.txt""")
    //文件拷贝
    //当前目录复制粘贴
    file.copyTo(File(file.parent, "3.txt"), overwrite = true)
    //目录拷贝
    file.parentFile?.copyRecursively(
        //文件所在的目录拷贝到另一个目录
        File(file.parentFile.parent, "2"), overwrite = true
    )
}

异常

kotlin的异常捕获由程序员来评估,自行捕获或规避。

如果通过编译器提醒进行捕获,滥用检查性异常会导致代码冗余,过多不想处理的异常不断向上抛出。

import java.io.File

class EmptyFileException(message: String?) : Exception(message)

fun main() {
    //不需要捕获异常,所有的异常都是unchecked exception
    val file = File("C:\\Windows\\System32\\cmd.exe")
    println(file.absolutePath)
    //根据需要抛出异常
    if (file.length() == 0L) throw EmptyFileException("没有内容")

    val str = ""
    try {
        str.toInt() //制造一个异常
    } catch (e: NumberFormatException) {
        println("$e $str is empty")
    }
}

高级篇

注解

元注解

注解类中用来描述注解类特性的注解。

//Flag注解一直存活到编译后
@Retention(AnnotationRetention.RUNTIME)
//可以在同一个位置重复标记
@Repeatable
//Flag重复标记时,会存放在Flag$Container注解容器
//如果需要指定注解容器,Java调用时兼容
@JvmRepeatable(Flags::class)
//被标记的成员将生成API文档
@MustBeDocumented
annotation class Flag

//Flag注解被Target限制在只能在字段和属性上标记
@Target(AnnotationTarget.FIELD, AnnotationTarget.ANNOTATION_CLASS)
annotation class Flags(val value: Array<Flag>) //必须使用val
AnnotationTarget描述
CLASS
ANNOTATION_CLASS注解类
TYPE_PARAMETER泛型形参
PROPERTY属性
FIELD字段、枚举常量
LOCAL_VARIABLE局部变量
VALUE_PARAMETER方法形参
CONSTRUCTOR构造方法
FUNCTION方法
PROPERTY_GETTER属性的获取方法
PROPERTY_SETTER属性的设置方法
TYPE类型
EXPRESSION表达式
FILE源文件
TYPEALIAS类型别名
AnnotationRetention描述
SOURCE源码可见,编译后无法获取
BINARY编译后可见,运行时无法获取
RUNTIME运行时可以获取

位置注解

更精确的添加注解。

@file:Flag//将Flag注解添加到源码

@Target(
    AnnotationTarget.FILE,
    AnnotationTarget.VALUE_PARAMETER,
    AnnotationTarget.LOCAL_VARIABLE,
    AnnotationTarget.PROPERTY,
    AnnotationTarget.PROPERTY_GETTER,
    AnnotationTarget.PROPERTY_SETTER,
    AnnotationTarget.FIELD,
    AnnotationTarget.FUNCTION
)
annotation class Flag

class Person {

    //根据Target注解顺序指定
    //优先级:参数->属性->字段
    //get添加到getId$annotations
    @Flag
    var id: Int = 0

    @field:Flag //字段上添加Flag注解
    @property:Flag//属性上添加Flag注解
    @get:Flag //getXXX方法上添加Flag注解
    @set:Flag //setXXX方法的参数上添加Flag注解
    @setparam:Flag//setXXX方法上添加Flag注解
    var name: String = ""

    @delegate:Flag//属性委托的XXX$delegate字段上添加Flag注解
    val age: Int by lazy { 18 }
}

//在被扩展的实例上(第一个参数)添加Flag注解
fun @receiver:Flag Person?.toJson(): String {
    return "{id=$this.id, name=${this?.name}, age=${this?.age}}"
}

Java注解

注解名作用
@VolatileJava关键字volatile
@StrictfpJava关键字strictfp
@JvmName改变由kotlin生成的java方法或字段的名称
@JvmStatic用在对象声明或者伴生对象的方法上,把它们暴露成java的静态方法
@JvmOverload指导kotlin编译器为带默认参数值的函数生成多个重载函数
@JvmField将属性暴露成一个没有访问修饰符的公有java字段
@Synchronized为被标记的方法添加线程同步锁

反射

需要引入kotlin-reflect依赖。

ORM框架

import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.findAnnotation

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Table(val name: String = "")

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class ID(val name: String = "")

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Field(val name: String = "")

interface IDao<T> {
    fun add(): Boolean
    fun delete(): Boolean
    fun update(): Boolean
    fun select(): Boolean
}

abstract class Dao<T> : IDao<T> {
    override fun add(): Boolean {
        val kc = javaClass.kotlin //获取当前实体的类,Dao类的实现对象
        val tbName = kc.findAnnotation<Table>()?.name ?: javaClass.simpleName
        val map = linkedMapOf<String, Any?>().apply {
            kc.declaredMemberProperties.filter {
                it.annotations.isNotEmpty()
            }.forEach {
                it.findAnnotation<ID>()?.let { anno ->
                    put(anno.name, it.get(this@Dao))
                    return@forEach
                }
                it.findAnnotation<Field>()?.let { anno ->
                    put(anno.name, it.get(this@Dao))
                    return@forEach
                }
            }
        }
        val fields = map.keys.joinToString(",")
        val values = map.values.joinToString(",")
        val sql = "insert into $tbName ($fields) values ($values)"
        println(sql) //by JDBC send SQL to DB
        return true
    }

    override fun delete(): Boolean = false
    override fun update(): Boolean = false
    override fun select(): Boolean = false
}

//使用以上框架,用注解标记实体类,让实体类可以和框架进行交互
@Table("书")
class Book(
    @ID("编号") val id: Int,
    @Field("书名") val name: String,
    @Field("作者") var author: String
) : Dao<Book>()

fun main() {
    Book(1, "TAOCP", "knuth").add()
}

ClassKClass

import kotlin.reflect.KClass
import kotlin.reflect.full.createInstance
import kotlin.reflect.full.primaryConstructor

class Book(val id: Int, val name: String, var author: String)

fun main() {
    //获取类
    var kc: KClass<Book> = Book::class
    //kotlin、java的class转换
    var jc: Class<Book> = Book::class.java
    var kcName = Class.forName("java.lang.Object").kotlin

    //创建对象
    val obj = kcName.createInstance()
    //使用一级构造方法创建对象
    var book = kc.primaryConstructor?.call(1, "书名", "作者名") as Book

    //使用对象获取类
    var c = book::class
    jc = book.javaClass

    //获取构造方法,创建对象
    val constructor = ::Book
    book = constructor(1, "书名", "作者名")

    //获取对象的属性的值
    var name = Book::name.get(book)
    name = book::name.get()

    //修改对象的属性值
    Book::author.set(book, name)
    book::author.set(name)
}

函数类型

将函数作为字段,这个字段的类型为函数类型

Lambda表达式

本质是匿名内部类(只有一个抽象方法的函数式接口)。

为了将表达式当作变量进行传递时使用,避免手动创建多余的类。

//常量gcd是函数类型((Int, Int) -> Int)
//a, b是形参,形参 -> 语句(表达式)
val gcd: (Int, Int) -> Int = lambda@{ a, b ->
    if (a == 0 || b == 0) return@lambda 0
    var t: Int
    var max: Int = kotlin.math.max(a, b)
    var min: Int = kotlin.math.min(a, b)
    while (max % min != 0) {
        t = max % min
        max = min
        min = t
    }
    return@lambda min
}

fun main() {
    val n = gcd(16, 60)
    println(n)
}

//只有一个参数it的Lambda表达式,有5种写法
val list = listOf(1, 2, 3)
list.forEach({ it -> println(it) })
list.forEach({ println(it) })
list.forEach() { println(it) }
list.forEach { println(it) }
list.forEach(::println)
fun main() {
    //Java基本类型使用Ref.IntRef类型封装
    //其他类型使用Ref.ObjectRef类型封装
    var n = 0
    val inc = {
        n++ //lambda内部访问外部变量
    }
    inc()
    println(n) //1
}

高阶函数

把Lambda表达式当作参数的函数。

//max是类型C的扩展(高阶)函数,返回类型R
//f是类型C的扩展函数,返回类型R
//调用函数f,传入两个I类型的变量,经过外部自定义的语句执行,返回类型R
fun <C, I, R> C.max(i1: I, i2: I, f: C.(a: I, b: I) -> R): R = f(i1, i2)

fun main() {
    //为String类型扩展了max函数
    //传入两个Int进行比较
    //自定义比较的函数当作参数传入
    val n = String.max(12, 15) { a, b -> if (a > b) a else b }
    println(n)
}

内联函数

属性使用const修饰可以成为常量,调用时由于已经初始化赋值,编译器可以直接拷贝值到调用处。

方法如果需要以拷贝的方式替换到调用处,可以使用内联函数。

inline

Lambda表达式作为函数的参数时,本质是创建了匿名内部类,然后创建出临时对象,传入形参。

为了避免性能损耗,可以使用内联函数,将代码拷贝到调用处展开,减少方法调用栈的层数(性能损耗忽略不计)。

如果内联函数的方法体太大,会导致字节码文件过大。

fun main() {
    for (i in 1..100) {
        task({ println("准备") }, { println("善后") })
    }
}

inline fun task(preTask: () -> Unit, postTask: () -> Unit): Unit {
    preTask()
    println("执行")
    postTask()
}

编译时代码块替换到调用的位置,不用直接调用task

...
println("准备")
println("执行")
println("善后")
...

如果不加inline,每次调用task方法,都会创建函数对象(kotlin.jvm.functions.Function0)传入task方法中用完就丢,不适合频繁调用的场景。

//特殊的使用场景(去掉调用方法前边的限定类)
import java.lang.Math as M
public inline fun min(a: Int, b: Int): Int = M.min(a, b)
public inline fun max(a: Int, b: Int): Int = M.max(a, b)
fun main() {
    println(max(1, 2))
}

noinline

如果只是将代码块替换到调用的位置,会有意外情况出现。

fun main() {
    val task = task({ println("准备") }, { println("善后") })
    task.invoke()
}

inline fun task(preTask: () -> Unit, noinline postTask: () -> Unit): () -> Unit {
    preTask()
    println("执行")
    postTask()
    //去掉return的"postTask"替换到调用位置后,成为孤立的存在
    //在形参上加上"noinline",就不参与内联,形参被保留
    return postTask
}

关闭内联优化,使函数类型的形参可以当作对象继续使用。

crossinline

fun main() {
    task(
        {
            println("准备")
            //不会中断后续的执行
            return@task
        },
        {
            println("善后")
            //内联后会中断后续的执行
            //添加"crossinline"后,这里不能直接“return”
            /*return*/
            //避免外部调用时错误的使用return
            return@task
        }
    )
    println("结束")
}

inline fun task(preTask: () -> Unit, crossinline postTask: () -> Unit) {
    preTask()
    println("执行")
    postTask()
}

Lambda表达式里不允许使用return,除非是内联函数的参数,这样参数必须加crossinline关键字,使其可以被间接调用。

使用内联函数的注意事项

reified

泛型的类型在编译后会被类型擦除。为了避免无法进行正常的类型判断,使用reified标记,函数必须是内联函数。

open class Animal //动物
open class Dog : Animal() //狗
open class Shiba : Dog()//柴犬

//必须使用内联函数,方法体将拷贝到调用处,将T替换为正确的类型,避免被编译时类型擦除
inline fun <reified T> get(list: List<Animal>): T? {
    //类型擦除T后,这里无法判断
    list.forEach { t -> if (t is T) return t }
    return null
}

fun main() {
    val list = listOf<Animal>(Dog(), Shiba())
    val dog = get<Dog?>(list)
    println(dog)
    val shiba = get<Shiba?>(list)
    println(shiba)
}

this

class A {
    inner class B {
        fun f1() {
            this@A.ptl("1") //类A
            this@B.ptl("2") //类A$B
        }
        val f2 = { s: String ->
            this.ptl("3") //A$B
        }
    }
    fun f() {
        val b = B()
        b.f1()
        b.f2("123")
    }
}
fun f() {
    val f = fun String.() {
        this.ptl("4") //abc
    }
    f("abc")
}
fun Int.f() {
    this.ptl("5") //1
    this@f.ptl("6") //1
}

fun main() {
    val a = A()
    a.f()
    f()
    1.f()
}
fun Any?.ptl(s: String) {
    println("$s $this")
}

Kotlin与Java互动

Kotlin 中调用 Java - Kotlin 语言中文站 (kotlincn.net)

Kotlin调用Java

关键字冲突
import org.jetbrains.annotations.NotNull;

import java.util.concurrent.Callable;

public class Main {
    @NotNull //使用JSR-305限制null
    public static String when(@NotNull Callable call) throws Exception {
        call.call();
        return "OK";
    }
}
fun main() {
    //when在Java中不是关键字,在Kotlin中是关键字
    //需要使用反引号修饰
    //返回类型可能为null,必须确认这个可能
    val str: String? = Main.`when` { println("Hello Java") }
    println(str)
}

避免使用Any的扩展函数,读取代码时很难分清调用的是哪个方法。

bean规范
public class User {
    private String name;
    private boolean admin;
    private boolean student;

    public void setName(String name) {
        this.name = name;
    }

    public void hasAdmin(boolean admin) {
        this.admin = admin;
    }

    public void setStudent(boolean student) {
        this.student = student;
    }

    public String getName() {
        return name;
    }

    public boolean isAdmin() {
        return admin;
    }

    public boolean getStudent() {
        return student;
    }
}
fun main() {
    val u = User()
    //使用get和set,直接使用属性名进行访问
    u.name = "张三"
    println(u.name)
    //使用is和has,以Getter的方法名为主
    u.hasAdmin(true)
    println(u.isAdmin)
    //使用get和set
    u.student = true
    println(u.student)
    //由于Kotlin规定所有属性都是可读,只有Setter的属性不会出现
}

建议使用getXxxsetXxx的命名规范。

运算符重载
import java.math.BigInteger;

public class Integer {
    private final BigInteger b;

    public Integer(BigInteger b) {
        this.b = b;
    }

    //Kotlin的运算符重载,自动匹配了这个方法
    public Integer plus(Integer other) {
        return new Integer(b.add(other.b));
    }

    @Override
    public String toString() {
        return b.toString(10);
    }

    public String toString(int radix) {
        return b.toString(radix);
    }
}
import java.math.BigInteger

fun main() {
    val a = Integer(BigInteger.ONE)
    val b = Integer(BigInteger.TWO)
    val c = a + b
    println(c)//3
}
函数表达式的传递问题
fun main() {
    val opt = Optional<Runnable>();
    val lambda: () -> Unit = {
        println("Hello")
    }
    //SAM转换的存在,每次都会将Lambda表达式封装为对象,值不一样
    println(lambda.hashCode())
    opt.add(lambda)
    opt.add(lambda)
    opt.add(lambda)
    opt.remove(lambda)
    opt.remove(lambda)
    opt.remove(lambda)
}
import java.util.ArrayList;

public class Optional<T> {
    private final ArrayList<T> list = new ArrayList<>();

    public void add(T t) {
        list.add(t);
        System.out.println("add:" + t.hashCode() + ",size:" + list.size());
    }

    public void remove(T t) {
        list.remove(t);
        System.out.println("remove:" + t.hashCode() + ",size:" + list.size());
    }
}

Java调用Kotlin

类成员
data class Person(
    //var标记后,有get和set方法
    var name: String,
    @JvmField //标记后没有get和set方法,并且public
    var age: Int
)
public class Main {
    public static void main(String[] args) {
        Person p = new Person("张三", 16);
        System.out.println(p.getName());
        p.setName("三张");
        p.age = 17;
        System.out.println(p);
        //调用kotlin时需要注意null
    }
}
单例
object Person{
    fun hello(){
        println("Hello Java")
    }
}
public class Main {
    public static void main(String[] args) {
        Person.INSTANCE.hello();
    }
}
默认参数
object Overloads {
    //加上注解后就拥有了多个重载的方法
    @JvmOverloads
    fun sum(a: Int, b: Int = 0, c: Int = 0): Int {
        return a + b + c
    }
}
public class Main {
    public static void main(String[] args) {
        int r = Overloads.INSTANCE.sum(1, 2, 3);
        System.out.println(r);
        r = Overloads.INSTANCE.sum(5);
        System.out.println(r);
    }
}
包方法
//Package.kt
fun hello() {
    println("Hello")
}
public class Main {
    public static void main(String[] args) {
        PackageKt.hello();
    }
}
扩展方法
//Package.kt
fun String.isEmpty(): Boolean {
    return this == ""
}
public class Main {
    public static void main(String[] args) {
        boolean b = PackageKt.isEmpty("");
        System.out.println(b);
    }
}
internal

在Kotlin规则中,被internal修饰后,跨越模块则不能访问。如果被Java调用,修饰Kotlin的类,对于Java处处可用,修饰Kotlin的类成员,无法访问。

协程

Kotlin的协程本质还是线程。不用过多关心线程也可以写出复杂的并发操作。在同一个代码块里进行灵活的线程切换。

使用协程需要引入kotlinx-coroutines依赖。

本质

协程是使用线程封装出的工具包框架。

fun main() = runBlocking {
    val exec = Executors.newSingleThreadScheduledExecutor()
    val task = Runnable { print("|") }
    repeat(10) {
        exec.schedule(task, 1, TimeUnit.SECONDS)
    }

    repeat(10) {
        launch {
            delay(1000L)
            print("-")
        }
    }
}

创建

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.Executors
import kotlin.concurrent.thread

fun main() = runBlocking {
    //创建线程
    Thread {
        Thread.sleep(5 * 1000L)
        println("${Thread.currentThread()} Thread")
    }.start()
    thread() {
        Thread.sleep(5 * 1000L)
        println("${Thread.currentThread()} thread")
    }
    val exec = Executors.newCachedThreadPool()
    exec.execute {
        Thread.sleep(5 * 1000L)
        println("${Thread.currentThread()} Executors")
    }
    //创建协程
    launch {
        delay(5 * 1000L)
        println("${Thread.currentThread()} 协程")
    }
    println("${Thread.currentThread()} main")
}

非阻塞式

所有的代码本质都是阻塞式的,最基本的一条汇编指令执行时都需要时间,而且执行时不能被其他汇编指令干扰,只是速度快到无法感知的纳秒级别,认为代码是非阻塞式的。如果遇到人可以感受卡顿,这时就认为是阻塞式代码导致的。

借助Kotlin语法优势,写出看似同步却是异步的过程,都在同一个代码块中,而且还不会“卡”当前线程,这就是非阻塞式。只要执行代码时不被耗时的过程卡住,创建个线程,把耗时过程交给另一个线程去做,当前线程就是非阻塞式。

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() {
    runBlocking {
        launch(Dispatchers.IO) {
            TODO("保存文件") //将任务切换到后台
        }
        launch(Dispatchers.Main) {
            TODO("更新数据") //将任务切换到前台
        }
        launch(Dispatchers.Default) {
            TODO("更新UI等复杂运算,其他调度器的任务")
        }
    }
}

将不同线程的代码写在同一个源文件中,避免大量的回调嵌套过程,甚至调用的顺序问题。

import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    //async:也是创建协程的方式
    //先获取name和先获取age不影响后续代码逻辑
    var name: String = ""
    async {
        //模拟网络IO阻塞,线程并没有等待
        //而是缓慢的处理(不停的循环判断结束的条件)
        /*api.getName(user)*/delay(5000L)
        name = "张三"
    }
    var age: Int = 0
    async {

        /*api.getAge(user)*/delay(3000L)
        age = 18
    }
    //主线程没有被阻塞,main直接把下面的代码挂起
    //交给上面两个线程执行完各自的过程后恢复,再走完下面的过程
    launch {
        val info = "{name=$name,age=$age}"
        println(info)
    }
    println("任务启动,等待结果")
}

协程的使用场景:当需要指定特定的线程去执行耗时的过程。

fun main() = runBlocking {
    launch(Dispatchers.Main) {
        //withContext:切换线程执行,然后自动切回原先的线程
        val data = withContext(Dispatchers.IO) {
            TODO("读取数据")
        }
        TODO("展示数据")
    }
}

suspend

当需要切换线程去执行时,如果将这部分代码封装成函数,需要加suspend关键字,成为挂起函数。

挂起函数中的过程主要是很耗时的计算或者IO操作,这些操作不适合主线程去完成,需要切换线程去执行(挂起的本质),执行完毕后再切回来(被挂起后的恢复)。

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch(Dispatchers.Default) {
        //被调用的挂起函数交给其他指定的线程执行,然后恢复后继续向下执行
        val data = loadData()
        println("${Thread.currentThread()} 展示数据开始") //step:3
        delay(3000L)
        println("${Thread.currentThread()} 展示数据完成") //step:3
    }
    println("${Thread.currentThread()} 任务启动完成") //step:1
}

//挂起函数,线程调用时不会被阻塞,继续往下执行
//线程切换是在withContext代码中,suspend只是起对调用者的提醒作用
//提醒这是个很耗时的过程,在主线程谨慎调用,避免卡顿
suspend fun loadData(): Any = withContext(Dispatchers.IO) {
    println("${Thread.currentThread()} 加载数据开始") //step:2
    delay(3000L)
    println("${Thread.currentThread()} 加载数据完成") //step:2
}

挂起函数必须在协程里面被调用,或者在另一个挂起函数里被调用,这样执行完挂起函数的时候,可以自动把线程切回来。如果在普通线程里调用,执行完就切不回来了(无法恢复被挂起的线程)。

非阻塞式

所有的代码本质都是阻塞式的,最基本的一条汇编指令执行时都需要时间,而且执行时不能被其他汇编指令干扰,只是速度快到无法感知的纳秒级别,认为代码是非阻塞式的。如果遇到人可以感受卡顿,这时就认为是阻塞式代码导致的。

借助Kotlin语法优势,写出看似同步却是异步的过程,都在同一个代码块中,而且还不会“卡”当前线程,这就是非阻塞式。只要执行代码时不被耗时的过程卡住,创建个线程,把耗时过程交给另一个线程去做,当前线程就是非阻塞式。

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() {
    runBlocking {
        launch(Dispatchers.IO) {
            TODO("保存文件") //将任务切换到后台
        }
        launch(Dispatchers.Main) {
            TODO("更新数据") //将任务切换到前台
        }
        launch(Dispatchers.Default) {
            TODO("更新UI等复杂运算,其他调度器的任务")
        }
    }
}

将不同线程的代码写在同一个源文件中,避免大量的回调嵌套过程,甚至调用的顺序问题。

import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    //async:也是创建协程的方式
    //先获取name和先获取age不影响后续代码逻辑
    var name: String = ""
    async {
        //模拟网络IO阻塞,线程并没有等待
        //而是缓慢的处理(不停的循环判断结束的条件)
        /*api.getName(user)*/delay(5000L)
        name = "张三"
    }
    var age: Int = 0
    async {

        /*api.getAge(user)*/delay(3000L)
        age = 18
    }
    //主线程没有被阻塞,main直接把下面的代码挂起
    //交给上面两个线程执行完各自的过程后恢复,再走完下面的过程
    launch {
        val info = "{name=$name,age=$age}"
        println(info)
    }
    println("任务启动,等待结果")
}

协程的使用场景:当需要指定特定的线程去执行耗时的过程。

fun main() = runBlocking {
    launch(Dispatchers.Main) {
        //withContext:切换线程执行,然后自动切回原先的线程
        val data = withContext(Dispatchers.IO) {
            TODO("读取数据")
        }
        TODO("展示数据")
    }
}

suspend

当需要切换线程去执行时,如果将这部分代码封装成函数,需要加suspend关键字,成为挂起函数。

挂起函数中的过程主要是很耗时的计算或者IO操作,这些操作不适合主线程去完成,需要切换线程去执行(挂起的本质),执行完毕后再切回来(被挂起后的恢复)。

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch(Dispatchers.Default) {
        //被调用的挂起函数交给其他指定的线程执行,然后恢复后继续向下执行
        val data = loadData()
        println("${Thread.currentThread()} 展示数据开始") //step:3
        delay(3000L)
        println("${Thread.currentThread()} 展示数据完成") //step:3
    }
    println("${Thread.currentThread()} 任务启动完成") //step:1
}

//挂起函数,线程调用时不会被阻塞,继续往下执行
//线程切换是在withContext代码中,suspend只是起对调用者的提醒作用
//提醒这是个很耗时的过程,在主线程谨慎调用,避免卡顿
suspend fun loadData(): Any = withContext(Dispatchers.IO) {
    println("${Thread.currentThread()} 加载数据开始") //step:2
    delay(3000L)
    println("${Thread.currentThread()} 加载数据完成") //step:2
}

挂起函数必须在协程里面被调用,或者在另一个挂起函数里被调用,这样执行完挂起函数的时候,可以自动把线程切回来。如果在普通线程里调用,执行完就切不回来了(无法恢复被挂起的线程)。

如果在挂起函数中没有线程切换(withContext)的操作,这个函数将只能在协程里被调用(必须加suspend关键字)。

同步

@Volatile
var count: Int = 0

@Synchronized
fun count() {
    synchronized(count) {
        count++
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

紫蝶冰澜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值