Kotlin分享

为什么使用 kotlin

简洁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
使用一行代码创建一个包含 getters、 setters、 `equals()`、 `hashCode()`、 `toString()` 以及 `copy()`
*/

data class Customer(val name: String, val email: String, val company: String)

// 或者使用 lambda 表达式来过滤列表:

val positiveNumbers = list.filter { it > 0 }

// 想要单例?创建一个 object 就可以了:

object ThisIsASingleton {
val companyName: String = "JetBrains"
}

安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
彻底告别那些烦人的 NullPointerException
*/
var output: String
output = null // 编译错误

// Kotlin 可以保护你避免对可空类型进行误操作

val name: String? = null // 可空类型
println(name.length()) // 编译错误

// 并且如果类型检测正确,编译器会为你做自动类型转换

fun calculateTotal(obj: Any) {
if (obj is Invoice)
obj.calculateTotal()
}

互操作性

Kotlin 可以使用 JVM 上的任何现有库,100%兼容
Java 可直接调用 Kotlin 写的文件

学习语法

官网

和java对比

注意点

java 和 kotlin 互相调用时,不可空校验就在 java 上失效了,所以 Java 调用 kotlin 时要判空。
或者将类型定义成可空。
java 返回 null 给 kotlin 也可以定义定义可空类型,不要定义不可 null 类型

https://blog.csdn.net/Goals1989/article/details/126964125
== :可以对基本数据类型以及 String 等类型进行内容比较,相当于 Java 中的 equals
=== :对引用的内存地址进行比较,相当于 Java 中的 ==

Elvis 操作符
可以通过 ?: 的操作来简化 if null 的操作

1
2
3
4
5
6
7
8

// lesson.date 为空时使⽤用默认值
val date = lesson.date?: "⽇日期待定"
// lesson.state 为空时提前返回函数
val state = lesson.state?: return
// lesson.content 为空时抛出异常
val content = lesson.content ?: throw IllegalArgumentException("content
expected")
  • kotlin 默认不能空,变量类型后面跟? 号定义,表明这是一个可空类型
  • ?. 代表着如果该类型为空的话就返回 null 不做后续的操作,如果不为空的话才会去访问对应的方法或者属性
  • !!. 代表着如果该类型为空的话就抛出 NullPointerException,如果不为空就去访问对应的方法或者属性,所以只有在很少的特定场景才用这种符号,代表着程序不处理这种异常的 case 了,会像 java 代码一样抛出 NullPointerException。而且代码中一定不用出现下面这种代码,会让代码可读性很差而且,如果有空指针异常,我们也不能马上发现是哪空了:
1
2
3
4
5
6
7
8
9
10
/*
* 不推荐这样的写法:链式的连续用!!.
* */
val user = User()
user!!.name!!.subSequence(0,5)!!.length


if (view != null) {
//view.setBackgroundColor(Color.RED) // 这样写不对,因为在多线程中也可能报错,需要在view后加上?
view?.setBackgroundColor(Color.RED)

使用 lateinit 关键字修饰属性,您可以将属性的初始化推迟到稍后的时间点。这样,您可以声明一个非空的属性,但在构造函数中可以不进行初始化。然后,在稍后的某个时间点,通过对该属性进行赋值操作来完成初始化。搭配 byLazy 使用,否则别使用。

lateinit 关键字只能用于 var(可变)属性,并且只能用于非空类型。它通常用于在某些情况下延迟初始化属性,例如在依赖注入框架中。

如果想要带着 index 遍历集合的话:

1
2
3
4
5
 for ((index, element) in newList?.withIndex()!!) {
if(index==6){
break
}
}

构造函数


使⽤类名:: class 获取的是 Kotlin 的类型是 KClass
使⽤类名:: class. java 获取的是 Java 的类型
Kotlin 的顶层⽗父类是 Any ,对应 Java 当中的 Object ,但是比 Object 少了wait ()/notify ()等函数

Unit
Kotlin 中的 Unit 对应 Java 中的 void
在 Java 中通过 「类名. this」 获取⽬标类引⽤
在 Kotlin 中通过「this@类名」获取目标类引⽤

if 不仅是是条件语句,也是表达式,可以接收参数,返回最后一行

1
2
3
4
5
6
val mode = if (args.isNotEmpty() && args[0] == "1") {
DEBUG
} else {
USER
}

无论是初始化代码块还是伴生对象的初始化块,它们都会在构造函数之前执行
首先执行初始化代码块,然后执行伴生对象的初始化块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyClass {
init {
// 初始化代码块
// 在构造函数之前执行
// 初始化实例变量或执行其他实例相关的操作
}

companion object {
init {
// 伴生对象的初始化代码块
// 在类的初始化块之后执行
// 初始化伴生对象的静态变量或执行其他静态操作
}
}

constructor() {
// 构造函数
// 对象创建时执行的逻辑
}
}

构造函数
不管什么构造方法,都是先执行 init 模块逻辑,后执行构造方法的逻辑

伴生对象

在 Kotlin 中,虽然伴生对象的行为类似于某些编程语言中的静态成员,但它们并不是真正的静态。Kotlin 中没有直接的静态成员概念,而是使用伴生对象来模拟类级别的静态成员。

在创建对象时,首先会执行类的初始化代码块,然后才会执行伴生对象的初始化块。这是因为伴生对象的初始化在类的实例化过程中被视为类的一部分。

虽然伴生对象的初始化块在概念上类似于静态初始化块,但它们不是在类加载时执行的。相反,它们在首次访问伴生对象或通过创建对象时执行。

因此,在 Kotlin 中,初始化代码块先于伴生对象的初始化块执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class MyClass {
public static final class Companion {
private static final int someProperty = 42;

public final void someFunction() {
System.out.println("Hello from companion object");
}
}

public static final Companion Companion = new Companion();

private MyClass() {
}
}

companion object 被编译为一个名为 Companion 的静态内部类,它包含伴生对象的成员变量和方法。
外部类加载后才会加载内部类,类似静态内部单例

扩展函数

在编译时,编译器会将 obj.extensionFunction () 转换为 MyClass.extensionFunction (obj) 的静态函数调用。

1
2
3
4
5
6
7
8
9
10
11
fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, length).show()
}

toast("hello")

//扩展函数,我们就可以在每一个Activity中直接使用toast()函数了。
fun Context.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, duration).show()
}

常见操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val list = listOf<Int>(1, 2, 3, 5, 10, 8, 2)
val newList = ArrayList<Int>();
list.forEach {
val newElement = it * 2 + 3
newList.add(newElement)
}
//和上面一样,上面麻烦,map可以对集合进行操作,返回一个修改过得集合
val newList2 = list.map { it * 2 + 3 }
newList2.forEach(::println)
newList2.map(::println) //和上面输出一样,但是又重新add了一个集合,不好,纯粹用于迭代的话会影响性能,实现里面还有一个数组

//提取开头指定数量或符合指定条件的子集
list.takeWhile { it <= 3 }.forEach(::println) //小于的去掉
list.forEach {
if (it % 2 == 0) {
println(it)
}
}
list.filter { it.isEvent() }.forEach(::println)//过滤

take 是从集合中取前几个元素
takeLast 是从集合中取后几个元素
sortedBy 排序
过滤 list,符合过滤条件的就是过滤结果
filterNot 把符合条件的过滤掉,剩下的是结果。这个操作和 filter 相反
slice, 取集合中的某一部分
takeIf: 避免在某些情况下使用冗长的 if-else 语

List 防止崩溃取值方式
getOrElse () 提供用于计算默认值的函数,如果集合中不存在索引,则返回默认值。
getOrNull () 返回 null 作为默认值。
joinToString 字符串拼接

kotlin 自带 copy 方法
需要更新 State 时,借助 data class 的 copy 方法可以快捷地拷贝构造一个新实例。

难点

高阶函数

高阶函数是将函数用作参数或返回值的函数, 还可以把函数赋值给一个变量。

所有函数类型都有一个圆括号括起来的参数类型列表以及一个返回类型:(A, B) -> C 表示接受类型分别为 A 与 B 两个参数并返回一个 C 类型值的函数类型。参数类型列表可以为空,如 () -> A,Unit 返回类型不可省略。

(Int) -> String

函数类型表示法可以选择性地包含函数的参数名:(x: Int, y: Int) -> Point。这些名称可用于表明参数的含义。
(Button, ClickEvent) -> Unit
如需将函数类型指定为可空,请使用圆括号:((Int, Int) -> Int)?

1
2
3
4
5
6
7
8
fun a(funParam: (Int) -> String): String {
return funParam(1)
}

fun b(param: Int): String {
return param.toString()
}

调用

1
2
3
4
5
6
a(::b)
var d = ::b
b(1) // 调用函数
d(1) // 实际上会调用 d.invoke(1)
(::b)(1) // 用对象 :: b 后面加上括号来实现 b() 的等价操作, 实际上会调用 (::b).invoke(1)
b.invoke(1) // 报错

对象是不能加个括号来调用的,但是函数类型的对象可以。为什么?因为这其实是个假的调用,它是 Kotlin 的语法糖,实际上你对一个函数类型的对象加括号、加参数,它真正调用的是这个对象的 invoke () 函数

双冒号

:: 创建一个函数引用或者一个类引用

函数引用

1
fun isOdd(x: Int) = x % 2 != 0

我们可以很容易地直接调用它(isOdd (5)),但是我们也可以将其作为一个函数类型的值,例如将其传给另一个函数。为此,我们使用 :: 操作符:

1
2
3
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd))

这里 :: isOdd 是函数类型 (Int) -> Boolean 的一个值。

在类外面,如果我们需要使用类的成员函数或扩展函数,它需要是限定的,例如 String:: toCharArray。

1
2
3
4
5
6
7
8
9
10
11
val args: Array<String> = arrayOf("1", "2")
args.filter(String::isNotEmpty)

class PdfPrinter {
fun println(any: Any) {
kotlin.io.println(any) //重名了可以用包名调用
}
}

val pdfPrinter = PdfPrinter()
args.forEach(pdfPrinter::println)

类引用

1
val c = MyClass::class

该引用是 KClass 类型的值
请注意,Kotlin 类引用与 Java 类引用不同。要获得 Java 类引用,请在 KClass 实例上使用 .java 属性。
平时写的类,其信息都可以在这个 KClass 来获取

属性引用

1
2
3
4
5
6
7
8
9
10
11
12
data class MediaItem(val title: String, val url: String)

var items= mutableListOf<MediaItem>()
items
.sortedBy { it.title }
.map { it.url }
.forEach { print(it) }

items
.sortedBy(MediaItem::title)
.map(MediaItem::url)
.forEach(::println)

匿名函数

没有名字的函数
要传一个函数类型的参数,或者把一个函数类型的对象赋值给变量,除了用双冒号来拿现成的函数使用,你还可以直接把这个函数挪过来写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun b(param: Int): String {
return param.toString()
}

a(fun b(param: Int): String {
return param.toString()
});

val d = fun b(param: Int): String {
return param.toString()
}

//名字没意义,省略
a(fun(param: Int): String {
return param.toString()
});
val d = fun(param: Int): String {
return param.toString()
}

如果你在 Java 里设计一个回调的时候是这么设计的:

1
2
3
4
5
6
public interface OnClickListener {
void onClick(View v);
}
public void setOnClickListener(OnClickListener listener) {
this.listener = listener;
}

使用的时候是这么用的:

1
2
3
4
5
6
view.setOnClickListener(new OnClickListener() {
@Override
void onClick(View v) {
switchToNextPage();
}
});

kotlin 写法

1
2
3
4
5
6
7
fun setOnClickListener(onClick: (View) -> Unit) {
this.onClick = onClick
}
view.setOnClickListener(fun(v: View): Unit) {
switchToNextPage()
})

Lambda 写法:

1
2
3
view.setOnClickListener({ v: View ->
switchToNextPage()
})

Lambda 表达式

简化匿名函数,代码更简洁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
view.setOnClickListener({ v: View ->
switchToNextPage()
})

//如果 Lambda 是函数的最后一个参数,你可以把 Lambda 写在括号的外面:
view.setOnClickListener() { v: View ->
switchToNextPage()
}
//而如果 Lambda 是函数唯一的参数,你还可以直接把括号去了:
view.setOnClickListener { v: View ->
switchToNextPage()
}
//另外,如果这个 Lambda 是单参数的,它的这个参数也省略掉不写:
//根据上下文推导,根据最后一行代码来推断出返回值类型
view.setOnClickListener {
switchToNextPage()
}

Lambda 表达式的完整语法形式如下:

1
2
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
val sum = { x: Int, y: Int -> x + y }

多参数例子:
fold 函数:将所提供的操作应用于集合元素并返回累积的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val items = listOf(1, 2, 3, 4, 5)

// Lambdas 表达式是花括号括起来的代码块。
items.fold(0, {
// 如果一个 lambda 表达式有参数,前面是参数,后跟“->”
acc: Int, i: Int ->
print("acc = $acc, i = $i, ")
val result = acc + i
println("result = $result")
// lambda 表达式中的最后一个表达式是返回值:
result
})

// lambda 表达式的参数类型是可选的,如果能够推断出来的话:
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })

输出:

1
2
3
4
5
6
7

acc = 0, i = 1, result = 1
acc = 1, i = 2, result = 3
acc = 3, i = 3, result = 6
acc = 6, i = 4, result = 10
acc = 10, i = 5, result = 15
joinedToString = Elements: 1 2 3 4 5

总结

函数不能直接传递或者赋给某个变量,需要函数类型实例化,有三种方式:

使用已有声明的可调用引用

  1. 函数引用

使用函数字面值的代码块

  1. 匿名函数
  2. lambda 表达式

Kotlin 的高阶函数、匿名函数和 Lambda 表达式

例子

实现接口

1
2
3
4
5
6
7
var onVideoStartCallBack: (() -> Unit)? = null

onVideoStartCallBack?.invoke()

videioView.onVideoStartCallBack = {

}

函数里实现接口

1
2
3
4
5
6
7
8
9
10
11
object UploaderListHelper {

fun startTaskUpload(activity: Activity, startCallBack: ((Int) -> Unit)?) {
startCallBack.invoke(position)
}
}

UploaderListHelper.startTaskUpload(activity) {
refreshProgress(it)
}

闭包就是 Lambda 表达式
lambda 表达式,通常是在需要一个函数,但是又不想去命名一个函数的场合下使用,也就是匿名函数。而 Lambda 可以简化匿名函数,使代码更加简洁。也可以将一个代码块赋值给一个变量,在调用方法的时候,可以直接将这个 lambda 表达式当作参数传递进去

kotlin 接口回调,省了个方法定义

1
2
3
4
5
6
7
private lateinit var delListener: ((Boolean, DiseaseBean) -> Unit)

fun setDelListener(delListener: ((Boolean, DiseaseBean) -> Unit)) {
this.delListener = delListener
}

delListener?.invoke(holder.btDel.isChecked, tagBean)

更简单,传个方法,接口都不用写了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mcDiagnosisType.showLayout {
if(it){ //it 就是boolean
mcDiseaseType.animateClose()
}
}

fun showLayout(callBack :(((Boolean)->Unit))?=null){
if(isShow){
animateClose()
callBack?.invoke(isShow)
}else{
animateOpen()
callBack?.invoke(isShow)
}
}

作用域函数

Kotlin 标准库包含几个函数,它们的唯一目的是在对象的上下文中执行代码块。当对一个对象调用这样的函数并提供一个 lambda 表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这些函数称为作用域函数。共有以下五种:let、run、with、apply 以及 also。

这些函数基本上做了同样的事情:在一个对象上执行一个代码块。不同的是这个对象在块中如何使用,以及整个表达式的结果是什么。
目的:简洁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val person = findPerson();
//person是可null的,所以需要?
println(person?.age)
println(person?.name)
//上面太麻烦,findPerson加了?,所以后面不需要了,减少的判空操作。let可以安全调用
findPerson()?.let { person ->
person.work()
println(person.age)
}
//还可以更简洁,person也不用写
findPerson()?.apply {
work()
println(age)
}

使⽤时可以通过简单的规则作出一些判断

返回自身

返回值是它本身
从 apply 和 also 中选

作⽤域中使⽤ this 作为参数选择 apply

1
2
3
4
5
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)

作⽤域中使⽤ it 作为参数选择 also

1
2
3
4
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")

with 非扩展函数

1
2
3
4
5
6
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}

不需要返回自身

从 run 和 let 中选择

作用域中使用 this 作为参数,选择 run
作用域中使用 it 作为参数,选择 let, 适合配合空判断的时候

1
2
3
4
5
6
7
8
9
10
11
12
val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}

// 同样的代码如果用 let() 函数来写:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}

==it 作为参数的好处==
let 允许我们自定义参数名字,使可读性更强,如果倾向可读性可以选择 T.let

委托机制

Kotlin 中的委托是一种抽象的设计模式,允许将工作委托给另一个对象处理。在 Kotlin 中委托分为两种类型: 类委托和属性委托

类委托

类委托的核心思想在于将一个类的具体实现委托给另一个类完成。通过使用类委托,可以将某些特定行为委托给其他类处理,从而实现代码的解耦和重用。类委托的使用害要创建实现了特定接口或继承了抽象类的类,并将其作为委托对象传递给需要处理的对象。

1
2
3
4
5
6
7
8
9
10
// 约束类
interface IGamePlayer {
// 打排位赛
fun rank()
// 升级
fun upgrade()
}

// 委托对象
class DelegateGamePlayer(private val player: IGamePlayer): IGamePlayer by player
1
2
3
4
5
6
7
// Client 场景测试
fun main() {
val realGamePlayer = RealGamePlayer("张三")
val delegateGamePlayer = DelegateGamePlayer(realGamePlayer)
delegateGamePlayer.rank()
delegateGamePlayer.upgrade()
}

属性委托

属性委托则是将属性的 getter 和 setter 方法逻辑委托给另一个类实现。属性委托允许将属性的读取和设置逻辑放在另一个地方处理,比如更改属性名称或延迟初始化等场景。通过使用属性委托,可以将属性的逻辑封装在一个类中并在需要时将其传递给其他对象使用

一文彻底搞懂Kotlin中的委托 - 掘金

val 属性实现 ReadOnlyPropertyvar 属性实现 ReadOnlyProperty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// val 属性委托实现
class Delegate1: ReadOnlyProperty<Any,String>{
override fun getValue(thisRef: Any, property: KProperty<*>): String {
return "通过实现ReadOnlyProperty实现,name:${property.name}"
}
}
// var 属性委托实现
class Delegate2: ReadWriteProperty<Any,Int>{
override fun getValue(thisRef: Any, property: KProperty<*>): Int {
return 20
}

override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) {
println("委托属性为: ${property.name} 委托值为: $value")
}

}
// 测试
class Test {
// 属性委托
val d1: String by Delegate1()
var d2: Int by Delegate2()
}

内联函数

内联函数 inline

高阶函数实现的原理:函数类型其实是生成了一个对象

使用 inline 能避免函数的 lambda 形参额外创建 Function 对像

  • kotlin 源码里使用了很多的内联函数,它的作用简单来说就是方便进行类型推导,能具体化类型参数。
  • 被 inline 标记的函数就是内联函数, 其原理就是:在编译时期, 把调用这个函数的地方用这个函数的方法体进行替换
    1
    2
    3
    4
    5
    6
    7
    8
    inline fun <T> method(lock: Lock, body: () -> T): T {
    lock.lock()
    try {
    return body()
    } finally {
    lock.unlock()
    }
    }

method (lock, {“我是 body 方法体”})//lock 是一个 Lock 对象

其实上面调用的方法, 在编译时期就会把下面的内容替换到调用该方法的地方, 这样就会减少方法压栈, 出栈, 进而减少资源消耗;

1
2
3
4
5
6
7

lock.lock()
try {
return "我是body方法体"
} finally {
lock.unlock()
}

也就是说 inline 关键字实际上增加了代码量,但是提升了性能,而且增加的代码量是在编译期执行的,对程序可读性不会造成影响。

inline 的坏处

  1. 对于一个 public 的 inline 方法,他不可以引用类的私有变量。
  2. inline 关键字对于 lambda 的处理有的时候不是我们想要的, inline 函数的形参 lambda 不能有 return 语句,避免 lambda 中的 return 影响外部程序流程
1
2
3
4
5
6
7
8
fun main(args: Array<String>) {
println("Start of main")
multiplyByTwo(5) {
println("Result is: $it")
return
}
println("End of main")} //不会执行,有问题

总结

  • 使用 inline,内联函数到调用的地方,能减少函数调用造成的额外开销,在循环中尤其有效
  • 使用 inline 能避免函数的 lambda 形参额外创建 Function 对象
  • 使用 noinline 可以拒绝形参 lambda 内联
  • 使用 crossinline 显示声明

inline 关键字的作用,是把 inline 方法以及方法中的 lambda 参数在编译期间复制到调用方,进而减少函数调用以及对象生成。对于有时候我们不想让 inline 关键字对 lambda 参数产生影响,可以使用 noline 关键字。如果想 lambda 也被 inline,但是不影响调用方的控制流程,那么就要是用 crossinline

unknown_filename.3

Lambda 表达式不允许使用 return,除非这个 Lambda 表达式是内联函数的参数。

(Function1)null. INSTANCE,是由于反编译器工具在找不到等效的 Java 类时的显示的结果。

Kotlin的inline noinline crossinline笔记 - 简书

Kotlin inline noinline crossinline 解答 - 掘金

Reified

  • 由于 Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。比如你不能检查一个对象是否为泛型类型 T 的实例,所以需要反射。
  • 而 reified,字面意思:具体化,其实就是具体化泛型
  • 主要还是有内联函数 inline, 才使得 kotlin 能够直接通过泛型就能拿到泛型的类型,只有内联函数的类型参数可以具体化。
    [[泛型#泛型擦除]]

例子

定义实现一个扩展函数启动 Activity,一般都需要传 Class< T > 参数:

1
2
3
4
5
6
7
// Function
private fun <T : Activity> Activity.startActivity(context: Context, clazz: Class<T>) {
startActivity(Intent(context, clazz))
}

// Caller
startActivity(context, NewActivity::class.java)

使用 reified,通过添加类型传递简化泛型参数

1
2
3
4
5
6
7
// Function
inline fun <reified T : Activity> Activity.startActivity(context: Context) {
startActivity(Intent(context, T::class.java))
}

// Caller
startActivity<NewActivity>(context)

Kotlin分享
http://peiniwan.github.io/2024/04/f5528598dcd3.html
作者
六月的雨
发布于
2024年4月6日
许可协议