- 官方教程 入门 · Kotlin 官方文档 中文版
- GPT咨询 loading
一、Android Studio 配置
apply plugin: 'kotlin-android'
buildscript {
ext.kotlin_version = '1.7.10'
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.7.2")
// 协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.2"
或者 下边这个是通用的Kotlin协程库,可以在多个平台上使用
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:xxx"
// 协程Android支持库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2"
implementation "androidx.activity:activity-ktx:1.2.2"
implementation "androidx.fragment:fragment-ktx:1.3.3"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
1.1 包与导入
源文件通常以包声明开头:
package org.example
fun printMessage() { /*......*/ }
class Message { /*......*/ }
// ......
- 源文件所有内容(无论是类还是函数)都包含在该包内。 所以上例中
printMessage()的全名是org.example.printMessage, 而Message的全名是org.example.Message。 - 如果没有指明包,该文件的内容属于无名字的默认包
1.1.1 Kotlin官方库
标准库
协程库
序列化库
测试库
日期时间库
有标准库的多个包会默认导入到每个 Kotlin 文件中:
- kotlin.*
- kotlin.annotation.*
- kotlin.collections.*
- kotlin.comparisons.*
- kotlin.io.*
- kotlin.ranges.*
- kotlin.sequences.*
- kotlin.text.*
根据目标平台还会导入额外的包:
- JVM:
-
- java.lang.*
- kotlin.jvm.*
- JS:
1.2 程序入口点
Kotlin 应用程序的入口点是 main 函数:
fun main() {
println("Hello world!")
}
main 的另一种形式接受可变数量的 String 参数:
fun main(args: Array<String>) {
println(args.contentToString())
}
print 将其参数打到标准输出
println 输出其参数并添加换行符,以便接下来输出的内容出现在下一行
1.3 源文件组织
鼓励多个声明(类、顶级函数或者属性)放在同一个 Kotlin 源文件中, 只要这些声明在语义上彼此紧密关联,并且文件保持合理大小 (不超过几百行)。
特别是在为类定义与类的所有客户都相关的扩展函数时, 请将它们放在与类自身相同的地方。而在定义仅对指定客户有意义的扩展函数时,请将它们放在紧挨该客户代码之后。避免只是为了保存 某个类的所有扩展函数而创建文件。
二、关键字
分号
尽可能省略分号
在 Kotlin 中,分号是可选的,因此换行很重要。语言设计采用 Java 风格的花括号格式,如果尝试使用不同的格式化风格,那么可能会遇到意外的行为。
(xx,xx)=--解构声明
有时把一个对象解构 成很多变量会很方便,例如:这种语法称为解构声明 。一个解构声明同时创建多个变量。 声明了两个新变量: name 和 age,并且可以独立使用它们:
val (name, age) = person
println(name)
println(age)
一个解构声明会被编译成以下代码:
val name = person.component1()
val age = person.component2()
其中的 component1() 和 component2() 函数是在 Kotlin 中广泛使用的约定原则的另一个示例
componentN() 函数需要用 operator 关键字标记,以允许在解构声明中使用它们 。
如果在解构声明中你不需要某个变量,那么可以用下划线取代其名称: 对于以这种方式跳过的组件,不会调用相应的 componentN() 操作符函数
val (_, status) = getResult()
你可以对 lambda 表达式参数使用解构声明语法。 如果 lambda 表达式具有 Pair 类型(或者 Map.Entry 或任何其他具有相应 componentN 函数的类型)的参数,那么可以通过将它们放在括号中来引入多个新参数来取代单个新参数:
map.mapValues { entry -> "${entry.value}!" }
map.mapValues { (key, value) -> "$value!" }
注意声明两个参数和声明一个解构对来取代单个参数之间的区别:
{ a //-> ...... } // 一个参数
{ a, b //-> ...... } // 两个参数
{ (a, b) //-> ...... } // 一个解构对
{ (a, b), c //-> ...... } // 一个解构对以及其他参数
==和===--相等性校验
- 结构 相等(
==------用equals()检测)
按照约定,像 a == b 这样的表达式会翻译成:
a?.equals(b) ?: (b === null)
fun main() {
var a = "hello"
var b = "hello"
var c = null
var d = null
var e = d
println(a == b)
// true
println(a == c)
// false
println(c == e)
// true
}
- 引用 相等(
===------检测两个引用指向同一对象)
引用相等由 ===(以及其否定形式 !==)操作判断。a === b 当且仅当 a 与 b 指向同一个对象时求值为 true:
fun main() {
var a = "Hello"
var b = a
var c = "world"
var d = "world"
println(a === b)
// true
println(a === c)
// false
println(c === d)
// true
}
- 如需比较两个数组是否具有相同顺序的相同元素,请使用 contentEquals()。
let\run\apply\also--作用域函数
Kotlin 标准库包含几个函数,它们的唯一目的是在对象的上下文中执行代码块。 当对一个对象调用这样的函数并提供一个 lambda 表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这些函数称为作用域函数 。 共有以下五种:let、 run、 with、 apply、 以及 also。
这些函数基本上都执行同样的操作:在一个对象上执行一个代码块。不同的是这个对象在块中如何使用,以及整个表达式的结果是什么。
|---------------------------------------------------------------------------------|----------|--------------|---------------|
| 函数 | 对象引用 | 返回值 | 是否是扩展函数 |
| let | it | Lambda 表达式结果 | 是 |
| run | this | Lambda 表达式结果 | 是 |
| run | - | Lambda 表达式结果 | 不是:调用无需上下文对象 |
| with | this | Lambda 表达式结果 | 不是:把上下文对象当做参数 |
| apply | this | 上下文对象 | 是 |
| also | it | 上下文对象 | 是 |
- 上下文对象:this 还是 it
-
- 在传入作用域函数的 lambda 表达式内部,上下文对象可以不使用其实际名称而是使用一个更简短的引用来访问。 每个作用域函数都使用以下两种方式之一来引用上下文对象:作为 lambda 表达式的接收者 (
this)或者作为 lambda 表达式的参数(it)。 - 两者都提供了同样的功能,因此我们会针对不同的场景描述两者的优缺点,并提供使用建议。
run、with以及apply通过关键字this将上下文对象引用为 lambda 表达式的接收者。 因此,在它们的 lambda 表达式中可以像在普通的类函数中一样访问上下文对象。在大多数场景,当你访问接收者对象时你可以省略this, 来让你的代码更简短。- 反过来,
let及also将上下文对象引用为 lambda 表达式参数。如果没有指定参数名,对象可以用隐式默认名称it访问。it比this简短,带有it的表达式通常更易读。 - 不过,当调用对象函数或属性时,不能像
this这样隐式地访问对象。 因此,当上下文对象在作用域中主要用作函数调用中的参数时,通过it访问上下文对象会更好
- 在传入作用域函数的 lambda 表达式内部,上下文对象可以不使用其实际名称而是使用一个更简短的引用来访问。 每个作用域函数都使用以下两种方式之一来引用上下文对象:作为 lambda 表达式的接收者 (
- 根据返回结果,作用域函数可以分为以下两类:
-
apply及also返回上下文对象。let、run及with返回 lambda 表达式结果.
以下是根据预期目的选择作用域函数的简短指南:
- 对一个非空(non-null)对象执行 lambda 表达式:
let - 将表达式作为变量引入为局部作用域中:
let - 对象配置:
apply - 对象配置并且计算结果:
run - 在需要表达式的地方运行语句:非扩展的
run - 附加效果:
also - 一个对象的一组函数调用:
with
let--闭包内非对象this而是it
data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}
fun main() {
//sampleStart
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
//sampleEnd
}
run--对象配置并且计算结果--闭包内是对象
当 lambda 表达式同时初始化对象并计算返回值时,run 很有用。
class MultiportService(var url: String, var port: Int) {
fun prepareRequest(): String = "Default request"
fun query(request: String): String = "Result for query '$request'"
}
fun main() {
//sampleStart
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}")
}
//sampleEnd
println(result)
println(letResult)
}
with--作用域对一个对象实例调用多个方法,快捷读--闭包内是对象作用域
class Turtle {
fun penDown()
fun penUp()
fun turn(degrees: Double)
fun forward(pixels: Double)
}
val myTurtle = Turtle()
with(myTurtle) { // 画一个 100 像素的正方形
penDown()
for (i in 1..4) {
forward(100.0)
turn(90.0)
}
penUp()
}
apply--作用域对一个对象的属性进行配置,快捷写-闭包内是对象作用域
val myRectangle = Rectangle().apply {
length = 4
breadth = 5
color = 0xFAFAFA
}
also--作用域对一个对象作为参数的操作--闭包内不是对象作用域是it
also 对于执行一些将上下文对象作为参数的操作很有用。 对于需要引用对象而不是其属性与函数的操作,或者不想屏蔽来自外部作用域的 this 引用时,请使用 also。
当你在代码中看到 also 时,可以将其理解为"并且用该对象执行以下操作"。
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
//sampleEnd
}
takeIf 与 takeUnless
除了作用域函数外,标准库还包含函数 takeIf 及 takeUnless。 这俩函数让你可以将对象状态检查嵌入到调用链中。
当在对象上调用谓词时,若满足给定的谓词,则 takeIf 返回此对象。 否则返回 null。因此,takeIf 是单个对象的过滤函数。
When using takeIf or takeUnless, the object is available as a lambda argument (it).
import kotlin.random.*
fun main() {
//sampleStart
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")
//sampleEnd
}
当在 takeIf 及 takeUnless 之后链式调用其他函数,不要忘记执行空检查或使用安全调用 (?.),因为他们的返回值是可为空的。
fun main() {
//sampleStart
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.uppercase()
//val caps = str.takeIf { it.isNotEmpty() }.uppercase() // 编译错误
println(caps)
//sampleEnd
}
*--数组展开操作符
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)
通常和可变入参一起使用
in--区间、遍历和集合包含检测
for (i in 1..100) { ...... } // 闭区间:包含 100
for (i in 1..< 100) { ...... } // 左闭右开区间:不包含 100
for (x in 2..10 step 2) { ...... }
for (x in 10 downTo 1) { ...... }
(1..10).forEach { ...... }
#生成区间
if (x in 1..y+1) {
println("fits in range")
}
for (x in 1..5) {
print(x)
}
for (x in 1..10 step 2) {
print(x)
}
#集合包含检测
val items = setOf("apple", "banana", "kiwifruit")
if("orange" in items){...}
::--指定对象的引用
-
在属性委托,判断属性是否延迟初始化完成,函数类型的参数传递方面都有使用
-
属性引用,函数引用,类引用
var student = student()
student::name// 函数引用也可以用于高阶函数调用,函数类型的参数传递
val product = items.fold(1, Int::times)
List<Int>::size//判断属性是否延迟初始化完成
if (foo::bar.isInitialized) {
println(foo.bar)
}//属性委托
var delegatedToMember: Int by this::memberInt
is--类型检测
when (x) {
is Foo -> ......
is Bar -> ......
else -> ......
}
as和as?--类型转换
#如果对象不是目标类型,那么常规类型转换可能会导致 ClassCastException
val x: String? = y as String?
#另一个选择是使用安全的类型转换,如果尝试转换不成功则返回 null:
val x: String? = y as? String
tailrec--声明尾递归函数,规避递归堆栈溢出
val eps = 1E-10 // "good enough", could be 10^-15
tailrec fun findFixPoint(x: Double = 1.0): Double =
if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))
object--对象表达式和单例模式
fun main() {
//sampleStart
val helloWorld = object {
val hello = "Hello"
val world = "World"
// object expressions extend Any, so `override` is required on `toString()`
override fun toString() = "$hello $world"
}
print(helloWorld)
//sampleEnd
}
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { ...... }
override fun mouseEntered(e: MouseEvent) { ...... }
})
fun countClicks(window: JComponent) {
var clickCount = 0
var enterCount = 0
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++
}
override fun mouseEntered(e: MouseEvent) {
enterCount++
}
})
// ......
}
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ......
}
val allDataProviders: Collection<DataProvider>
get() = // ......
}
companion--伴生对象,类内部的对象声明,单例模式
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
该伴生对象的成员可通过只使用类名作为限定符来调用:
val instance = MyClass.create()
可以省略伴生对象的名称,在这种情况下将使用名称 Companion:
class MyClass {
companion object { }
}
val x = MyClass.Companion
inline,noinline--声明内联函数,节省高阶函数内存占用
inline fun <T> lock(lock: Lock, body: () -> T): T { ...... }
-
如果不希望内联所有传给内联函数的 lambda 表达式参数都内联,那么可以用 noinline****修饰符标记不希望内联的函数参数:
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { ...... }
value class--内联类
To declare an inline class, use the value modifier before the name of the class:
value class Password(private val s: String)
reified--限定类型参数使其可以在函数内部访问它
inline fun <reified T> TreeNode.findParentOfType(): T? {
var p = parent
while (p != null && p !is T) {
p = p.parent
}
return p as T?
}
# 可以按以下方式调用它:
treeNode.findParentOfType<MyTreeNode>()
by--类、属性的代理委托实现
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
class Derived(b: Base) : Base by b
fun main() {
val b = BaseImpl(10)
Derived(b).print()
}
class Example {
var p: String by DelegateYHF()
}
class DelegateYHF {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}
this 表达式
表示当前的 接收者 可使用 this 表达式:
- 在类的成员中,
this指的是该类的当前对象。 - 在扩展函数或者带有接收者的函数字面值中,
this表示在点左侧传递的 接收者 参数。
如果 this 没有限定符,它指的是最内层的包含它的作用域。要引用其他作用域中的 this,请使用 标签限定符
fun main() {
//sampleStart
val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
}
三、变量
3.0 值类型和引用类型
- Kotlin 的基础类型(如
Int、Double、String等)在底层实际上是对象类型,但在使用时表现得像值类型。 - Kotlin 的对象类型是基于类(
class)定义的,使用引用语义。 - Kotlin 的类型系统结合了值类型和引用类型的特点,同时引入了可空性、数据类等现代化的语言特性。
|------|-----------------------------------|-------------------|
| 特性 | 基础类型(值类型) | 对象类型(引用类型) |
| 定义方式 | 内置类型(如 Int , String 等) | 使用类(class )定义 |
| 语义 | 值语义(赋值时复制数据) | 引用语义(赋值时共享引用) |
| 性能 | 更高效(底层优化为原始类型) | 较低效(堆上分配,动态派发) |
| 可变性 | 不可变(如 Int 和 String ) | 可变(属性可以被修改) |
| 继承支持 | 不支持继承 | 支持继承 |
| 典型例子 | Int, Double, String, Char | 自定义类(如 Person ) |
3.1 声明变量、常量
#常量,初始化后不可修改,相当于java中final 修饰符
val x: Int = 5
#声明和初始化可以分开,分开的时候不能自动推测类型,必须显式注明
val c: Int
c = 3
#const 用来修饰编译时的常量,也就是说修饰一个已经确定的值, 使用const修饰需要满足以下条件:
#只能是基本类型的数据
#top-level/全局变量,注意和类变量的区别,object成员,companion对象成员
#const 不能单独使用必须和val同时修饰一个变量也就是说const val 必须同时出现
#const val ≠ java static final
class test{
//const val test = 1 //const不能出现在类变量中, Const 'val' are only allowed on top level, in named objects, or in companion objects
fun testFun(){
//const val test = 1 //const不能出现在函数变量中,Modifier 'const' is not applicable to 'local variable'
}
}
#变量
var x: Int = 5
3.1.1 类型推测
#自动推测为Int
val x = 5
#自动推测为Long
val x = 5L
3.1.2 延迟属性by lazy
- 定义
延迟属性是指一个属性的值在其第一次被访问时才会被计算或初始化。 - 关键字
使用by lazy委托来声明延迟属性。 - 特点
-
-
延迟属性是只读的(
val),不能是可变的(var)。 -
初始化代码只会在第一次访问该属性时执行一次。
-
默认情况下,
by lazy是线程安全的(可以通过参数调整线程安全性)。val propertyName: Type by lazy {
// 初始化代码
SomeType()
}
-
-
by lazy****委托:标记该属性为延迟属性。
-
闭包表达式:用于定义延迟属性的初始化逻辑。
-
线程安全性 :默认情况下,
by lazy是线程安全的,但可以通过传递参数调整。val image: String by lazy {
println("Loading image...")
"example_image"
}
3.1.3 默认不可null
Kotlin 的设计哲学强调安全性 ,因此所有变量默认是不可为空的(Non-Nullable)。如果需要允许变量为 null,必须显式地声明。
在Kotlin和Swift中可空类型和非空类型实质上都是两个类型,但是
- 在 Kotlin 中,如果一个函数的参数是可空类型(如 Int?****),你可以直接传入非空类型的值(如 Int**)。这是因为 Kotlin 的类型系统会自动将非空类型隐式转换为可空类型** 。Kotlin 的类型系统允许将非空类型(
T)隐式转换为可空类型(T?)。因此,Int类型的值可以直接赋值给Int?类型的变量或作为参数传递。 - 在 Swift 中,如果一个函数的参数是可选类型(如 Int?****),你**不能直接传入非可选类型(如 Int**)**的值。需要显式地将非可选类型包装为可选类型** 。Swift 的类型系统严格区分了可选类型(
Optional<T>)和非可选类型(T)。非可选类型不会自动转换为可选类型。如果需要将非可选类型转换为可选类型,必须显式地进行包装(例如通过Int?(value)或使用可选绑定)。
?--空值声明
- 函数引用中当可能用 null 值时必须将引用显式标记为可空,可空类型名称以问号(?)结尾。同时使用时必须进行空检测
-
- 编译器默认认为引用是非null的,如果引用被标记为非null值但是出现了null赋值则编译报错
- 如果引用声明为可null,则在调用处也必须追加以问号(?)结尾
-
函数体声明一个变量的时候也需要显式标记为可空,使用时可以进行空检测
fun parseInt(str: String?): Int? {
return str?.toIntOrNull()
}fun test(a:String?){
//var test:String? = "sss" //也可以这样子
var test = "sss"
test?.length ?: "empty" //调用时可以进行空检测
}fun printProduct(arg1: String, arg2: String) {
val x = parseInt(arg1)
val y = parseInt(arg2)//println(x * y) //直接使用 `x * y` 会导致编译错误,因为它们可能为 null //println(x * y) if (x != null && y != null) { // 在空检测后,x 与 y 会自动转换为非空值(non-nullable) println(x * y) } else { println("'$arg1' or '$arg2' is not a number") }}
如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用 filterNotNull 来实现:
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
?.和?:--自动空检测调用可空值
-
空检测if-not-null 缩写?. 可以集合let实现复杂逻辑执行
-
空检测if-not-null-else 缩写 ?: 可以集合run实现复杂逻辑执行
val files = File("Test").listFiles()
println(files?.size) // 如果 files 不是 null,那么输出其大小(size)
files?.let {
...... // 如果非空会执行这个代码块
}println(files?.size ?: "empty") // 如果 files 为 null,那么输出"empty"
// 如需在代码块中计算更复杂的备用值,请使用run
val filesSize = files?.size ?: run {
val someSize = getSomeSize()
someSize * 2
}val files2 = files ?: throw IllegalStateException("files is missing!")
#在可能会空的集合中取第一元素
val emails = ...... // 可能会是空集合
val mainEmail = emails.firstOrNull() ?: ""
!! --抛异常方式掉用可空值
第三种选择是为 NPE 爱好者准备的:非空断言运算符(!!)将任何值转换为非空类型,若该值为 null 则抛出异常。可以写 b!! ,这会返回一个非空的 b 值 (例如:在我们示例中的 String)或者如果 b 为 null,就会抛出一个 NPE 异常:
val l = b!!.length
3.2 数字
3.2.1 整数类型
- 当初始化一个没有显式指定类型的变量时,编译器会自动推断为自 Int****起足以表示该值的最小类型。 如果不超过
Int的表示范围,那么类型是Int。 如果超过了,那么类型是Long。 如需显式指定Long值,请给该值追加后缀L。 - 显式指定类型会触发编译器检测该值是否超出指定类型的表示范围。
|---------|-------------|-----------------------------------|-------------------------------------|
| 类型 | 大小(比特数) | 最小值 | 最大值 |
| Byte | 8 | -128 | 127 |
| Short | 16 | -32768 | 32767 |
| Int | 32 | -2,147,483,648 (-231) | 2,147,483,647 (231 - 1) |
| Long | 64 | -9,223,372,036,854,775,808 (-263) | 9,223,372,036,854,775,807 (263 - 1) |
|----------|-----------------|---------------|--------------------------------------|
| Type | Size (bits) | Min value | Max value |
| UByte | 8 | 0 | 255 |
| UShort | 16 | 0 | 65,535 |
| UInt | 32 | 0 | 4,294,967,295 (232 - 1) |
| ULong | 64 | 0 | 18,446,744,073,709,551,615 (264 - 1) |
3.2.2 浮点类型
Float表达 IEEE 754 单精度 ,而Double表达双精度。- 可以使用带小数部分的数字初始化
Double与Float变量。 小数部分与整数部分之间用句点(.)分隔 - 对于以小数初始化的变量,编译器会推断为 Double****类型
|----------|-------------|-------------|-----------|-----------|
| 类型 | 大小(比特数) | 有效数字比特数 | 指数比特数 | 十进制位数 |
| Float | 32 | 24 | 8 | 6-7 |
| Double | 64 | 53 | 11 | 15-16 |
-
如需将一个值显式指定为
Float类型,请添加f或F后缀。 如果这样的值包含多于 6~7 位十进制数,那么会将其舍入:val e = 2.7182818284 // Double
val eFloat = 2.7182818284f // Float,实际值为 2.7182817
3.2.3 数字字面常量
数值常量字面值有以下几种:
- 十进制:
123
-
- Long 类型用大写
L标记:123L
- Long 类型用大写
-
十六进制:
0x0F -
二进制:
0b00001011 -
Kotlin 不支持八进制。
-
你可以使用下划线使数字常量更易读:
val oneMillion = 1_000_000
val creditCardNumber = 1234_5678_9012_3456L
val socialSecurityNumber = 999_99_9999L
3.2.4 数字转换不支持隐式
-
与一些其他语言不同,Kotlin 中的数字没有隐式拓宽转换。 例如,具有 Double****参数的函数只能对 Double****值调用,而不能对 Float**、** Int****或者其他数字值调用:
fun main() {
fun printDouble(d: Double) { print(d) }val i = 1 val d = 1.0 val f = 1.0f printDouble(d)// printDouble(i) // 错误:类型不匹配
// printDouble(f) // 错误:类型不匹配
} -
由于不同的表示方式,较小类型并不是较大类型的子类型。 如果它们是的话,就会出现下述问题:
// 假想的代码,实际上并不能编译:
val a: Int? = 1 // 一个装箱的 Int (java.lang.Integer)
val b: Long? = a // 隐式转换产生一个装箱的 Long (java.lang.Long)
print(b == a) // 惊!这将输出"false"鉴于 Long 的 equals() 会检测另一个是否也为 Long -
因此较小的类型不能 隐式转换 为较大的类型。 这意味着把
Byte型值赋给一个Int变量必须显式转换:val b: Byte = 1 // OK, 字面值会静态检测
// val i: Int = b // 错误
val i1: Int = b.toInt()
所有数字类型都支持转换为其他类型:
toByte(): BytetoShort(): ShorttoInt(): InttoLong(): LongtoFloat(): FloattoDouble(): Double
很多情况都不需要显式类型转换,因为类型会从上下文推断出来, 而算术运算会有重载做适当转换,例如:
val l = 1L + 3 // Long + Int => Long
-
整数间的除法总是返回整数。会丢弃任何小数部分.如需返回浮点类型,请将其中的一个参数显式转换为浮点类型:
fun main() {
//sampleStart
val x = 5L / 2
println(x == 2L)
//sampleEnd
}fun main() {
//sampleStart
val x = 5 / 2.toDouble()
println(x == 2.5)
//sampleEnd
}
3.2.5 位运算
Kotlin 对整数提供了一组位运算 。它们直接使用数字的比特表示在二进制级别进行操作。 位运算有可以通过中缀形式调用的函数表示。只能应用于 Int 与 Long:
val x = (1 shl 2) and 0x000FF000
这是完整的位运算列表:
shl(bits)-- 有符号左移shr(bits)-- 有符号右移ushr(bits)-- 无符号右移and(bits)-- 位与or(bits)-- 位或xor(bits)-- 位异或inv()-- 位非
3.2.6 JVM 平台的数字表示
在 JVM 平台数字存储为原生类型 int、 double 等。 例外情况是当创建可空数字引用如 Int? 或者使用泛型时。 在这些场景中,数字会装箱为 Java 类 Integer、 Double 等。
对相同数字的可为空引用可能会引用不同的对象:
fun main() {
//sampleStart
val a: Int = 100
val boxedA: Int? = a
val anotherBoxedA: Int? = a
val b: Int = 10000
val boxedB: Int? = b
val anotherBoxedB: Int? = b
println(boxedA === anotherBoxedA) // true
println(boxedB === anotherBoxedB) // false
//sampleEnd
}
3.3 布尔
-
可null的布尔值为null时,标记为null
fun main() {
//sampleStart
val myTrue: Boolean = true
val myFalse: Boolean = false
val boolNull: Boolean? = nullprintln(myTrue || myFalse) // true println(myTrue && myFalse) // false println(!myTrue) // false println(boolNull) // null//sampleEnd
}
3.4 数组
- 创建数组。
-
-
functions, such as arrayOf(), arrayOfNulls()) or emptyArray().
-
the
Arrayconstructorfun main() {
//sampleStart
// Creates an array with values [1, 2, 3]
val simpleArray = arrayOf(1, 2, 3)
println(simpleArray.joinToString())
// 1, 2, 3
//sampleEnd
}
fun main() {
//sampleStart
// Creates an array with values [null, null, null]
val nullArray: Array<Int?> = arrayOfNulls(3)
println(nullArray.joinToString())
// null, null, null
//sampleEnd
}
fun main() {
//sampleStart
var exampleArray = emptyArray<String>()
var exampleArray: Array<String> = emptyArray()
//sampleEnd
}
fun main() {
//sampleStart
// Creates an Array<Int> that initializes with zeros [0, 0, 0]
val initArray = Array<Int>(3) { 0 }
println(initArray.joinToString())
// 0, 0, 0// 创建一个 Array<String> 初始化为 ["0", "1", "4", "9", "16"] val asc = Array(5) { i -> (i * i).toString() } asc.forEach { print(it) } // 014916//sampleEnd
}
-
-
嵌套数组
fun main() {
//sampleStart
// Creates a two-dimensional array
val twoDArray = Array(2) { Array<Int>(2) { 0 } }
println(twoDArray.contentDeepToString())
// [[0, 0], [0, 0]]// Creates a three-dimensional array val threeDArray = Array(3) { Array(3) { Array<Int>(3) { 0 } } } println(threeDArray.contentDeepToString()) // [[[0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0]]]//sampleEnd
} -
访问与修改元素
fun main() {
//sampleStart
val simpleArray = arrayOf(1, 2, 3)
val twoDArray = Array(2) { Array<Int>(2) { 0 } }// Accesses the element and modifies it simpleArray[0] = 10 twoDArray[0][0] = 2 // Prints the modified element println(simpleArray[0].toString()) // 10 println(twoDArray[0][0].toString()) // 2//sampleEnd
}
3.4.1 原生类型数组
- If you use the
Arrayclass with primitive values, these values are boxed into objects. As an alternative, you can use primitive-type arrays, which allow you to store primitives in an array without the side effect of boxing overhead: - To convert primitive-type arrays to object-type arrays, use the .toTypedArray() function.
- To convert object-type arrays to primitive-type arrays, use .toBooleanArray(), .toByteArray(), .toCharArray(), and so on.
|----------------------------------------------------------------------------------------------------|------------------------|
| Primitive-type array | Equivalent in Java |
| BooleanArray | boolean[] |
| ByteArray | byte[] |
| CharArray | char[] |
| DoubleArray | double[] |
| FloatArray | float[] |
| IntArray | int[] |
| LongArray | long[] |
| ShortArray | short[] |
This example creates an instance of the IntArray class:
fun main() {
//sampleStart
// Creates an array of Int of size 5 with the values initialized to zero
val exampleArray = IntArray(5)
println(exampleArray.joinToString())
// 0, 0, 0, 0, 0
//sampleEnd
}
3.4.2 数组比较
-
Don't use equality (
==) and inequality (!=) operators to compare the contents of arrays. These operators check whether the assigned variables point to the same object. -
To compare whether two arrays have the same elements in the same order, use the .contentEquals() and .contentDeepEquals() functions:
fun main() {
//sampleStart
val simpleArray = arrayOf(1, 2, 3)
val anotherArray = arrayOf(1, 2, 3)// Compares contents of arrays println(simpleArray.contentEquals(anotherArray)) // true // Using infix notation, compares contents of arrays after an element // is changed simpleArray[0] = 10 println(simpleArray contentEquals anotherArray) // false//sampleEnd
}
3.4.3 向函数传入可变数量的实参
-
In Kotlin, you can pass a variable number of arguments to a function via the vararg parameter.
-
传递数组时用*
fun main() {
val lettersArray = arrayOf("c", "d")
printAllStrings("a", "b", *lettersArray)
// abcd
}fun printAllStrings(vararg strings: String) {
for (string in strings) {
print(string)
}
}
3.4.4 与集合的差别
与数组相比,集合具有以下优点:
-
集合可以是只读的,这提供了更多的控制权而支持编写具有明确意图的健壮代码。
-
易于对集合增删元素。相比之下,数组大小是固定的。 对数组增删元素的唯一方式是每次创建一个新数组,效率很低:
fun main() {
//sampleStart
var riversArray = arrayOf("Nile", "Amazon", "Yangtze")// 使用 += 赋值操作创建了一个新的 riversArray, // 复制了原始元素并添加了"Mississippi" riversArray += "Mississippi" println(riversArray.joinToString()) // Nile, Amazon, Yangtze, Mississippi//sampleEnd
} -
可以使用相等操作符(
==)来检验两个集合是否在结构上相等。但不能对数组使用这个操作符。 相反,必须使用一个特殊函数,关于这点可以参阅比较数组了解更多信息。
3.4.5 将数组转换为集合或Map
fun main() {
//sampleStart
val simpleArray = arrayOf("a", "b", "c", "c")
// Converts to a Set
println(simpleArray.toSet())
// [a, b, c]
// Converts to a List
println(simpleArray.toList())
// [a, b, c, c]
//sampleEnd
}
fun main() {
//sampleStart
val pairArray = arrayOf("apple" to 120, "banana" to 150, "cherry" to 90, "apple" to 140)
// Converts to a Map
// The keys are fruits and the values are their number of calories
// Note how keys must be unique, so the latest value of "apple"
// overwrites the first
println(pairArray.toMap())
// {apple=140, banana=150, cherry=90}
//sampleEnd
}
3.5 字符与字符串
- 字符用 Char****类型表示。 字符字面值用单引号括起来: '1'****。 特殊字符可以以转义反斜杠 ****开始
-
- 如果字符变量的值是数字,那么可以使用 digitToInt()函数将其显式转换为 Int****数字
-
Kotlin 中字符串用 String类型表示。字符串值是双引号( "****)中的字符序列。字符串的元素------字符可以使用索引运算符访问: s[i]
-
多行字符串 可以包含换行以及任意文本。 它使用三个引号(
""")分界符括起来,内部没有转义并且可以包含换行以及任何其他字符:val text = """
for (c in "foo")
print(c)
""" -
字符串是不可变的。 一旦初始化了一个字符串,就不能改变它的值或者给它赋新值。 所 有转换字符串的操作都以一个新的 String****对象来返回结果,而保持原始字符串不变
-
如需连接字符串,可以用
+操作符。这也适用于连接字符串与其他类型的值, 只要表达式中的第一个元素是字符串 -
To format a string to your specific requirements, use the String.format() function.
3.5.1 字符串创建
复制
|-----------------------|-------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| 方式 | 语法示例 | 特点 |
| 字面量字符串 | kotlin val str = "Hello, World!" | - 最常用的方式。 - 使用双引号(" )包裹。 - 支持转义字符(如 \n , \" 等)。 |
| 多行字符串(Raw String) | kotlin val multiLine = """ Hello, World! This is a multi-line string. """ | - 使用三对双引号(""" )包裹。 - 支持多行文本,无需手动换行符(\n )。 - 不支持转义字符。 |
| 字符串模板 | kotlin val name = "Alice" val greeting = "Hello, $name!" | - 在字符串中嵌入变量或表达式。 - 使用 $ 符号引用变量,${} 嵌入表达式。 - 动态生成字符串。 |
| 通过构造函数创建 | kotlin val charArray = charArrayOf('H', 'e', 'l', 'l', 'o') val str = String(charArray) | - 使用 String 类的构造函数从字符数组或其他数据源创建字符串。 - 适用于底层操作或数据转换。 |
| 格式化字符串 | kotlin val formatted = "Name: %s, Age: %d".format("Alice", 25) | - 使用 format 方法格式化字符串。 - 类似于 Java 的 String.format 。 - 支持占位符(如 %s , %d )。 |
| 空字符串 | kotlin val emptyString = "" | - 表示一个空字符串。 - 长度为 0,内容为空。 |
3.5.2 字符串模版$
- 模板表达式以美元符(
$)开头,要么由一个变量名构成,要么是用花括号括起来的表达式
了解 Java 与 Kotlin 字符串连接的区别
//字符串模板,模板中的任意表达式:
var a = 1
val s1 = "a is $a"
a = 2
val s2 = "${s1.replace("is", "was")}, but now is $a"
3.5.3 字符串拼接
复制
|--------------------------|-----------------------------------------------------------------------------------------------------------|------------------------------------------------|
| 方式 | 语法示例 | 特点 |
| 使用 + 运算符 | kotlin val str = "Hello" + " " + "World!" | - 最简单直接的方式。 - 适用于少量字符串的拼接。 |
| 使用模板字符串(推荐) | kotlin val name = "Alice" val greeting = "Hello, $name!" | - 使用 $ 或 ${} 嵌入变量或表达式。 - 动态生成字符串,代码更简洁、易读。 |
| 使用 StringBuilder | kotlin val sb = StringBuilder() sb.append("Hello").append(" ").append("World!") val str = sb.toString() | - 高效拼接大量字符串。 - 避免频繁创建新的字符串对象,性能更优。 |
| 使用 joinToString | kotlin val list = listOf("A", "B", "C") val str = list.joinToString(", ") | - 将集合中的元素拼接成字符串。 - 支持分隔符、前缀和后缀等选项。 |
-
在 Kotlin 中,使用 buildString() -- 一个内联函数,它采用逻辑来构造字符串作为 lambda 参数:
fun main() {
//sampleStart
// Kotlin
val countDown = buildString {
for (i in 5 downTo 1) {
append(i)
appendLine()
}
}
println(countDown)
//sampleEnd
}
在后台,buildString 使用与 Java 中相同的 StringBuilder 类,您可以通过 lambda 中的隐式 this 来访问它。
-
在 Kotlin 中,使用 joinToString() 函数,Kotlin 为每个列表定义该函数:
fun main() {
//sampleStart
// Kotlin
val numbers = listOf(1, 2, 3, 4, 5, 6)
val invertedOddNumbers = numbers
.filter { it % 2 != 0 }
.joinToString(separator = ";") {"${-it}"}
println(invertedOddNumbers)
//sampleEnd
}
3.4.4 字符串为null的默认值
在 Java 中,您可以使用三元运算符
Kotlin 提供了内联函数 ifBlank(),该函数接受默认值作为参数:
// Kotlin
import kotlin.random.Random
//sampleStart
fun main() {
val name = getName().ifBlank { "John Doe" }
println(name)
}
fun getName(): String =
if (Random.nextBoolean()) "" else "David"
//sampleEnd
3.4.5 字符串替换和拆分
在 Kotlin 中,使用带有字符串分隔符 ## 的 removeSurrounding() 函数:
fun main() {
//sampleStart
// Kotlin
val input = "##place##holder##"
val result = input.removeSurrounding("##")
println(result)
//sampleEnd
}
在 Kotlin 中,使用 Kotlin 函数 split(),该函数接受分隔符的变量作为输入参数:
fun main() {
//sampleStart
// Kotlin
println("Sometimes.text.should.be.split".split("."))
//sampleEnd
}
3.6 集合概述
了解Java 与 Kotlin 中的集合的区别
#只读 list
val list = listOf("a", "b", "c")
#只读 map
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
#访问
println(map["key"])
map["key"] = value
#🏪
for ((k, v) in map) {
println("$k -> $v")
}
3.6.1 可变性
Java 中,有可变的集合:
// Java
// This list is mutable!
public List<Customer> getCustomers() { ... }
部分可变的:
// Java
List<String> numbers = Arrays.asList("one", "two", "three", "four");
numbers.add("five"); // Fails in runtime with `UnsupportedOperationException`
和不可变的:
// Java
List<String> numbers = new LinkedList<>();
// This list is immutable!
List<String> immutableCollection = Collections.unmodifiableList(numbers);
immutableCollection.add("five"); // Fails in runtime with `UnsupportedOperationException`
如果您在 IntelliJ IDEA 中编写最后两段代码,IDE 将警告您正在尝试修改不可变对象。此代码将在运行时编译并失败,并出现 UnsupportedOperationException。您无法通过查看集合的类型来判断集合是否可变。
与 Java 不同,在 Kotlin 中,您可以根据需要明确声明可变或只读集合。如果尝试修改只读集合,代码将无法编译:
// Kotlin
val numbers = mutableListOf("one", "two", "three", "four")
numbers.add("five") // This is OK
val immutableNumbers = listOf("one", "two")
//immutableNumbers.add("five") // Compilation error - Unresolved reference: add

- Collection<T> 是集合层次结构的根。这个接口表示一个只读集合的共同行为:检索大小、 检测是否为成员等等。
Collection继承自Iterable <T>接口,它定义了迭代元素的操作。可以使用Collection作为适用于不同集合类型的函数的参数。对于更具体的情况,请使用Collection的继承者: List 与 Set。 - MutableCollection<T> 是一个具有写操作的
Collection接口,例如add以及remove
3.6.2 协变性
在 Java 中,不能将具有后代类型的集合传递给接受祖先类型的集合的函数。例如,如果 Rectangle 扩展了 Shape,则不能将 Rectangle 元素的集合传递给接受 Shape 元素集合的函数。要使代码可编译,请使用 ? 扩展 Shape 类型,以便函数可以接受 Shape 的任何继承者的集合:
// Java
class Shape {}
class Rectangle extends Shape {}
public void doSthWithShapes(List<? extends Shape> shapes) {
/* If using just List<Shape>, the code won't compile when calling
this function with the List<Rectangle> as the argument as below */
}
public void main() {
var rectangles = List.of(new Rectangle(), new Rectangle());
doSthWithShapes(rectangles);
}
在 Kotlin 中,只读集合类型是 协变的。这意味着,如果 Rectangle****类继承自 Shape****类,则可以在需要 **List<Shape>**类型的任何位置使用 **List<Rectangle>**类型。换言之,集合类型与元素类型具有相同的子类型关系。
映射Map在值类型上是协变的,但在键类型上不是协变的。可变集合不是协变的 - 这将导致运行时失败。
// Kotlin
open class Shape(val name: String)
class Rectangle(private val rectangleName: String) : Shape(rectangleName)
fun doSthWithShapes(shapes: List<Shape>) {
println("The shapes are: ${shapes.joinToString { it.name }}")
}
fun main() {
val rectangles = listOf(Rectangle("rhombus"), Rectangle("parallelepiped"))
doSthWithShapes(rectangles)
}
3.6.3 List
在 Kotlin 中,MutableList 的默认实现是 ArrayList, 可以将其视为可调整大小的数组。
ArrayDeque<T> 是双端队列的实现,它允许您在队列的开头或结尾添加或删除元素。因此,ArrayDeque 还在 Kotlin 中担任 Stack 和 Queue 数据结构的角色
-
创建集合的最常用方法是使用标准库函数 listOf<T>()、 setOf<T>()、 mutableListOf<T>()、 mutableSetOf<T>()**。 如果以逗号分隔的集合元素列表作为参数,编译器会自动检测元素类型。**创建空集合时,须明确指定类型。
-
对于 List,有一个接受 List 的大小与初始化函数的类似构造函数的函数,该初始化函数根据索引定义元素的值。
fun main() {
//sampleStart
val doubled = List(3, { it * 2 }) // 如果你想操作这个集合,应使用 MutableList
println(doubled)
//sampleEnd
} -
要创建具体类型的集合,例如
ArrayList或LinkedList,可以使用这些类型的构造函数。 类似的构造函数对于Set与Map的各实现中均有提供。val linkedList = LinkedList<String>(listOf("one", "two", "three"))
val presizedSet = HashSet<Int>(32)
|------------|----------------------------|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| 类别 | 方法 | 功能描述 | 简单示例 |
| 创建与初始化 | listOf() | 创建一个不可变的 List 。 | kotlin val list = listOf(1, 2, 3) println(list) // 输出:[1, 2, 3] |
| | mutableListOf() | 创建一个可变的 List 。 | kotlin val list = mutableListOf(1, 2, 3) list.add(4) println(list) // 输出:[1, 2, 3, 4] |
| | emptyList() | 创建一个空的不可变 List 。 | kotlin val list = emptyList<Int>() println(list) // 输出:[] |
| | buildList(builderAction) | 动态构建一个 List,通过闭包操作逐步添加元素。 | kotlin val list = buildList { add(1) add(2) addAll(listOf(3, 4)) } println(list) // 输出:[1, 2, 3, 4] |
| 访问与查找 | get(index) | 根据索引获取元素。 | kotlin val list = listOf(1, 2, 3) println(list.get(0)) // 输出:1 |
| | contains(element) | 检查是否包含指定元素,返回布尔值。 | kotlin val list = listOf(1, 2, 3) println(list.contains(2)) // 输出:true |
| | indexOf(element) | 返回指定元素的第一个索引位置(如果不存在则返回 -1 )。 | kotlin val list = listOf(1, 2, 3) println(list.indexOf(2)) // 输出:1 |
| 添加与修改 | add(element) | 向可变 List 中添加元素。 | kotlin val list = mutableListOf(1, 2, 3) list.add(4) println(list) // 输出:[1, 2, 3, 4] |
| | remove(element) | 移除指定元素。 | kotlin val list = mutableListOf(1, 2, 3) list.remove(2) println(list) // 输出:[1, 3] |
| | clear() | 清空所有元素。 | kotlin val list = mutableListOf(1, 2, 3) list.clear() println(list) // 输出:[] |
| 遍历与迭代 | forEach(action) | 遍历 List 中的每个元素,并执行闭包操作。 | kotlin val list = listOf(1, 2, 3) list.forEach { println(it) } // 输出:1 2 3 |
| | for (element in list) | 使用 for-in 循环逐一遍历 List 中的元素。 | kotlin val list = listOf(1, 2, 3) for (element in list) { println(element) } // 输出:1 2 3 |
| | forEachIndexed(action) | 遍历 List 中的每个元素及其索引,并执行闭包操作。 | kotlin val list = listOf(1, 2, 3) list.forEachIndexed { index, value -> println("Index: $index, Value: $value") } // 输出:Index: 0, Value: 1 ... |
| | withIndex() | 返回一个包含索引和元素的对象序列,用于遍历。 | kotlin val list = listOf(1, 2, 3) for ((index, value) in list.withIndex()) { println("Index: $index, Value: $value") } // 输出:Index: 0, Value: 1 ... |
| 转换与筛选 | map(transform) | 对 List 中的每个元素应用转换闭包,返回新列表。 | kotlin val list = listOf(1, 2, 3) val doubled = list.map { it * 2 } println(doubled) // 输出:[2, 4, 6] |
| | filter(predicate) | 根据条件筛选 List 中的元素,返回新列表。 | kotlin val list = listOf(1, 2, 3, 4) val filtered = list.filter { it > 2 } println(filtered) // 输出:[3, 4] |
- 要创建与现有集合具有相同元素的集合,可以使用复制函数。 标准库中的集合复制函数创建了具有相同元素引用的 浅 复制集合。 因此,对集合元素所做的更改会反映在其所有副本中。在特定时刻通过集合复制函数,例如toList()、 toMutableList()、 toSet() 等等。
3.6.4 Map
MutableMap 的默认实现 -- LinkedHashMap------ 迭代 Map 时保留元素插入的顺序。 反之,另一种实现 -- HashMap------ 不声明元素的顺序。
-
创建集合的最常用方法是使用标准库函数 listOf<T>()、 setOf<T>()、 mutableListOf<T>()、 mutableSetOf<T>()**。 如果以逗号分隔的集合元素列表作为参数,编译器会自动检测元素类型。**创建空集合时,须明确指定类型。
-
映射的键和值作为 Pair****对象传递(通常使用中缀函数 to****创建)。
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)
注意,to 符号创建了一个短时存活的 Pair 对象,因此建议仅在性能不重要时才使用它。
为避免过多的内存使用,请使用其他方法。
例如,可以创建可写 Map 并使用写入操作填充它。 apply() 函数可以帮助保持初始化流畅
val numbersMap = mutableMapOf<String, String>().apply { this["one"] = "1"; this["two"] = "2" }
|-----------|-----------------------------------|------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|
| 类别 | 方法 | 功能描述 | 简单示例 |
| 创建与初始化 | mapOf() | 创建一个不可变的 Map 。 | kotlin val map = mapOf("a" to 1, "b" to 2) println(map) // 输出:{a=1, b=2} |
| | mutableMapOf() | 创建一个可变的 Map 。 | kotlin val map = mutableMapOf("a" to 1, "b" to 2) map["c"] = 3 println(map) // 输出:{a=1, b=2, c=3} |
| | emptyMap() | 创建一个空的不可变 Map 。 | kotlin val map = emptyMap<String, Int>() println(map) // 输出:{} |
| | 自定义实现或使用 mutableMapOf + apply | 动态构建一个 Map,通过闭包操作逐步添加键值对。 | kotlin val map = buildMap { put("a", 1) put("b", 2) putAll(mapOf("c" to 3, "d" to 4)) } println(map) // 输出:{a=1, b=2, c=3, d=4} |
| 访问与查找 | get(key) | 根据键获取值,返回可选类型(null 如果键不存在)。 | kotlin val map = mapOf("a" to 1, "b" to 2) println(map.get("a")) // 输出:1 |
| | containsKey(key) | 检查是否包含指定键,返回布尔值。 | kotlin val map = mapOf("a" to 1, "b" to 2) println(map.containsKey("a")) // 输出:true |
| | containsValue(value) | 检查是否包含指定值,返回布尔值。 | kotlin val map = mapOf("a" to 1, "b" to 2) println(map.containsValue(2)) // 输出:true |
| 添加与修改 | put(key, value) | 向可变 Map 中插入或更新键值对。 | kotlin val map = mutableMapOf("a" to 1) map.put("b", 2) println(map) // 输出:{a=1, b=2} |
| | remove(key) | 移除指定键的键值对。 | kotlin val map = mutableMapOf("a" to 1, "b" to 2) map.remove("a") println(map) // 输出:{b=2} |
| | clear() | 清空所有键值对。 | kotlin val map = mutableMapOf("a" to 1, "b" to 2) map.clear() println(map) // 输出:{} |
| 遍历与迭代 | forEach(action) | 遍历 Map 中的每个键值对,并执行闭包操作。 | kotlin val map = mapOf("a" to 1, "b" to 2) map.forEach { (key, value) -> println("$key: $value") } // 输出:a: 1 b: 2 |
| | for (entry in map) | 使用 for-in 循环逐一遍历 Map.Entry 对象。 | kotlin val map = mapOf("a" to 1, "b" to 2) for ((key, value) in map) { println("$key: $value") } // 输出:a: 1 b: 2 |
| | keys.forEach(action) | 遍历 Map 中的所有键,并对每个键执行闭包操作。 | kotlin val map = mapOf("a" to 1, "b" to 2) map.keys.forEach { println(it) } // 输出:a b |
| | values.forEach(action) | 遍历 Map 中的所有值,并对每个值执行闭包操作。 | kotlin val map = mapOf("a" to 1, "b" to 2) map.values.forEach { println(it) } // 输出:1 2 |
| 转换与筛选 | map(transform) | 对 Map 中的每个键值对应用转换闭包,返回新列表。 | kotlin val map = mapOf("a" to 1, "b" to 2) val result = map.map { "${it.key.uppercase()}=${it.value * 2}" } println(result) // 输出:[A=2, B=4] |
| | filter(predicate) | 根据条件筛选 Map 中的键值对,返回新 Map。 | kotlin val map = mapOf("a" to 1, "b" to 2, "c" to 3) val filtered = map.filter { it.value > 1 } println(filtered) // 输出:{b=2, c=3} |
3.6.5 Set
MutableSet的默认实现 - LinkedHashSet------ 保留元素插入的顺序。 因此,依赖于顺序的函数,例如 first() 或 last(),会在这些 set 上返回可预测的结果。
|------------|---------------------------|-----------------------------------|--------------------------------------------------------------------------------------------------------------|
| 类别 | 方法 | 功能描述 | 简单示例 |
| 创建与初始化 | setOf() | 创建一个不可变的 Set 。 | kotlin val set = setOf(1, 2, 3) println(set) // 输出:[1, 2, 3] |
| | mutableSetOf() | 创建一个可变的 Set 。 | kotlin val set = mutableSetOf(1, 2, 3) set.add(4) println(set) // 输出:[1, 2, 3, 4] |
| | emptySet() | 创建一个空的不可变 Set 。 | kotlin val set = emptySet<Int>() println(set) // 输出:[] |
| | buildSet(builderAction) | 动态构建一个 Set,通过闭包操作逐步添加元素(自动去重)。 | kotlin val set = buildSet { add(1) add(2) add(2) addAll(setOf(3, 4)) } println(set) // 输出:[1, 2, 3, 4] |
| 访问与查找 | contains(element) | 检查是否包含指定元素,返回布尔值。 | kotlin val set = setOf(1, 2, 3) println(set.contains(2)) // 输出:true |
| | isEmpty() | 检查集合是否为空,返回布尔值。 | kotlin val set = setOf<Int>() println(set.isEmpty()) // 输出:true |
| | size | 返回集合中元素的数量。 | kotlin val set = setOf(1, 2, 3) println(set.size) // 输出:3 |
| 添加与修改 | add(element) | 向可变 Set 中添加元素(如果元素已存在,则不会重复添加)。 | kotlin val set = mutableSetOf(1, 2, 3) set.add(3) println(set) // 输出:[1, 2, 3] |
| | remove(element) | 移除指定元素。 | kotlin val set = mutableSetOf(1, 2, 3) set.remove(2) println(set) // 输出:[1, 3] |
| | clear() | 清空所有元素。 | kotlin val set = mutableSetOf(1, 2, 3) set.clear() println(set) // 输出:[] |
| 集合运算 | union(other) | 返回两个集合的并集(包含所有元素)。 | kotlin val set1 = setOf(1, 2, 3) val set2 = setOf(3, 4, 5) println(set1.union(set2)) // 输出:[1, 2, 3, 4, 5] |
| | intersect(other) | 返回两个集合的交集(共同元素)。 | kotlin val set1 = setOf(1, 2, 3) val set2 = setOf(3, 4, 5) println(set1.intersect(set2)) // 输出:[3] |
| | subtract(other) | 返回当前集合减去另一个集合的结果(差集)。 | kotlin val set1 = setOf(1, 2, 3) val set2 = setOf(3, 4, 5) println(set1.subtract(set2)) // 输出:[1, 2] |
| 遍历与迭代 | forEach(action) | 遍历 Set 中的每个元素,并执行闭包操作。 | kotlin val set = setOf(1, 2, 3) set.forEach { println(it) } // 输出:1 2 3 |
| | for (element in set) | 使用 for-in 循环逐一遍历 Set 中的元素。 | kotlin val set = setOf(1, 2, 3) for (element in set) { println(element) } // 输出:1 2 3 |
| | map(transform) | 对 Set 中的每个元素应用转换闭包,返回新列表。 | kotlin val set = setOf(1, 2, 3) val doubled = set.map { it * 2 } println(doubled) // 输出:[2, 4, 6] |
| | filter(predicate) | 根据条件筛选 Set 中的元素,返回新集合。 | kotlin val set = setOf(1, 2, 3, 4) val filtered = set.filter { it > 2 } println(filtered) // 输出:[3, 4] |
3.6.6 List 、Set和Map的迭代器遍历
对于遍历集合元素, Kotlin 标准库支持 迭代器 的常用机制------ 对象可按顺序提供对元素的访问权限,而不会暴露集合的底层结构。 当需要逐个处理集合的所有元素(例如打印值或对其进行类似更新)时, 迭代器非常有用。
Iterable<T> 接口的继承者 (包括 Set 与 List)可以通过调用 iterator() 函数获得迭代器。
一旦获得迭代器它就指向集合的第一个元素;调用 next() 函数将返回此元素,并将迭代器指向下一个元素(如果下一个元素存在)。
一旦迭代器通过了最后一个元素,它就不能再用于检索元素;也无法重新指向到以前的任何位置。要再次遍历集合,请创建一个新的迭代器。
fun main() {
//sampleStart
val numbers = listOf("one", "two", "three", "four")
val numbersIterator = numbers.iterator()
while (numbersIterator.hasNext()) {
println(numbersIterator.next())
// one
// two
// three
// four
}
//sampleEnd
}
遍历 Iterable 集合的另一种方法是众所周知的 for 循环。在集合中使用 for 循环时, 会隐式获取迭代器。因此,以下代码与上述示例等效:
fun main() {
//sampleStart
val numbers = listOf("one", "two", "three", "four")
for (item in numbers) {
println(item)
// one
// two
// three
// four
}
//sampleEnd
}
List 特殊迭代器
对于列表,有一个特殊的迭代器实现: ListIterator。 它支持列表双向迭代:正向与反向。
反向迭代由 hasPrevious() 与 previous() 函数实现。 此外, ListIterator 通过 nextIndex() 与 previousIndex() 函数提供有关元素索引的信息。
fun main() {
//sampleStart
val numbers = listOf("one", "two", "three", "four")
val listIterator = numbers.listIterator()
while (listIterator.hasNext()) listIterator.next()
println("Iterating backwards:")
// Iterating backwards:
while (listIterator.hasPrevious()) {
print("Index: ${listIterator.previousIndex()}")
println(", value: ${listIterator.previous()}")
// Index: 3, value: four
// Index: 2, value: three
// Index: 1, value: two
// Index: 0, value: one
}
//sampleEnd
}
具有双向迭代的能力意味着 ListIterator 在到达最后一个元素后仍可以使用。
可变迭代器
为了迭代可变集合,于是有了 MutableIterator 来扩展 Iterator 使其具有元素删除函数 remove() 。 因此,可以在迭代时从集合中删除元素。
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three", "four")
val mutableIterator = numbers.iterator()
mutableIterator.next()
mutableIterator.remove()
println("After removal: $numbers")
// After removal: [two, three, four]
//sampleEnd
}
除了删除元素, MutableListIterator 还可以在迭代列表时插入和替换元素(使用 add() 与 set() 函数)。
fun main() {
//sampleStart
val numbers = mutableListOf("one", "four", "four")
val mutableListIterator = numbers.listIterator()
mutableListIterator.next()
mutableListIterator.add("two")
println(numbers)
// [one, two, four, four]
mutableListIterator.next()
mutableListIterator.set("three")
println(numbers)
// [one, two, three, four]
//sampleEnd
}
3.6.7 更多 集合操作
公共操作可用于只读集合与可变集合。 公共操作分为以下几类:
-
- 这些函数根据提供的转换规则从现有集合中构建新集合
-
- 在Kotlin中,过滤条件由 谓词 定义------接受一个集合元素并且返回布尔值的 lambda 表达式, 其返回值含义:
true说明给定元素与谓词匹配,false则表示不匹配 - 标准库包含了一组让你能够通过单个调用就可以过滤集合的扩展函数。 这些函数不会改变原始集合,因此它们既可用于可变集合也可用于只读集合
- 在Kotlin中,过滤条件由 谓词 定义------接受一个集合元素并且返回布尔值的 lambda 表达式, 其返回值含义:
这些页面中描述的操作将返回其结果,而不会影响原始集合。例如,一个过滤操作产生一个新集合,其中包含与过滤谓词匹配的所有元素。 此类操作的结果应存储在变量中,或以其他方式使用,例如,传到其他函数中。
fun main() {
//sampleStart
val numbers = listOf("one", "two", "three", "four")
numbers.filter { it.length > 3 } // `numbers` 没有任何改变,结果丢失
println("numbers are still $numbers")
val longerThan3 = numbers.filter { it.length > 3 } // 结果存储在 `longerThan3` 中
println("numbers longer than 3 chars are $longerThan3")
//sampleEnd
}
对于可变集合,还存在可更改集合状态的 写操作 。这些操作包括添加、删除和更新元素。写操作在集合写操作以及 List 写操作与 Map 写操作的相应部分中列出。
对于某些操作,有成对的函数可以执行相同的操作:一个函数就地应用该操作, 另一个函数将结果作为单独的集合返回。 例如, sort() 就地对可变集合进行排序,因此其状态发生了变化; sorted() 创建一个新集合,该集合包含按排序顺序相同的元素。
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three", "four")
val sortedNumbers = numbers.sorted()
println(numbers == sortedNumbers) // false
numbers.sort()
println(numbers == sortedNumbers) // true
//sampleEnd
}
3.6.8 在 Java 与 Kotlin 中相同的操作
对 list、 set、 queue 与 deque 的操作
|-----------------|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 描述 | 共有操作 | 更多 Kotlin 替代方式 |
| 添加一个或多个元素 | add() , addAll() | 使用 plusAssign(+=) 运算符 :collection += element , collection += anotherCollection 。 |
| 检查集合是否包含一个或多个元素 | contains() , containsAll() | 使用 in关键字 以运算符形式调用 contains(): 集合中的元素 。 |
| 检查集合是否为空 | isEmpty() | 使用 isNotEmpty() 检查集合是否不为空。 |
| 在特定条件下移除 | removeIf() | |
| 仅保留选定的元素 | retainAll() | |
| 从集合中删除所有元素 | clear() | |
| 从集合中获取流 | stream() | Kotlin 有自己的处理流的方式:序列 和方法,如 map() 和 filter()。 |
| 从集合中获取迭代器 | iterator() | |
对 map 的操作
|-----------------|----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 描述 | 共有操作 | 更多 Kotlin 替代方式 |
| 添加一个或多个元素 | put() , putAll() , putIfAbsent() | 在 Kotlin 中,赋值 map[key] = value的行为与 put(key, value) 相同。此外,您可以使用 plusAssign(+=) 运算符:map += Pair(key, value) 或 map += anotherMap 。 |
| 替换一个或多个元素 | put() 、replace() 、replaceAll() | 使用索引运算符 map[key] = value 而不是 put()和 replace()。 |
| 获取元素 | get() | 使用索引运算符获取元素:map[index]。 |
| 检查地图是否包含一个或多个元素 | containsKey() , containsValue() | 使用 in关键字 以运算符形式调用 contains() map: 元素 。 |
| 检查地图是否为空 | isEmpty() | 使用 isNotEmpty() 检查地图是否不为空。 |
| 删除元素 | remove(键) , remove(键, 值) | 使用 minusAssign(-=) 运算符 :map -= key 。 |
| 从地图中删除所有元素 | clear() | |
| 从地图中获取流 | stream() 在条目、键或值上 | |
仅针对 list 的操作
|-------------|--------------------------|--------------------------------------------|
| 描述 | 共有操作 | 更多 Kotlin 替代方式 |
| 获取元素的索引 | indexOf() | |
| 获取元素的最后一个索引 | lastIndexOf() | |
| 获取元素 | get() | 使用索引运算符获取元素:list[index]。 |
| 获取子列表 | subList() | |
| 替换一个或多个元素 | set() , replaceAll() | 使用索引运算符代替 set() :list[index] = value 。 |
示例:检测元素是否存在于集合中
val items = setOf("apple", "banana", "kiwifruit")
//sampleStart
when {
"orange" in items -> println("juicy")
"apple" in items -> println("apple is fine too")
}
示例:过滤和映射
#使用 lambda 表达式来过滤(filter)与映射(map)集合
val fruits = listOf("banana", "avocado", "apple", "kiwifruit")
fruits
.filter { it.startsWith("a") }
.sortedBy { it }
.map { it.uppercase() }
.forEach { println(it) }
val positives = list.filter { x -> x > 0 }
val positives = list.filter { it > 0 }
在 Java 中,要从集合中过滤元素,您需要使用 Stream API。Stream API 具有中间操作和终端操作。filter() 是一个中间操作,它返回一个流。要接收集合作为输出,您需要使用终端操作,例如 collect()。例如,要仅保留键以 1 结尾且值大于 10 的那些对:
// Java
public void filterEndsWith() {
var numbers = Map.of("key1", 1, "key2", 2, "key3", 3, "key11", 11);
var filteredNumbers = numbers.entrySet().stream()
.filter(entry -> entry.getKey().endsWith("1") && entry.getValue() > 10)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
System.out.println(filteredNumbers);
}
在 Kotlin 中,过滤功能内置于集合中,filter() 返回与过滤过的相同集合类型。所以,你需要写的就是filter()和它的谓词:
fun main() {
//sampleStart
// Kotlin
val numbers = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
val filteredNumbers = numbers.filter { (key, value) -> key.endsWith("1") && value > 10 }
println(filteredNumbers)
//sampleEnd
3.6.9 在 Java 与 Kotlin 中略有不同的操作
对任意集合类型的操作
|---------------------------|--------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 描述 | Java | Kotlin |
| 获取集合的大小 | size() | count() , 大小 |
| 获取对嵌套集合元素的平面访问 | collectionOfCollections.forEach(flatCollection::addAll) 或 collectionOfCollections.stream().flatMap().collect() | flatten() 或 flatMap() |
| 将给定的函数应用于每个元素 | stream().map().collect() | map() |
| 按顺序将提供的操作应用于集合元素,并返回累积的结果 | stream().reduce() | reduce(),fold() |
| 通过分类器对元素进行分组并对其进行计数 | stream().collect(Collectors.groupingBy(classifier, counting())) | eachCount() |
| 按条件筛选 | stream().filter().collect() | filter() |
| 检查集合元素是否满足条件 | stream().noneMatch() , stream().anyMatch() , stream().allMatch() | None(),Any(),All() |
| 对元素进行排序 | stream().sorted().collect() | sorted() |
| 取前 N 个元素 | stream().limit(N).collect() | take(N) |
| 取带有谓语的元素 | stream().takeWhile().collect() | takeWhile() |
| 跳过前 N 个元素 | stream().skip(N).collect() | drop(N) |
| 跳过带有谓词的元素 | stream().dropWhile().collect() | dropWhile() |
| 从集合元素和与其关联的某些值构建映射 | stream().collect(toMap(keyMapper, valueMapper)) | associate() |
要在地图上执行上述所有操作,首先需要获取地图的 entrySet。
对 list 的操作
|---------------|-------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| 描述 | Java | Kotlin |
| 按自然顺序对列表进行排序 | sort(null) | sort() |
| 按降序对列表进行排序 | sort(comparator) | sortDescending() |
| 从列表中删除元素 | remove(索引) , remove(元素) | removeAt(index) 、 remove(element) 或 集合 -= 元素 |
| 用特定值填充列表的所有元素 | Collections.fill() | fill() |
| 从列表中获取唯一元素 | stream().distinct().toList() | distinct() |
在 Java 中,remove()) 函数接受要删除的元素的索引。
删除整数元素时,使用 Integer.valueOf() 函数作为 remove() 函数的参数:
// Java
public void remove() {
var numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(1);
numbers.remove(1); // This removes by index
System.out.println(numbers); // [1, 3, 1]
numbers.remove(Integer.valueOf(1));
System.out.println(numbers); // [3, 1]
}
在 Kotlin 中,有两种类型的元素删除:使用 removeAt() 按索引删除和使用 remove() 按值删除。
fun main() {
//sampleStart
// Kotlin
val numbers = mutableListOf(1, 2, 3, 1)
numbers.removeAt(0)
println(numbers) // [2, 3, 1]
numbers.remove(1)
println(numbers) // [2, 3]
//sampleEnd
}
Java 标准库中不存在的操作
- zip(),unzip() -- 转换集合。
- aggregate() -- 按条件分组。
- takeLast(),takeLastWhile(),dropLast(),dropLastWhile() -- 通过谓词获取或删除元素。
- slice(),chunked(),windowed() -- 检索集合部分。
- 加号 (+) 和减号 (-) 运算符 -- 添加或删除元素。
遍历 map
在 Java 中,您可以通过 forEach 遍历地图):
// Java
numbers.forEach((k,v) -> System.out.println("Key = " + k + ", Value = " + v));
在 Kotlin 中,使用 for 循环或 forEach(类似于 Java 的 forEach)来遍历地图:
// Kotlin
for ((k, v) in numbers) {
println("Key = $k, Value = $v")
}
// Or
numbers.forEach { (k, v) -> println("Key = $k, Value = $v") }
获取可能会空的集合的首末元素
在 Java 中,您可以通过检查集合的大小并使用索引来安全地获取第一项和最后一项:
// Java
var list = new ArrayList<>();
//...
if (list.size() > 0) {
System.out.println(list.get(0));
System.out.println(list.get(list.size() - 1));
}
您还可以将 getFirst()) 和 getLast()) 函数用于 Deque 及其继承者:
// Java
var deque = new ArrayDeque<>();
//...
if (deque.size() > 0) {
System.out.println(deque.getFirst());
System.out.println(deque.getLast());
}
在 Kotlin 中,有特殊函数 firstOrNull() 和 lastOrNull()。使用 Elvis 运算符,您可以根据函数的结果立即执行进一步的操作。例如,firstOrNull():
// Kotlin
val emails = listOf<String>() // Might be empty
val theOldestEmail = emails.firstOrNull() ?: ""
val theFreshestEmail = emails.lastOrNull() ?: ""
由 list 创建 set
在 Java 中,要从 List 创建 Set,可以使用 Set.copyOf) 函数:
// Java
public void listToSet() {
var sourceList = List.of(1, 2, 3, 1);
var copySet = Set.copyOf(sourceList);
System.out.println(copySet);
}
在 Kotlin 中,使用函数 toSet():
fun main() {
//sampleStart
// Kotlin
val sourceList = listOf(1, 2, 3, 1)
val copySet = sourceList.toSet()
println(copySet)
//sampleEnd
}
3.7 区间与数列
Kotlin 可通过调用 kotlin.ranges 包中的 rangeTo() 及 .rangeUntil() 函数轻松地创建两个值的区间
-
a closed-ended range, call the
.rangeTo()function with the..operator. -
an open-ended range, call the
.rangeUntil()function with the..<operator.val x = 10
val y = 9
if (x in 1..y+1) {
println("fits in range")
}for (x in 1..5) {
print(x)
}
for (x in 1..10 step 2) {
print(x)
}
println()
for (x in 9 downTo 0 step 3) {
print(x)
}
3.8 序列
除了集合之外,Kotlin 标准库还包含另一种类型------序列 (Sequence<T>)。 Unlike collections, sequences don't contain elements, they produce them while iterating. 序列提供与 Iterable 相同的函数,但实现另一种方法来进行多步骤集合处理。
当 Iterable 的处理包含多个步骤时,它们会优先执行:每个处理步骤完成并返回其结果------中间集合。 在此集合上执行以下步骤。反过来, 序列的多步处理在可能的情况下会延迟执行:仅当请求整个处理链的结果时才进行实际计算。
操作执行的顺序也不同:Sequence 对每个元素逐个执行所有处理步骤。 反过来,Iterable 完成整个集合的每个步骤,然后进行下一步。
3.8.1 构造
由元素
要创建一个序列,请调用 sequenceOf() 函数,列出元素作为其参数。
val numbersSequence = sequenceOf("four", "three", "two", "one")
由 Iterable
如果已经有一个 Iterable 对象(例如 List 或 Set),则可以通过调用 asSequence() 从而创建一个序列。
val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()
由函数
创建序列的另一种方法是通过使用计算其元素的函数来构建序列。 要基于函数构建序列,请以该函数作为参数调用 generateSequence()。 (可选)可以将第一个元素指定为显式值或函数调用的结果。 当提供的函数返回 null 时,序列生成停止。因此,以下示例中的序列是无限的。
fun main() {
//sampleStart
val oddNumbers = generateSequence(1) { it + 2 } // `it` 是上一个元素
println(oddNumbers.take(5).toList())
//println(oddNumbers.count()) // 错误:此序列是无限的。
//sampleEnd
}
3.8.2 序列操作
关于序列操作,根据其状态要求可以分为以下几类:
- 无状态 操作不需要状态,并且可以独立处理每个元素,例如 map() 或 filter()。 无状态操作还可能需要少量常数个状态来处理元素,例如 take()与drop()。
- 有状态 操作需要大量状态,通常与序列中元素的数量成比例。
如果序列操作返回延迟生成的另一个序列,则称为 中间序列 。 否则,该操作为 末端 操作。 末端操作的示例为 toList() 或 sum()。只能通过末端操作才能检索序列元素。
序列可以多次迭代;但是,某些序列实现可能会约束自己仅迭代一次。其文档中特别提到了这一点
3.9 元组
Kotlin 本身并没有直接提供像 Swift 那样的内置元组(Tuple)类型,Kotlin 标准库提供了 Pair 和 Triple 类,可以用来表示两个或三个值的组合
与 Swift 的元组相比,Kotlin 的元组有以下限制:
-
标准库仅支持 Pair****和 Triple:对于超过三个值的组合,需要使用数据类或第三方库。
-
不可变性 :
Pair和Triple是不可变的,不能修改其值。 -
无索引访问 :不像 Swift 元组可以通过
.0、.1等索引访问值,Kotlin 的Pair和Triple必须通过first、second和third访问。val pair = Pair("Alice", 25)
println(pair.first) // 输出:Alice
println(pair.second) // 输出:25val triple = Triple("Alice", 25, true)
println(triple.first) // 输出:Alice
println(triple.second) // 输出:25
println(triple.third) // 输出:true
无论是 Pair、Triple 还是自定义数据类,都可以通过解构声明将值提取出来。
kotlin
val pair = Pair("Alice", 25)
val (name, age) = pair
println(name) // 输出:Alice
println(age) // 输出:25
val triple = Triple("Bob", 30, false)
val (name2, age2, isStudent) = triple
println(name2) // 输出:Bob
println(age2) // 输出:30
println(isStudent) // 输出:false
3.10 类型检测与自动类型转换
-
is操作符检测一个表达式是否某类型的一个实例。 -
如果一个不可变的局部变量或属性已经判断出为某类型,那么检测后的分支中可以直接当作该类型使用,无需显式转换
fun getStringLength(obj: Any): Int? {
//obj在&&右边自动转换成String类型
if (obj is String && obj.length > 0) {
return obj.length
}return null}
-
如果转换是不可能的,转换操作符会抛出一个异常。因此,称为 不安全的 。 Kotlin 中的不安全转换使用中缀操作符
asval x: String = y as String
请注意,null 不能转换为 String 因该类型不是可空的。 如果 y 为空,上面的代码会抛出一个异常。 为了让这样的代码用于可空值,请在类型转换的右侧使用可空类型:
val x: String? = y as String?
为了避免异常,可以使用安全 转换操作符 as?,它可以在失败时返回 null:
val x: String? = y as? String
四、控制
表达式可以用于赋值,语句只能用作函数体的部分逻辑控制
4.1 if表达式
在 Kotlin 中,if 是一个表达式:它会返回一个值。 因此就不需要三元运算符(条件 ? 然后 : 否则),因为普通的 if 就能胜任这个角色。
#if表达式
fun maxOf(a: Int, b: Int) = if (a > b) a else b
val maxLimit = 1
val maxOrLimit = if (maxLimit > a) maxLimit else if (a > b) a else b
#if 表达式的分支可以是代码块,这种情况最后的表达式作为该块的值:
val max = if (a > b) {
print("Choose a")
a
} else {
print("Choose b")
b
}
4.2 when 表达式
-
when既可以作为表达式使用也可以作为语句使用。 -
如果它被当做表达式, 第一个符合条件的分支的值就是整个表达式的值,如果当做语句使用, 则忽略个别分支的值。 类似于
if,每一个分支可以是一个代码块,它的值是块中最后的表达式的值。 -
如果其他分支都不满足条件将会求值
else分支。 如果when作为一个表达式 使用,那么必须有else分支, 除非编译器能够检测出所有的可能情况都已经覆盖了, 例如,对于 枚举(enum)类条目与密封(sealed)类子类型〕。#When表达式
fun describe(obj: Any): String =
when (obj) {
1 -> "One"
"Hello" -> "Greeting"
is Long -> "Long"
!is String -> "Not a string"
else -> "Unknown"
}when (x) {
0, 1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}when (x) {
s.toInt() -> print("s encodes x")
else -> print("s does not encode x")
}fun Request.getBody() =
when (val response = executeRequest()) {
is Success -> response.body
is HttpError -> throw HttpException(response.status)
}
4.3 for循环语句
-
for循环可以对任何提供迭代器(iterator)的对象进行遍历,这相当于像 C# 这样的语言中的foreach循环。 -
对区间或者数组的
for循环会被编译为并不创建迭代器的基于索引的循环#循环
val items = listOf("apple", "banana", "kiwifruit")
for (item in items) {
println(item)
}
for (index in items.indices) {
println("item at index is {items[index]}")
}fun main() {
//sampleStart
for (i in 1..3) {
println(i)
}
for (i in 6 downTo 0 step 2) {
println(i)
}
//sampleEnd
}fun main() {
val array = arrayOf("a", "b", "c")
//sampleStart
for (i in array.indices) {
println(array[i])
}
//sampleEnd
}fun main() {
val array = arrayOf("a", "b", "c")
//sampleStart
for ((index, value) in array.withIndex()) {
println("the element at index is value")
}
//sampleEnd
}
4.4 while循环语句
var index = 0
while (index < items.size) {
println("item at $index is ${items[index]}")
index++
}
4.5 跳转表达式和跳转标签
Kotlin 有三种结构化跳转表达式,这些表达式的类型是 Nothing 类型。
return默认从最直接包围它的函数或者匿名函数返回。break终止最直接包围它的循环。continue继续下一次最直接包围它的循环。
在 Kotlin 中任何表达式都可以用标签来标记。 标签的格式为标识符后跟 @符号,例如: **abc@**、 fooBar@****。 要为一个表达式加标签,我们只要在其前加标签即可
loop@ for (i in 1..100) {
for (j in 1..100) {
if (......) break@loop
}
}
//sampleStart
fun foo() {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return // 非局部直接返回到 foo() 的调用者
print(it)
}
println("this point is unreachable")
}
//sampleEnd
fun main() {
foo()
}
#现在,它只会从 lambda 表达式中返回
//sampleStart
fun foo() {
listOf(1, 2, 3, 4, 5).forEach lit@{
if (it == 3) return@lit // 局部返回到该 lambda 表达式的调用者------forEach 循环
print(it)
}
print(" done with explicit label")
}
//sampleEnd
fun main() {
foo()
}
#通常情况下使用隐式标签更方便,因为该标签与接受该 lambda 的函数同名。
//sampleStart
fun foo() {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return@forEach // 局部返回到该 lambda 表达式的调用者------forEach 循环
print(it)
}
print(" done with implicit label")
}
//sampleEnd
fun main() {
foo()
}
#或者,我们用一个匿名函数替代 lambda 表达式。 匿名函数内部的 return 语句将从该匿名函数自身返回
//sampleStart
fun foo() {
listOf(1, 2, 3, 4, 5).forEach(fun(value: Int) {
if (value == 3) return // 局部返回到匿名函数的调用者------forEach 循环
print(value)
})
print(" done with anonymous function")
}
//sampleEnd
fun main() {
foo()
}
#请注意,前文三个示例中使用的局部返回类似于在常规循环中使用 continue。
#并没有 break 的直接等价形式,不过可以通过增加另一层嵌套 lambda 表达式并从其中非局部返回来模拟
//sampleStart
fun foo() {
run loop@{
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return@loop // 从传入 run 的 lambda 表达式非局部返回
print(it)
}
}
print(" done with nested loop")
}
//sampleEnd
fun main() {
foo()
}
4.6 trycatch表达式
-
try-表达式的返回值是try块中的最后一个表达式或者是(所有)catch块中的最后一个表达式。finally块中的内容不会影响表达式的结果#try-catch 表达式
fun test() {
val result = try {
count()
} catch (e: ArithmeticException) {
throw IllegalStateException(e)
}// 使用 result}
4.6.1 Kotlin 没有受检异常
受检异常 是 Java 中的一种异常类型,它要求开发者在编译时显式处理可能抛出的异常。如果一个方法可能会抛出受检异常,则调用该方法的代码必须通过 try-catch 块捕获异常,或者通过 throws 关键字将异常向上抛出。
- 受检异常由编译器在编译阶段进行检查。
- 如果未正确处理受检异常,代码将无法通过编译。
以下是 JDK 中 StringBuilder 类实现的一个示例接口:
Appendable append(CharSequence csq) throws IOException;
这个签名是说,每次我追加一个字符串到一些东西(一个 StringBuilder、某种日志、一个控制台等)上时,我就必须捕获 IOException。 为什么?因为相应实现可能正在执行 IO 操作(Writer 也实现了 Appendable)。 其结果是这种代码随处可见:这并不好,看看《Effective Java》第三版 第 77 条:不要忽略异常 就知道了。
try {
log.append(message)
} catch (IOException e) {
// 必须要安全
}
|--------|-----------------------------------|--------------------------------------------|
| 特性 | Java | Kotlin |
| 受检异常 | 存在受检异常,编译器强制要求处理。 | 没有受检异常,所有异常均为非受检异常。 |
| 异常处理方式 | 必须通过 try-catch 或 throws 显式处理。 | 无需显式处理,但可以选择使用 try-catch 捕获异常。 |
| 设计理念 | 通过强制处理提高代码健壮性。 | 通过移除受检异常简化代码,提升灵活性。 |
在 Kotlin 中,所有异常都是非受检异常(unchecked exception)。开发者不需要显式捕获或声明异常。
log.append(message)
- 在 Kotlin 中,上述代码无需声明
throws或使用try-catch,即使 append 的方法可能会抛出IOException。
4.7 throw表达式和Nothing类型
在 Kotlin 中 throw 是表达式,所以你可以使用它(比如)作为 Elvis 表达式的一部分:
val s = person.name ?: throw IllegalArgumentException("Name required")
throw 表达式的类型是 Nothing 类型。 这个类型没有值,而是用于标记永远不能达到的代码位置。 在你自己的代码中,你可以使用 Nothing 来标记一个永远不会返回的函数:
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
当你调用该函数时,编译器会知道在该调用后就不再继续执行了:
val s = person.name ?: fail("Name required")
println(s) // 在此已知"s"已初始化
当处理类型推断时还可能会遇到这个类型。这个类型的可空变体 Nothing? 有一个可能的值是 null。如果用 null 来初始化一个要推断类型的值,而又没有其他信息可用于确定更具体的类型时,编译器会推断出 Nothing? 类型:
val x = null // "x"具有类型 `Nothing?`
val l = listOf(null) // "l"具有类型 `List<Nothing?>
4.8 typealias--类型别名
typealias NodeSet = Set<Network.Node>
typealias FileTable<K> = MutableMap<K, MutableList<File>>
#你可以为函数类型提供另外的别名:
typealias MyHandler = (Int, String, Any) -> Unit
typealias Predicate<T> = (T) -> Boolean
#你可以为内部类和嵌套类创建新名称:
class A {
inner class Inner
}
class B {
inner class Inner
}
typealias AInner = A.Inner
typealias BInner = B.Inner
4.8 suspend--挂起函数,协程
挂起函数属于函数类型的特殊种类,它的表示法中有一个 修饰符 ,例如 suspend () -> Unit 或者 suspend A.(B) -> C
五、函数
5.1 定义和调用
5.1.1 函数声明fun xx(x:xx): xx { xxx }
-
Kotlin 函数使用 fun****关键字声明
-
函数参数使用 Pascal 表示法定义------ name : type 。参数用逗号隔开, 每个参数必须有显式类型
-
函数参数可以有默认值,当省略相应的参数时使用默认值。这可以减少重载数量。通过在类型的后面添加
=符号来设置参数的默认值fun printSum(a: Int = 0, b: Int = 1): Unit {
println("sum of a and b is ${a + b}")
}#简单函数定义可以直接使用表达式
fun sum(a: Int, b: Int) = a + b
5.1.2 函数调用和具名实参
函数参数可以隐式按顺序传递,但是Kotlin支持具名调用
-
当一个方法接受多个相同的原生类型参数或者多个 Boolean 类型参数时,请使用具名实参语法, 除非在上下文中的所有参数的含义都已绝对清楚。
-
除了省略掉所有有默认值的参数,你也可以选择只省略某些特定的有默认值的参数。 但是你需要在跳过第一个参数后,对后续的所有参数都使用具名实参:
drawSquare(x = 10, y = 10, width = 100, height = 100, fill = true)
reformat("This is a short String!", upperCaseFirstLetter = false, wordSeparator = '_')
-
如果一个有默认值参数在一个无默认值的参数之前,那么该默认值只能通过使用具名实参调用该函数来使用:
fun foo(
bar: Int = 0,
baz: Int,
) { /....../ }foo(baz = 1) // 使用默认值 bar = 0
最后一个参数是lambda( 支持尾随闭包(Trailing Closure)语法**)**
如果函数的最后一个参数是一个 lambda 表达式(闭包),它可以以两种方式传递:
- 作为具名实参在括号内传入。
- 在括号外直接传入。
-
-
只有当闭包是函数的最后一个参数时,才可以将其移出括号外。
-
如果闭包不是最后一个参数,则必须在括号内传递。
fun foo(
bar: Int = 0,
baz: Int = 1,
qux: () -> Unit,
) { /....../ }foo(1) { println("hello") } // 使用默认值 baz = 1
foo(qux = { println("hello") }) // 使用两个默认值 bar = 0 与 baz = 1
foo { println("hello") } // 使用两个默认值 bar = 0 与 baz = 1
-
5.1.3 可变参数vararg
可以通过具名实参和展开 操作符来传入可变参数(vararg):
-
In Kotlin, you can pass a variable number of arguments to a function via the vararg parameter.
-
传递数组时用*
fun foo(vararg strings: String) { /....../ }
foo(strings = *arrayOf("a", "b", "c"))
-
函数的参数(通常是最后一个)可以用 vararg****修饰符标记:
fun <T> asList(vararg ts: T): List<T> {
val result = ArrayList<T>()
for (t in ts) // ts 是一个数组
result.add(t)
return result
}
在本例中,可以将可变数量的参数传递给函数:
val list = asList(1, 2, 3)
5.1.4 返回 Unit 的函数
如果一个函数并不返回有用的值,其返回类型是 Unit。Unit 是一种只有一个值------Unit 的类型。 这个值不需要显式返回:
fun printHello(name: String?): Unit {
if (name != null)
println("Hello $name")
else
println("Hi there!")
// `return Unit` 或者 `return` 是可选的
}
Unit****返回类型声明也是可选的。上面的代码等同于:
fun printHello(name: String?) { ...... }
5.1.5 单表达式函数
当函数体由单个表达式构成时,可以省略花括号并且在 = 符号之后指定代码体即可:
fun double(x: Int): Int = x * 2
当返回值类型可由编译器推断时,显式声明返回类型是可选的:
fun double(x: Int) = x * 2
5.1.6 中缀表示法
标有 infix 关键字的函数也可以使用中缀表示法(忽略该调用的点与圆括号)调用。 中缀函数必须满足以下要求:
-
它们必须是成员函数或扩展函数。
-
它们必须只有一个参数。
-
infix fun Int.shl(x: Int): Int { ...... }
// 用中缀表示法调用该函数
1 shl 2// 等同于这样
1.shl(2)
5.1.7 泛型函数
泛型:in、out、where · Kotlin 官方文档 中文版
函数可以有泛型参数,通过在函数名前使用尖括号指定:
fun <T> singletonList(item: T): List<T> { /*......*/ }
5.1.8 throws 标记一个函数可能会抛出错误
在 Kotlin 中,函数声明可以使用 throws 关键字来显式声明该函数可能抛出的受检异常(Checked Exceptions)。这是 Kotlin 与 Java 兼容性的一部分,因为 Kotlin 支持调用 Java 的受检异常方法。
fun functionName(...): ReturnType throws ExceptionType1, ExceptionType2 {
// 函数体
}
-
throws后面列出的是可能抛出的受检异常类型。 -
Kotlin 本身并没有强制要求处理受检异常,但当调用 Java 的受检异常方法时,
throws声明是必要的。import java.io.IOException
fun readFile(filePath: String): String throws IOException {
if (filePath.isEmpty()) {
throw IOException("File path is empty")
}
return "File content" // 模拟读取文件内容
}
除了受检异常外,Kotlin 还支持非受检异常(Unchecked Exceptions),例如 IllegalArgumentException、NullPointerException 等。对于这些异常,Kotlin 不要求显式声明或处理。
kotlin
fun validate(value: Int) {
if (value < 0) {
throw IllegalArgumentException("Value must be non-negative")
}
}
在这种情况下,不需要使用 throws 声明,因为 IllegalArgumentException 是非受检异常。
5.1.9 不支持rethrows 标记一个函数仅当其参数中的某个闭包抛出错误时才会抛出错误
在 Kotlin 中,没有直接与 Swift 的 rethrows****等价的关键字或机制 。然而,Kotlin 提供了其他方式来处理类似的需求,即根据闭包或函数参数是否抛出异常来决定是否重新抛出异常
在 Kotlin 中,可以通过泛型和 throws 声明来实现类似的功能。例如:
fun <T> executeOperation(operation: () -> T): T {
return operation()
}
fun <T> executeOperationWithException(operation: () -> T): T throws Exception {
return operation()
}
- 如果
operation是一个普通函数(不抛出异常),则调用executeOperation。 - 如果
operation可能抛出异常,则需要显式声明throws或使用受检异常
5.2 函数作用域
Kotlin 函数可以在文件顶层声明,这意味着你不需要像一些语言如 Java、C# 与 Scala (在 Scala 3 之后新增了顶层作用域的支持) 那样需要创建一个类来保存一个函数。此外除了顶层函数,Kotlin 中函数也可以声明在局部作用域、作为成员函数以及扩展函数。
5.2.1 局部函数
Kotlin 支持局部函数,即一个函数在另一个函数内部:
fun dfs(graph: Graph) {
fun dfs(current: Vertex, visited: MutableSet<Vertex>) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v, visited)
}
dfs(graph.vertices[0], HashSet())
}
局部函数可以访问外部函数(闭包)的局部变量。在上例中,visited 可以是局部变量:
fun dfs(graph: Graph) {
val visited = HashSet<Vertex>()
fun dfs(current: Vertex) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v)
}
dfs(graph.vertices[0])
}
5.2.2 成员函数
成员函数是在类或对象内部定义的函数:
class Sample {
fun foo() { print("Foo") }
}
成员函数以点表示法调用:
Sample().foo() // 创建类 Sample 实例并调用 foo
5.2.3 顶级函数
定义
- 定义在任何类、对象或接口之外的函数。
- 不需要通过类或对象实例调用。
特点
-
全局可用性:可以在同一个文件或其他文件中直接调用。
-
简洁性:避免了不必要的类包装
// 文件:MathUtils.kt
fun add(a: Int, b: Int): Int {
return a + b
}// 文件:Main.kt
fun main() {
val result = add(2, 3) // 直接调用顶级函数
println(result) // 输出:5
}
5.2.4 扩展函数
定义
- 为现有类添加新函数,而无需修改类的源代码。
- 通过在函数名前加上接收者类型来定义。
特点
-
灵活性:可以为第三方库中的类添加功能。
-
直观性:调用时看起来像是类的原生方法。
fun String.isPalindrome(): Boolean {
return this == this.reversed()
}fun main() {
val word = "madam"
println(word.isPalindrome()) // 输出:true
}
5.3 高阶函数
Kotlin 函数都是 头等的,这意味着它们可以存储在变量与数据结构中,并可以作为参数传给其他 高阶函数以及从其他高阶函数返回 。可以像操作任何其他非函数值一样对函数进行操作。
为促成这点,作为一门静态类型编程语言的 Kotlin 使用一系列函数类型来表示函数并提供一组特定的语言结构,例如 lambda 表达式。
5.3.1 (Int, Int) -> Int 函数类型声明和函数调用
- 高阶函数是将函数用作参数或返回值的函数。
- val onClick: (Int, Int) -> Int = {....}
- 类型声明
-
- 所有函数类型都有一个圆括号括起来的参数类型列表以及一个返回类型:
(A, B) -> C表示接受类型分别为A与B两个参数并返回一个C类型值的函数类型
- 所有函数类型都有一个圆括号括起来的参数类型列表以及一个返回类型:
-
-
- (Int, Int) -> Int 或者 (Int, Int) -> Unit
- (x: Int, y: Int) ->Int
-
-
- 参数类型列表可以为空,如
-
-
- () -> Unit
-
-
- 函数类型为可null,请使用圆括号
-
-
- ((Int, Int) -> Int)?
-
-
- 函数类型嵌套,如
-
-
- (Int) -> ((Int) -> Unit)
-
- 如果有足够信息,编译器可以推断变量的函数类型
-
-
val a = { i: Int -> i + 1 } // 推断出的类型是 (Int) -> Int
○ 高阶函数的一个不错的示例是集合的函数式风格的fold扩展参数, 它接受一个初始累积值与一个接合函数
○ 并通过将当前累积值与每个集合元素连续接合起来代入累积值来构建返回值:#参数 combine 具有函数类型 (R, T) -> R,因此 fold 接受一个函数作为参数,
该函数接受类型分别为 R 与 T 的两个参数并返回一个 R 类型的值
fun <T, R> Collection<T>.fold(
initial: R,
combine: (acc: R, nextElement: T) -> R
): R {
var accumulator: R = initial
for (element: T in this) {
accumulator = combine(accumulator, element)
}
return accumulator
}#为了调用 fold,需要传给它一个函数类型的实例作为参数, 而在高阶函数调用处,lambda 表达 式广泛用于此目的
fun main() {
//sampleStart
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 }) // 函数引用也可以用于高阶函数调用: val product = items.fold(1, Int::times) //sampleEnd println("joinedToString = $joinedToString") println("product = $product")}
-
[扩展函数形式] 带接受者的函数类型
- 函数类型可以有一个额外的接收者 类型,它在表示法中的点之前指定: 类型
A.(B) -> C表示可以在A的接收者对象上以一个B类型参数来调用并返回一个C类型值的函数。 带有接收者的函数字面值通常与这些类型一起使用
Kotlin 中,带接受者的函数类型(也称为扩展函数类型)可以作为函数参数传递,并且可以在函数内部对接受者使用成员函数或成员变量。这种函数类型的语法形式为 A.(B) -> C,其中 A 是接受者类型,B 是参数类型,C 是返回类型。
// 定义一个带接受者的函数类型
val greeting: String.() -> Unit = {
println("Hello, $this")
}
// 使用带接受者的函数类型
val name = "John"
name.greeting()
在这个示例中,我们首先定义了一个带接受者的函数类型 String.() -> Unit,其中 String 是接受者类型,() -> Unit 表示函数没有参数,并且没有返回值。这个函数类型表示可以在 String 对象上调用该函数。
接下来,我们创建了一个字符串 name,并使用带接受者的函数类型来调用 greeting 函数。在函数体内部,$this 表示接受者对象(即这里的字符串 name),所以最终会打印出 "Hello, John"。
[扩展函数形式] 带接受者的函数类型的转换
-
带与不带接收者的函数类型非字面 值可以互换,其中接收者可以替代第一个参数,反之亦然。例如,
(A, B) -> C类型的值可以传给或赋值给期待A.(B) -> C类型值的地方,反之亦然:fun main() {
//sampleStart
val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
val twoParameters: (String, Int) -> String = repeatFun // OKfun runTransformation(f: (String, Int) -> String): String { return f("hello", 3) } val result = runTransformation(repeatFun) // OK //sampleEnd println("result = $result")}
-
在这样的函数字面值内部,传给调用的接收者对象成为 隐式 的 this**,以便访问接收者对象的成员而无需任何额外的限定符,亦可使用** this表达式访问接收者对象
-
当接收者类型可以从上下文推断时,lambda 表达式可以用作带接收者的函数字面值。 One of the most important examples of their usage is type-safe builders:
class HTML {
fun body() { ...... }
}fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // 创建接收者对象
html.init() // 将该接收者对象传给该 lambda
return html
}html { // 带接收者的 lambda 由此开始
body() // 调用该接收者对象的一个方法
}
5.3.2 高阶函数的实例化
|----------------|--------------------------|----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 类别 | 方法 | 描述 | 示例 |
| Lambda 表达式 | { 参数 -> 函数体 } | 使用 Lambda 表达式创建函数类型的实例。 | val add: (Int, Int) -> Int = { a, b -> a + b } println(add(2, 3)) // 输出:5 |
| | { 函数体 } | 如果 Lambda 表达式只有一个参数,可以省略参数声明,直接使用隐式参数 it 。 | kotlin val square: (Int) -> Int = { it * it } println(square(4)) // 输出:16 |
| 匿名函数 | fun(参数): 返回值类型 { 函数体 } | 使用匿名函数创建函数类型的实例。 | val multiply = fun(a: Int, b: Int): Int { return a * b } println(multiply(2, 3)) // 输出:6 |
| 方法引用 | ::方法名 | 使用方法引用将现有函数赋值给函数类型变量。 | fun subtract(a: Int, b: Int): Int = a - b val operation: (Int, Int) -> Int = ::subtract println(operation(5, 3)) // 输出:2 |
| 构造函数引用 | 类名::构造函数 | 使用构造函数引用来创建函数类型的实例。 | class Person(val name: String) val createPerson: (String) -> Person = ::Person val person = createPerson("Alice") println(person.name) // 输出:Alice |
| 成员引用 | 对象::成员 | 引用类的成员属性或成员函数。 | class Calculator { fun add(a: Int, b: Int): Int = a + b } val calc = Calculator() val operation: (Int, Int) -> Int = calc::add println(operation(2, 3)) // 输出:5 |
| 高阶函数返回 | 在高阶函数中返回一个函数类型的实例。 | 高阶函数可以动态生成并返回函数类型的实例。 | fun makeOperation(isAddition: Boolean): (Int, Int) -> Int { return if (isAddition) { { a, b -> a + b } } else { { a, b -> a - b } } } val operation = makeOperation(true) println(operation(2, 3)) // 输出:5 |
方式一:使用函数字面值的代码块
- Lambda 表达式 { a, b -> a + b }
- 匿名函数 fun(s: String): Int { return s.toIntOrNull() ?: 0 }
- 带有接收者的函数字面值可用作带有接收者的函数类型的值
lambda 表达式与匿名函数是函数字面值,函数字面值即没有声明而是立即做为表达式传递的函数。考虑下面的例子:
max(strings, { a, b -> a.length < b.length })
函数 max 是一个高阶函数,因为它接受一个函数作为第二个参数。 其第二个参数是一个表达式,它本身是一个函数,称为函数字面值
方式二:使用已有声明的可调用引用:
方式三:使用实现函数类型接口的自定义类的实例:
class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO()
}
val intFunction: (Int) -> Int = IntTransformer()
5.3.3 高阶函数的调用
-
函数类型的值可以通过其 invoke(......)操作符调用:
f.invoke(x)或者直接f(x) -
也可以普通调用(xx)
fun main() {
//sampleStart
val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int = Int::plusprintln(stringPlus.invoke("<-", "->")) println(stringPlus("Hello, ", "world!")) println(intPlus.invoke(1, 1)) println(intPlus(1, 2)) println(2.intPlus(3)) // 类扩展调用 //sampleEnd}
5.3.4 闭包
在讲lambda表达式之前,我们还是先讲下闭包。**闭包本质上是一个函数类型的实例。**函数类型是闭包的类型描述。
- 函数类型只描述函数的签名,不包含具体实现。函数类型本身没有名字,它只是描述函数的签名。
- 闭包是函数类型的具体实现,可以是具名的(==函数)或匿名的(如闭包表达式)。
-
-
所有函数都是闭包,但并非所有闭包都是具名函数(闭包可以是匿名的)
// 具名闭包(函数)
fun multiply(a: Int, b: Int): Int {
return a * b
}// 匿名闭包
var multiplyClosure = { a: Int, b: Int ->
return a * b
}
-
由于函数和闭包共享相同的类型,它们可以在许多场景下互换使用
- 将函数传递给接受闭包的参数
- 将闭包赋值给函数类型变量
Lambda 是语法糖:最常用的闭包实现方式,但非唯一(函数引用、匿名函数均可)
**闭包是一个独立的代码块,可以捕获和存储上下文中的常量和变量,**闭包可能延长被捕获变量的生命周期
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)
对比Java和Swift
|------------|--------------------|-----------------|-----------------------------|
| 特性 | Java | Kotlin | Swift |
| 变量捕获 | 仅 final或等效 final | 可捕获并修改 | 可捕获并修改 |
| 变量修改 | 无法修改外部变量 | 可以 | 可以 |
| 语法简洁性 | 一般(Lambda 简化) | 高(尾随闭包支持) | 极高(多种简写形式) |
| 内存管理 | 自动垃圾回收 | 自动垃圾回收 | ARC + 捕获列表控制 |
| 闭包类型 | 函数式接口 | Lambda/匿名函数 | Closure |
| 循环引用处理 | 无特殊机制 | 需手动处理 | [weak self] 或 [unowned] |
| 典型用途 | 替代匿名内部类 | 函数式编程、DSL 设计 | 异步回调、UI 动画 |
5.3.5 实例方式:Lambda 表达式=闭包
表达式语法
Lambda 表达式的完整语法形式如下:
{ xx:xx参数列表 -> 函数体 }
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
#如果将所有可选标注都留下,看起来如下:
val sum = { x: Int, y: Int -> x + y }
- 参数列表:可以包含零个或多个参数,每个参数需要指定名称(可选)和类型(可选)。
- **->**符号:分隔参数列表和函数体。
- 函数体:实现具体的逻辑,最后一行表达式的结果会作为返回值。
- { x: Int, y: Int -> x + y }
-
- l**ambda 表达式总是括在花括号中。**在 lambda 表达式中,应该在花括号左右以及分隔参数与代码体的箭头左右留空
- 完整语法形式的参数声明放在花括号内,并有可选的类型标注。
- 函数体跟在一个 **->**之后。
- 如果推断出的该 lambda 的返回类型不是 Unit**,那么该 lambda 主体中的最后一个(或可能是单个)表达式会视为返回值。**
-
常见的写法
#在多行的 lambda 表达式中声明参数名时,将参数名放在第一行,后跟箭头与换行符:
appendCommaSeparated(properties) { prop ->
val propertyValue = prop.get(obj) // ......
}#如果参数列表太长而无法放在一行上,请将箭头放在单独一行:
foo {
context: Context,
environment: Env
->
context.configureEnv(environment)
} -
如果为 lambda 表达式分配一个标签,那么不要在该标签与左花括号之间留空格:
fun foo() {
ints.forEach lit@{
// ......
}
} -
下划线用于未使用的变量。如果 lambda 表达式的参数未使用,那么可以用下划线取代其名称:
map.forEach { (_, value) -> println("$value!") }
省略参数类型
如果编译器可以根据上下文推断出参数类型,则可以省略参数类型。
单表达式闭包省略 return, 和上文单表达式函数一样
如果闭包的函数体只有一个表达式,可以省略 return 关键字,编译器会自动返回该表达式的结果。
无参数的 Lambda 表达式
如果闭包没有参数,可以直接省略参数列表。
it单个参数的隐式名称
一个 lambda 表达式只有一个参数很常见。
If the compiler can parse the signature without any parameters, the parameter does not need to be declared and **->**can be omitted. 该参数会隐式声明为 it:
ints.filter { it > 0 } // 这个字面值是"(it: Int) -> Boolean"类型的
支持尾随闭包(Trailing Closure)语法
-
如果一个调用接受单个 lambda 表达式,尽可能将其放在圆括号外边传入
list.filter { it > 10 }
list.filter ( { it -> it > 10 } ) -
如果函数的最后一个参数是函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外:
val product = items.fold(1) { acc, e -> acc * e }
从 lambda 表达式中返回一个值
- 可以使用 限定的返回语法从 lambda 显式返回一个值。 否则,将隐式返回最后一个表达式的值。
因此,以下两个片段是等价的:
ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
lambda表达式中中止外部函数
在 Kotlin 中,只能对具名或匿名函数使用正常的、非限定的 return 来退出。 要退出一个 lambda 表达式,需要使用一个标签。 在 lambda 表达式内部禁止使用裸 return**,因为 lambda 表达式不能使包含它的函数返回:**
fun ordinaryFunction(block: () -> Unit) {
println("hi!")
}
//sampleStart
fun foo() {
ordinaryFunction {
return // 错误:不能使 `foo` 在此处返回
}
}
//sampleEnd
fun main() {
foo()
}
但是如果 lambda 表达式传给的函数是内联的,该 return 也可以内联。因此可以这样:
inline fun inlined(block: () -> Unit) {
println("hi!")
}
//sampleStart
fun foo() {
inlined {
return // OK:该 lambda 表达式是内联的
}
}
//sampleEnd
fun main() {
foo()
}
5.3.6 匿名函数
-
上文的 lambda 表达式语法缺少一个东西------指定函数的返回类型的能力。在大多数情况下, 这是不必要的。因为返回类型可以自动推断出来。然而,如果确实需要显式指定, 可以使用另一种语法: 匿名函数 。
-
Lambda表达式与匿名函数之间的另一个区别是非局部返回的行为。 一个不带标签的
return语句总是在用fun关键字声明的函数中返回。这意味着 lambda 表达式中的 return****将从包含它的函数返回,而匿名函数中的 return****将从匿名函数自身返回fun(x: Int, y: Int): Int = x + y
匿名函数看起来非常像一个常规函数声明,除了其名称省略了。其函数体既可以是表达式(如上所示)也可以是代码块:
fun(x: Int, y: Int): Int {
return x + y
}
参数和返回类型的指定方式与常规函数相同,除了能够从上下文推断出的参数类型可以省略:
ints.filter(fun(item) = item > 0)
匿名函数的返回类型推断机制与正常函数一样:对于具有表达式函数体的匿名函数将自动推断返回类型,但具有代码块函数体的返回类型必须显式指定(或者已假定为 Unit)
当匿名函数作为参数传递时,需将其放在括号内。 允许将函数留在圆括号外的简写语法仅适用于 lambda 表达式。
5.3.7 尾递归函数
Kotlin 支持一种称为尾递归的函数式编程风格。 对于某些使用循环的算法,可以使用尾递归替代而不会有堆栈溢出的风险。 当一个函数用 tailrec****修饰符标记并满足所需的形式条件时,编译器会优化该递归, 留下一个快速而高效的基于循环的版本:
val eps = 1E-10 // "good enough", could be 10^-15
tailrec fun findFixPoint(x: Double = 1.0): Double =
if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))
这段代码计算余弦的不动点(fixpoint of cosine),这是一个数学常数。 它只是重复地从 1.0 开始调用 Math.cos, 直到结果不再改变,对于这里指定的 eps 精度会产生 0.7390851332151611 的结果。最终代码相当于这种更传统风格的代码:
val eps = 1E-10 // "good enough", could be 10^-15
private fun findFixPoint(): Double {
var x = 1.0
while (true) {
val y = Math.cos(x)
if (Math.abs(x - y) < eps) return x
x = Math.cos(x)
}
}
要符合 tailrec 修饰符的条件的话,函数必须将其自身调用作为它执行的最后一个操作。在递归调用后有更多代码时, 不能使用尾递归,不能用在 try/catch/finally 块中,也不能用于 open 的函数。 目前在 Kotlin for the JVM 与 Kotlin/Native 中支持尾递归
5.4 内联函数
使用高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭包。 闭包那些在函数体内会访问到的变量的作用域。 内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销。但是在许多情况下通过内联化 lambda 表达式可以消除这类的开销。
- inline****修饰符影响函数本身和传给它的 lambda 表达式:所有这些都将内联到调用处。
- 内联可能导致生成的代码增加。不过如果使用得当(避免内联过大函数), 性能上会有所提升,尤其是在循环中的"超多态(megamorphic)"调用处。
使用 inline 修饰符标记 lock() 函数:
inline fun <T> lock(lock: Lock, body: () -> T): T { ...... }
调用使用内联函数
lock(l) { foo() }
编译器没有为参数创建一个函数对象并生成一个调用。取而代之,编译器可以生成以下代码:
l.lock()
try {
foo()
} finally {
l.unlock()
}
5.4.1 内联函数中的部分参数不内联
-
如果不希望内联所有传给内联函数的 lambda 表达式参数都内联,那么可以用 noinline****修饰符标记不希望内联的函数参数:
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { ...... }
可以内联的 lambda 表达式只能在内联函数内部调用或者作为可内联的参数传递。 但是 noinline 的 lambda 表达式可以以任何喜欢的方式操作,包括存储在字段中、或者进行传递。
5.4.2 公有 API 内联函数的限制
当一个内联函数是 public 或 protected 而不是 private 或 internal 声明的一部分时, 就会认为它是一个模块级的公有 API。可以在其他模块中调用它,并且也可以在调用处内联这样的调用。
这带来了一些由模块做这样变更时导致的二进制兼容的风险------ 声明一个内联函数但调用它的模块在它修改后并没有重新编译。
为了消除这种由非 公有 API 变更引入的不兼容的风险,公有 API 内联函数体内不允许使用非公有声明,即,不允许使用 private 与 internal 声明以及其部件
5.4.3 内联属性
inline 修饰符可用于没有幕后字段的属性的访问器。 你可以标注独立的属性访问器:
val foo: Foo
inline get() = Foo()
var bar: Bar
get() = ......
inline set(v) { ...... }
你也可以标注整个属性,将它的两个访问器都标记为内联(inline):
inline var bar: Bar
get() = ......
set(v) { ...... }
在调用处,内联访问器如同内联函数一样内联。
5.4.4 具体化的类型参数
有时候需要访问一个作为参数传递的一个类型:
fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {
var p = parent
while (p != null && !clazz.isInstance(p)) {
p = p.parent
}
@Suppress("UNCHECKED_CAST")
return p as T?
}
在这里向上遍历一棵树并且检测每个节点是不是特定的类型。 这都没有问题,但是调用处不是很优雅:
treeNode.findParentOfType(MyTreeNode::class.java)
更好的解决方案是只要传一个类型给该函数,可以按以下方式调用它:
treeNode.findParentOfType<MyTreeNode>()
为能够这么做,内联函数支持 具体化的类型参数,于是可以这样写:
inline fun <reified T> TreeNode.findParentOfType(): T? {
var p = parent
while (p != null && p !is T) {
p = p.parent
}
return p as T?
}
上述代码使用 reified 修饰符来限定类型参数使其可以在函数内部访问它, 几乎就像是一个普通的类一样。由于函数是内联的,不需要反射,正常的操作符如 !is 和 as 现在均可用。此外,还可以按照如上所示的方式调用该函数:myTree.findParentOfType<MyTreeNodeType>()。
5.5 函数覆盖
-
覆盖方法总是使用与基类型方法相同的默认实参值。 当覆盖一个有默认实参值的方法时,必须从签名中省略默认实参值:
open class A {
open fun foo(i: Int = 10) { /....../ }
}class B : A() {
override fun foo(i: Int) { /....../ } // 不能有默认值
}
5.6 操作符和重载
5.6.1 相等与不等操作符
|----------|-----------------------------------|
| 表达式 | 翻译为 |
| a == b | a?.equals(b) ?: (b === null) |
| a != b | !(a?.equals(b) ?: (b === null)) |
这些操作符只使用函数 equals(other: Any?): Boolean, 可以覆盖它来提供自定义的相等性检测实现。不会调用任何其他同名函数(如 equals(other: Foo))。
=== 和 !==(同一性检测)不可重载,因此不存在对他们的约定。
这个 == 操作符有些特殊:它被翻译成一个复杂的表达式,用于筛选 null 值。 null == null 总是 true,对于非空的 x,x == null 总是 false 而不会调用 x.equals()。
六、类
class Person
#创建对象,不需要new关键字
val Person = Person("yhf")
一个类的内容应按以下顺序排列:
- 属性声明与初始化块
- 次构造函数
- 方法声明
- 伴生对象
6.1 构造函数
在 Kotlin 中的一个类有一个主构造函数并可能有一个或多个次构造函数
- 主构造函数在类头中声明,它跟在类名与可选的类型参数后
-
- 主构造的参数可以在初始化块中使用
- 声明类属性时,可以使用尾部逗号
- 如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字
-
val表示该参数会被转换为一个只读属性(read-only property)var表示该参数会被转换为一个可变属性(mutable property)- 如果不加
val或var,主构造函数中的参数不会成为类的属性,而仅仅作为构造函数的一部分
-
如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是 public。
class Person constructor(firstName: String) { /....../ }
class Person(val firstName: String, val lastName: String, var age: Int, var isEmployed: Boolean = true) {
//主构造的参数可以在初始化块中使用
val name = firstName + lastName
}#声明类属性时,可以使用尾部逗号:
class Person(
val firstName: String,
val lastName: String,
var age: Int, // 尾部逗号
) { /....../ }#如果你不希望你的类有一个公有构造函数,那么声明一个带有非默认可见性的空的主构造函数:
class DontCreateMe private constructor() { /....../ } -
在 JVM 上,如果主构造函数的所有的参数都有默认值,编译器会生成一个额外的无参构造函数,它将使用默认值。
-
这使得 Kotlin 更易于使用像 Jackson 或者 JPA 这样的通过无参构造函数创建类的实例的库。
6.1.1 次构造函数
类也可以声明前缀有 constructor的次构造函数
- 如果类有一个主构造函数,每个次构造函数需要委托给主构造函数, 可以直接委托或者通过别的次构造函数间接委托。
-
-
委托到同一个类的另一个构造函数用 this****关键字即可:
class Person(val name: String) {
val children: MutableList<Person> = mutableListOf()
constructor(name: String, parent: Person) : this(name) {
parent.children.add(this)
}
}
-
初始化块中的代码实际上会成为主构造函数的一部分。
对主构造函数的委托发生在访问次构造函数的第一条语句时, 因此所有初始化块与属性初始化器中的代码都会在次构造函数体之前执行
6.2 可见性修饰符
在 Kotlin 中有这四个可见性修饰符:private、 protected、 internal 和 public。 默认可见性都是 public**。**
- 类、对象、接口、构造函数、方法与属性及其 setter 都可以有 可见性修饰符 。 getter 总是与属性有着相同的可见性。
-
- 类对象接口
-
-
- 如果你不使用任何可见性修饰符,默认为
public,这意味着你的声明将随处可见。 - 如果你声明为
private,它只会在声明它的文件内可见。 - 如果你声明为
internal,它会在相同模块内随处可见。 protected修饰符不适用于顶层声明。
- 如果你不使用任何可见性修饰符,默认为
-
-
- 构造函数
-
-
- 默认情况下,所有构造函数都是公开(
public)的,这实际上等于类可见的地方它就可见 - class C private constructor(a: Int) { ...... }
- 默认情况下,所有构造函数都是公开(
-
-
- 类方法和属性
-
-
private意味着只该成员在这个类内部(包含其所有成员)可见;protected意味着该成员具有与private一样的可见性,但也在子类中可见。internal意味着能见到类声明的本模块内 的任何客户端都可见其internal成员。public意味着能见到类声明的任何客户端都可见其public成员
-
-
-
-
- 如果你覆盖一个
protected或internal成员并且没有显式指定其可见性,该成员还会具有与原始成员相同的可见性。
- 如果你覆盖一个
-
-
- 局部的变量、函数和类不能有可见性修饰符。
6.3 属性
属性对应的,一定有setter,getter方法,主要是对成员变量赋值,取值的。而成员变量=字段,真的只是一个变量。
在Kotlin中,属性是一个声明的接口,通过getter和setter方法实现,而字段变量是属性的底层存储机制, 这是Kotlin不同于Java的。在Kotlin中,属性可以通过委托实现,这样可以隐藏字段的实现细节,并提供额外的逻辑。
Swift 中的属性没有对应的实例变量,属性的后端存储也无法直接访问。这就避免了不同场景下访问方式的困扰,同时也将属性的定义简化成一个语句。
6.3.1 属性声明和Getter\Setter
- 声明一个属性的完整语法如下: 其初始器(initializer)、getter 和 setter 都是可选的
-
-
属性必须初始化,要么声明时,要么构造函数中;要么延迟初始化
-
属性类型如果可以从初始器, 或其 getter 的返回值(如下文所示)中推断出来,也可以省略:
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]class Person(val name: String, val age: Int)
class Person(var name: String, var age: Int)//自定义 Getter
val area: Int
get() = width * height
//自定义 Setter
var name: String = "Default Name"
set(value) {
field = value.trim() // 去除首尾空格
}
//完整自定义
var name: String = "Default Name"
set(value) {
field = value.trim() // 自定义 setter,去除首尾空格
}
get() {
return field.uppercase() // 自定义 getter,返回大写名称
}属性类型如果可以从初始器, 或其 getter 的返回值(如下文所示)中推断出来,也可以省略:
var initialized = 1 // 类型 Int、默认 getter 和 setter
// var allByDefault // 错误:需要显式初始化器,隐含默认 getter 和 setterval simple: Int? // 类型 Int、默认 getter、必须在构造函数中初始化
val inferredType = 1 // 类型 Int 、默认 getter
-
-
一个只读属性的语法和一个可变的属性的语法有两方面的不同:
-
-
1、只读属性的用 val****而不是 var****声明
-
2、只读属性不允许 setter
val count = 0
#对于非常简单的只读属性,请考虑单行格式:
val isEmpty: Boolean get() = size == 0#对于更复杂的属性,总是将 get 与 set 关键字放在不同的行上:
val foo: String
get() { /....../ }
-
-
可以为属性定义自定义的访问器。
-
-
如果定义了一个自定义的 getter,那么每次访问该属性时都会调用它 (这让可以实现计算出的属性)。
//sampleStart
class Rectangle(val width: Int, val height: Int) {
val area: Int // property type is optional since it can be inferred from the getter's return type
get() = this.width * this.height如果可以从 getter 推断出属性类型,则可以省略它: //val area get() = this.width * this.height}
//sampleEnd
fun main() {
val rectangle = Rectangle(3, 4)
println("Width={rectangle.width}, height={rectangle.height}, area=${rectangle.area}")
}
-
-
如果定义了一个自定义的 setter,那么每次给属性赋值时都会调用它, except its initialization. 一个自定义的 setter 如下所示:
var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value) // 解析字符串并赋值给其他属性
}
按照惯例,setter 参数的名称是 value,但是如果你喜欢你可以选择一个不同的名称
-
如果需要改变对一个访问器进行注解或者改变其可见性,但是不希望改变默认的实现, 那么可以定义访问器而不定义其实现:
var setterVisibility: String = "abc"
private set // 此 setter 是私有的并且有默认实现var setterWithAnnotation: Any? = null
@Inject set // 用 Inject 注解此 setter
幕后字段(实例变量)
-
在 Kotlin 中,字段仅作为属性的一部分在内存中保存其值时使用。这个幕后字段可以使用 field****标识符在访问器中引用
-
字段不能直接声明。 然而,当一个属性需要一个幕后字段时,Kotlin 会自动提供。
#field 标识符只能用在属性的访问器内。
var counter = 0 // 这个初始器直接为幕后字段赋值
set(value) {
if (value >= 0)
field = value
// counter = value // ERROR StackOverflow: Using actual name 'counter' would make setter recursive
}
如果属性至少一个访问器使用默认实现, 或者自定义访问器通过 field 引用幕后字段,将会为该属性生成一个幕后字段。例如,以下情况下就没有幕后字段:
val isEmpty: Boolean
get() = this.size == 0
幕后属性(直接使用实例变量)
如果你的需求不符合这套隐式的幕后字段 方案, 那么总可以使用 幕后属性(backing property):
private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
get() {
if (_table == null) {
_table = HashMap() // 类型参数已推断出
}
return _table ?: throw AssertionError("Set to null by another thread")
}
对于 JVM 平台:通过默认 getter 和 setter 访问私有属性会被优化以避免函数调用开销
如果一个类有两个概念上相同的属性,一个是公共 API 的一部分,另一个是实现细节,那么使用下划线作为私有属性名称的前缀
class C {
private val _elementList = mutableListOf<Element>()
val elementList: List<Element>
get() = _elementList
}
6.3.2 延迟初始化属性与变量

属性声明时未初始化的必须在构造函数中初始化。 然而,这经常不方便。例如:属性可以通过依赖注入来初始化, 或者在单元测试的 setup 方法中初始化。 这种情况下,你不能在构造函数内提供一个非空初始器。 但你仍然想在类体中引用该属性时避免空检测。
有时并不能定义一个变量或对象值为空,而也没办法在对象或变量在定义声明时就为它赋值初始化,那么这时就需要用到Kotlin提供的延迟初始化lateinit。
另外,在Android中,有些写在外层的View对象,如Button,TextView虽然可以通过?+null定义,但在使用时候必须加很多 ?.,显得代码臃肿多余,Kotlin设计出lateinit,这样开发者又可以像写Java那样,直接 button. 了,而不是 button?.
为处理这种情况,你可以用 lateinit****修饰符标记该属性:
Kotlin语言假定开发者在使用该lateinit变量或对象时,开发者会完成初始化赋值、保证提供非空(null)值(然而,天知道呢!也许开发者还是会忘记!)。lateinit关键词只是告诉编译器,不要在编译阶段因为变量没有初始化赋值而报错。
class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
var state: String? = null
var zip: String = "123456"
!!!Property must be initialized or be abstract
var state: String
var simple: Int? // 类型 Int、默认 getter、必须在构造函数中初始化
constructor(simple: Int?) {
this.simple = simple
}
}
-
该修饰符只能用于在类体中的属性(不是在主构造函数中声明的 var****属性, 并且仅当该属性没有自定义 getter 或 setter 时),也用于顶层属性与局部变量。 该属性或变量必须为非空类型,并且不能是原生类型。
public class MyTest {
lateinit var subject: TestSubject
}
判断延迟属性是否初始化
-
在初始化前访问一个 lateinit****属性会抛出一个特定异常,该异常明确标识该属性被访问及它没有初始化的事实。要检测一个 lateinit var****是否已经初始化过,请在 该属性的引用上使用 .isInitialized**:**
#类内调用
if (::subject.isInitialized) {
println(subject)
}
#跨类调用
if (foo::bar.isInitialized) {
println(foo.bar)
}
6.3.3 属性委托
- Kotlin 支持 委托属性,语 法是:
val/var <属性名>: <类型>by <表达式>。 - 在
by后面的表达式是该 委托 , 因为属性对应的get()(与set())会被委托给它的getValue()与setValue()方法。
-
-
属性的委托不必实现接口,但是需要提供一个
getValue()函数(对于var属性还有setValue()) -
它的第一个参数是读出
p的对象、第二个参数保存了对p自身的描述class Example {
var p: String by DelegateYHF()
}class DelegateYHF {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "thisRef, thank you for delegating '{property.name}' to me!"
}operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("$value has been assigned to '${property.name}' in $thisRef.") }}
val e = Example()
println(e.p)
输出结果:
Example@33a17727, thank you for delegating 'p' to me!e.p = "NEW"
输出结果:
NEW has been assigned to 'p' in Example@33a17727.
-
委托类提供函数
对于一个只读 属性(即 val 声明的),委托必须提供一个操作符函数 getValue(),该函数具有以下参数:
thisRef必须与属性所有者类型(对于扩展属性必须是被扩展的类型)相同或者是其超类型。property必须是类型KProperty<*>或其超类型。
getValue() 必须返回与属性相同的类型(或其子类型)。
class Resource
class Owner {
val valResource: Resource by ResourceDelegate()
}
class ResourceDelegate {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return Resource()
}
}
对于一个可变 属性(即 var 声明的),委托必须额外提供一个操作符函数 setValue(), 该函数具有以下参数:
-
thisRef必须与属性所有者类型(对于扩展属性必须是被扩展的类型)相同或者是其超类型。 -
property必须是类型KProperty<*>或其超类型。 -
value必须与属性类型相同(或者是其超类型)。class Resource
class Owner {
var varResource: Resource by ResourceDelegate()
}class ResourceDelegate(private var resource: Resource = Resource()) {
operator fun getValue(thisRef: Owner, property: KProperty<>): Resource {
return resource
}
operator fun setValue(thisRef: Owner, property: KProperty<>, value: Any?) {
if (value is Resource) {
resource = value
}
}
}
getValue() 或/与 setValue() 函数可以通过委托类的成员函数提供或者由扩展函数提供。 当你需要委托属性到原本未提供的这些函数的对象时后者会更便利。 两函数都需要用 operator 关键字来进行标记。
kotlin实现属性委托的原理
Kotlin 编译器会为某些类型的委托属性生成辅助属性并委托给它们。例如下边实例,对于属性 prop,生成隐藏属性 prop$delegate,而访问器的代码只是简单地委托给这个附加属性:
class C {
var prop: Type by MyDelegate()
}
// 这段是由编译器生成的相应代码:
class C {
private val prop$delegate = MyDelegate()
var prop: Type
get() = prop$delegate.getValue(this, this::prop)
set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}
延迟属性Lazy properties
延迟属性和延迟初始化属性是两回事
-
lazy() 是接受一个 lambda 并返回一个
Lazy <T>实例的函数,返回的实例可以作为实现延迟属性的委托。 -
第一次调用
get()会执行已传递给lazy()的 lambda 表达式并记录结果。 后续调用get()只是返回记录的结果。val lazyValue: String by lazy {
println("computed!")
"Hello"
}fun main() {
println(lazyValue)
println(lazyValue)
}
默认情况下,对于 lazy 属性的求值是同步锁的(synchronized):该值只在一个线程中计算,但所有线程都会看到相同的值。
可观察属性 Observable properties
-
每当我们给属性赋值时会调用该处理程序(在赋值 后 执行)。它有三个参数:被赋值的属性、旧值与新值
-
Delegates.observable() 接受两个参数:初始值与修改时处理程序(handler)
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("old -> new")
}
}fun main() {
val user = User()
user.name = "first"
user.name = "second"
} -
如果你想截获赋值并否决 它们,那么使用 vetoable() 取代
observable()。 在属性被赋新值 之前 会调用传递给 vetoable****的处理程序。
委托给另一个属性
一个属性可以把它的 getter 与 setter 委托给另一个属性。这种委托对于顶层和类的属性(成员和扩展)都可用。该委托属性可以为:
- 顶层属性
- 同一个类的成员或扩展属性
- 另一个类的成员或扩展属性
为将一个属性委托给另一个属性,应在委托名称中使用 :: 限定符,例如,this::delegate 或 MyClass::delegate。
var topLevelInt: Int = 0
class ClassWithDelegate(val anotherClassInt: Int)
class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
var delegatedToMember: Int by this::memberInt
var delegatedToTopLevel: Int by ::topLevelInt
val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt
将属性储存在映射中
一个常见的用例是在一个映射(map)里存储属性的值。 这经常出现在像解析 JSON 或者执行其他"动态"任务的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
fun main() {
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
//sampleStart
println(user.name) // Prints "John Doe"
println(user.age) // Prints 25
//sampleEnd
}
这也适用于 var 属性,如果把只读的 Map 换成 MutableMap 的话:
class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}
局部委托属性
你可以将局部变量声明为委托属性。 例如,你可以使一个局部变量惰性初始化:
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
memoizedFoo 变量只会在第一次访问时计算。 如果 someCondition 失败,那么该变量根本不会计算。
6.4 继承
6.4.1 公共超类
在 Kotlin 中所有类都有一个共同的超类 Any,对于没有超类型声明的类它是默认超类:
class Example // 从 Any 隐式继承
Any 有三个方法:equals()、 hashCode() 与 toString()。因此,为所有 Kotlin 类都定义了这些方法。
|-----------------|---------------------------------------------------|------------------------------------|
| 特性 | Kotlin | Swift |
| 共同超类 | 所有类都隐式继承自 Any。 | 没有默认的共同超类。 |
| Any的作用 | 表示所有类型的共同基类,包含 equals、hashCode和 toString方法。 | 表示任何类型的值(包括类、结构体、枚举、函数等),但不提供默认方法。 |
| AnyObject 的作用 | 不适用。 | 表示任何类类型的实例,主要用于与 Objective-C 交互。 |
| 类型转换 | 使用 is和 as进行类型检查和转换。 | 使用 is和 as?/as!进行类型检查和转换。 |
6.4.2 类默认不可继承和实现继承类
-
默认情况下,Kotlin 类是最终(final)的------它们不能被继承。 要使一个类可继承,请用
open关键字标记它 -
类之间继承由冒号(:)声明。
open class Base // 该类开放继承
#如需声明一个显式的超类型,请在类头中把超类型放到冒号之后:
open class Base(p: Int)
class Derived(p: Int) : Base(p)open class Shape
class Rectangle(val height: Double, val length: Double): Shape() {
val perimeter = (height + length) * 2
} -
如果派生类有一个主构造函数,其基类可以(并且必须)根据其参数在该主构造函数中初始化。
-
如果派生类没有主构造函数,那么每个次构造函数必须使用
super关键字初始化其基类型,或委托给另一个做到这点的构造函数。
-
-
请注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数:
class MyView : View {
constructor(ctx: Context) : super(ctx)constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)}
-
6.4.3 方法和属性覆盖open``override
- 可以被覆盖的函数必须open修饰, 否则子类中不允许定义相同签名的函数, 无论子类函数加不加 override
- 子类重写的函数上必须加上 override****修饰符。如果没写,编译器会报错
-
-
将 open****修饰符添加到 final 类的成员上不起作用(即没有 open****的类)。
open class Shape {
open fun draw() { /....../ }
#如果函数没有标注 open 如 Shape.fill(),
#那么子类中不允许定义相同签名的函数, 无论加不加 override
fun fill() { /....../ }
}class Circle() : Shape() {
override fun draw() { /....../ }
}#标记为 override 的成员本身是开放的,因此可以在子类中覆盖。
#如果你想禁止再次覆盖, 使用 final 关键字:
open class Rectangle() : Shape() {
final override fun draw() { /....../ }
}
-
-
属性与方法的覆盖机制相同。在超类中声明然后在派生类中重新声明的属性必须以 override****开头,并且它们必须具有兼容的类型
-
-
你也可以用一个
var属性覆盖一个val属性,但反之则不行。 这是允许的,因为一个val属性本质上声明了一个get方法, 而将其覆盖为var只是在子类中额外声明一个set方法open class Shape {
open val vertexCount: Int = 0
}class Rectangle : Shape() {
override val vertexCount = 4
}
-
6.4.4 调用超类super
派生类中的代码可以使用 super****关键字调用其超类的函数与属性访问器的实现
open class Rectangle {
open fun draw() { println("Drawing a rectangle") }
val borderColor: String get() = "black"
}
class FilledRectangle : Rectangle() {
override fun draw() {
super.draw()
println("Filling the rectangle")
}
val fillColor: String get() = super.borderColor
}
6.4.5 接口和类覆盖冲突的规则
- 一个类同时继承接口和抽象类,指向同一个函数
在 Kotlin 中,实现继承由下述规则规定:如果一个类从它的直接超类继承相同成员的多个实现, 它必须覆盖这个成员并提供其自己的实现(也许用继承来的其中之一)。
如需表示采用从哪个超类型继承的实现,请使用由尖括号中超类型名限定的 super ,如 super<Base>:
open class Rectangle {
open fun draw() { /* ...... */ }
}
interface Polygon {
fun draw() { /* ...... */ } // 接口成员默认就是"open"的
}
class Square() : Rectangle(), Polygon {
// 编译器要求覆盖 draw():
override fun draw() {
super<Rectangle>.draw() // 调用 Rectangle.draw()
super<Polygon>.draw() // 调用 Polygon.draw()
}
}
可以同时继承 Rectangle 与 Polygon, 但是二者都有各自的 draw() 实现,所以必须在 Square 中覆盖 draw(), 并为其提供一个单独的实现以消除歧义。
6.5 抽象类与接口
6.5.1 抽象类
- 类以及其中的某些或全部成员可以声明为 abstract**。 抽象成员在本类中可以不用实现。**
-
-
open用来标注类可以被集成,但是并不需要用open标注抽象类或者函数。abstract class Polygon {
abstract fun draw()
}class Rectangle : Polygon() {
override fun draw() {
// draw the rectangle
}
}
-
6.5.2 接口
- 使用关键字 interface****来定义接口
-
-
在接口中没有方法体时默认为抽象
-
Kotlin 的接口可以既包含抽象方法的声明也包含实现。
interface MyInterface {
fun bar()
fun foo() {
// 可选的方法体
}
}#一个类或者对象可以实现一个或多个接口:
class Child : MyInterface {
override fun bar() {
// 方法体
}
}
-
-
与抽象类不同的是,接口无法保存状态。它可以有属性但必须声明为抽象或提供访问器实现。
-
- 可以在接口中定义属性。在接口中声明的属性要么是抽象的,要么提供访问器的实现。
-
-
- 在接口中声明的属性不能有幕后字段(backing field),因此接口中声明的访问器不能引用它们
interface MyInterface {
val prop: Int // 抽象的val propertyWithImplementation: String get() = "foo" fun foo() { print(prop) }}
class Child : MyInterface {
override val prop: Int = 29
}
-
一个接口可以从其他接口派生,意味着既能提供基类型成员的实现也能声明新的函数与属性。很自然地,实现这样接口的类只需定义所缺少的实现:
interface Named {
val name: String
}
interface Person : Named {
val firstName: String
val lastName: String
override val name: String get() = "$firstName $lastName"
}
data class Employee(
// 不必实现"name"
override val firstName: String,
override val lastName: String,
val position: Position
) : Person
实现多个接口时,可能会遇到同一方法继承多个实现的问题:
interface A {
fun foo() { print("A") }
fun bar()
}
interface B {
fun foo() { print("B") }
fun bar() { print("bar") }
}
class C : A {
override fun bar() { print("bar") }
}
class D : A, B {
override fun foo() {
super<A>.foo()
super<B>.foo()
}
override fun bar() {
super<B>.bar()
}
}
6.5.3 函数式(SAM)接口
- 只有一个抽象方法的接口称为 函数式接口 或 单一抽象方法(SAM) 接口。可以用 fun****修饰符在 Kotlin 中声明一个函数式接口。
-
-
函数式接口可以有多个非抽象成员,但只能有一个抽象成员。
fun interface KRunnable {
fun invoke()
}
-
对于函数式接口,可以通过 lambda 表达式实现 SAM 转换,从而使代码更简洁、更有可读性。
例如,有这样一个 Kotlin 函数式接口:
fun interface IntPredicate {
fun accept(i: Int): Boolean
}
如果不使用 SAM 转换,那么你需要像这样编写代码:
// 创建一个类的实例
val isEven = object : IntPredicate {
override fun accept(i: Int): Boolean {
return i % 2 == 0
}
}
通过利用 Kotlin 的 SAM 转换,可以改为以下等效代码:
// 通过 lambda 表达式创建一个实例
val isEven = IntPredicate { it % 2 == 0 }
函数式接口与类型别名比较,函数式接口和类型别名用途并不相同。 类型别名只是现有类型的名称------它们不会创建新的类型,而函数式接口却会创建新类型。
typealias IntPredicate = (i: Int) -> Boolean
val isEven: IntPredicate = { it % 2 == 0 }
fun main() {
println("Is 7 even? - ${isEven(7)}")
}
6.6 特殊类关系
6.6.1 代理委托类
委托模式已经证明是实现继承的一个很好的替代方式, 而 Kotlin 可以零样板代码地原生支持它。
-
Derived****类可以通过将其所有公有成员都委托给指定对象来实现一个接口 Base**:**
interface Base {
fun print()
}class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}class Derived(b: Base) : Base by b
fun main() {
val b = BaseImpl(10)
Derived(b).print()
}
Derived 的超类型列表中的 by子句表示 b 将会在 Derived 中内部存储, 并且编译器将生成转发给 b 的所有 Base 的方法。
- 如果代理类中覆盖实现了某函数,那编译器会优先使用 override****覆盖的实现而不是委托对象中的
6.6.2 嵌套类和内部类
-
类可以嵌套在其他类中, 实例化方法 Outer.Nested()
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}val demo = Outer.Nested().foo() // == 2
还可以使用带有嵌套的接口。所有类与接口的组合都是可能的:可以将接口嵌套在类中、将类嵌套在接口中、将接口嵌套在接口中。
interface OuterInterface {
class InnerClass
interface InnerInterface
}class OuterClass {
class InnerClass
interface InnerInterface
} -
但是只有标记为 inner****的嵌套类能够访问其外部类的成员,实例化方法 Outer().Inner()。内部类会带有一个对外部类的对象的引用:
class Outer {
private val bar: Int = 1
inner class Inner {
fun foo() = bar
}
}val demo = Outer().Inner().foo() // == 1
在一个内部类中访问外部类的超类,可以使用由外部类名限定的 super 关键字来实现:super@Outer:
open class Rectangle {
open fun draw() { println("Drawing a rectangle") }
val borderColor: String get() = "black"
}
//sampleStart
class FilledRectangle: Rectangle() {
override fun draw() {
val filler = Filler()
filler.drawAndFill()
}
inner class Filler {
fun fill() { println("Filling") }
fun drawAndFill() {
super@FilledRectangle.draw() // 调用 Rectangle 的 draw() 实现
fill()
println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // 使用 Rectangle 所实现的 borderColor 的 get()
}
}
}
//sampleEnd
fun main() {
val fr = FilledRectangle()
fr.draw()
}
6.6.3 object对象表达式 和 object: 匿名内部类
-
对象表达式创建匿名类的对象,即没有通过类声明显式声明的类。这样的类对于一次性使用很有用
fun main() {
//sampleStart
val helloWorld = object {
val hello = "Hello"
val world = "World"
// object expressions extend Any, sooverrideis required ontoString()
override fun toString() = "hello world"
}print(helloWorld)//sampleEnd
} -
使用 对象表达式创建匿名内部类实例,对象表达式中的代码可以访问来自包含它的作用域的变量:
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { ...... } override fun mouseEntered(e: MouseEvent) { ...... }})
fun countClicks(window: JComponent) {
var clickCount = 0
var enterCount = 0window.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { clickCount++ } override fun mouseEntered(e: MouseEvent) { enterCount++ } }) // ......}
对于 JVM 平台, 如果对象是函数式 Java 接口(即具有单个抽象方法的 Java 接口)的实例,可以使用带接口类型前缀的 lambda 表达式创建它:
val listener = ActionListener { println("clicked") }
匿名对象实现多个接口
-
如果超类型有一个构造函数,那么传递适当的构造函数参数给它。 多个超类型可以由跟在冒号后面的逗号分隔的列表指定:
open class A(x: Int) {
public open val y: Int = x
}interface B { /....../ }
val ab: A = object : A(1), B {
override val y = 15
}
匿名对象用于局部类型时指向同一对象
-
当一个匿名对象被用作局部或私有类型,而不是内联声明(函数或属性)时,它的所有成员都可以通过这个函数或属性访问:
class C {
private fun getObject() = object {
val x: String = "x"
}fun printX() { println(getObject().x) }}
对象表达式和对象声明之间有一个重要的语义差别:
- 对象表达式是在使用他们的地方 立即 执行(及初始化)的。
- 对象声明是在第一次被访问到时 延迟 初始化的。
- 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配 。
6.6.4 object 对象声明和单例类实现
单例模式在一些场景中很有用, 而 Kotlin 使单例声明变得很容易:这称为对象声明。并且它
- 总是在 object****关键字后跟一个名称。 就像变量声明一样,对象声明不是一个表达式,不能用在赋值语句的右边。
-
- 对象声明的初始化过程是线程安全的并且在首次访问时进行
-
如需引用该对象,直接使用其名称即可:
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ......
}val allDataProviders: Collection<DataProvider> get() = // ......}
DataProviderManager.registerDataProvider(......)
这些对象可以有超类型:
object DefaultListener : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { ...... }
override fun mouseEntered(e: MouseEvent) { ...... }
}
6.6.5 伴生对象 companion
如果你需要写一个可以无需用一个类的实例来调用、但需要访问类内部的函数(例如,工厂方法),你可以把它写成该类内对象声明中的一员。
更具体地讲,如果在你的类内声明了一个 伴生对象, 你就可以访问其成员,只是以类名作为限定符
-
类内部的对象声明可以用 companion****关键字标记:
class MyClass {
companion object {
// 定义静态属性或方法
const val CONSTANT_VALUE: String = "Static Value"fun staticMethod() { println("This is a static method.") } }}
//该伴生对象的成员可通过只使用类名作为限定符来调用:
val instance = MyClass.CONSTANT_VALUE
给伴生对象命名
虽然伴生对象通常是匿名的,但也可以为其指定名称。
kotlin
class MyClass {
companion object NamedCompanion {
fun namedStaticMethod() {
println("This is a named static method.")
}
}
}
//如果为伴生对象命名,可以通过 MyClass.NamedCompanion.namedStaticMethod() 访问
类名称可默认用于伴生对象
-
其自身所用的类的名称(不是另一个名称的限定符)可用作对该类的伴生对象 (无论是否具名)的引用:
class MyClass1 {
companion object Named { }
}//注意没有MyClass1(). 所以x是MyClass1的伴生对象
val x = MyClass1class MyClass2 {
companion object { }
}val y = MyClass2
伴生对象不是真的静态函数,而是一个静态类的静态实例
请注意,即使伴生对象的成员看起来像其他语言的静态成员,在运行时他们仍然是真实对象的实例成员,而且,例如还可以实现接口:
interface Factory<T> {
fun create(): T
}
class MyClass {
companion object : Factory<MyClass> {
override fun create(): MyClass = MyClass()
}
}
val f: Factory<MyClass> = MyClass
6.7 特殊类
6.7.1 data class 数据类
data class User(val name: String, val age: Int)
编译器自动从主构造函数中声明的所有属性导出以下成员:
.equals()/.hashCode()对。.toString()格式是"User(name=John, age=42)"。- .componentN()函数 按声明顺序对应于所有属性。
.copy()函数(见下文)
为了确保生成的代码的一致性以及有意义的行为,数据类必须满足以下要求:
- 主构造函数必须至少有一个参数。
- 主构造函数的所有参数必须标记为
val或var。 - 数据类不能是抽象、开放、密封或者内部的。
此外,数据类成员的生成遵循关于成员继承的这些规则:
- 如果在数据类体中有显式实现 .equals()****、 .hashCode()或者 **.toString()**,或者这些函数在父类中有 final****实现,那么不会生成这些函数,而会使用现有函数。
- 如果超类型具有 open****的 **.componentN()**函数并且返回兼容的类型, 那么会为数据类生成相应的函数,并覆盖超类的实现。如果超类型的这些函数由于签名不兼容或者是 final 而导致无法覆盖,那么会报错。
- 不允许为 **.componentN()**以及 **.copy()**函数提供显式实现。
equals函数
数据类只会为定义在表头的属性设置equals等成员,其他的不会,如下述例子,equals是相等的,虽然age不同
data class Person(val name: String) {
var age: Int = 0
}
fun main() {
//sampleStart
val person1 = Person("John")
val person2 = Person("John")
person1.age = 10
person2.age = 20
println("person1 == person2: ${person1 == person2}")
// person1 == person2: true
println("person1 with age ${person1.age}: ${person1}")
// person1 with age 10: Person(name=John)
println("person2 with age ${person2.age}: ${person2}")
// person2 with age 20: Person(name=John)
//sampleEnd
}
copy函数
#自动生成copy函数
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
#然后可以这样调用
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
解构函数
为数据类生成的 component 函数 使它们可在解构声明中使用:
val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age")
6.7.2 enum class枚举类
枚举类的最基本的应用场景是实现类型安全的枚举:
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
每个枚举常量都是一个对象。枚举常量以逗号分隔。
因为每一个枚举都是枚举类的实例,所以可以这样初始化:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
使用枚举常量
Kotlin 中的枚举类有合成的属性与方法用于列出定义的枚举常量以及通过其名称获取枚举常量。这些方法的签名如下(假设枚举类的名称是 EnumClass):
EnumClass.valueOf(value: String): EnumClass
EnumClass.entries: EnumEntries<EnumClass> // specialized List<EnumClass>
Below is an example of them in action:
enum class RGB { RED, GREEN, BLUE }
fun main() {
for (color in RGB.entries) println(color.toString()) // prints RED, GREEN, BLUE
println("The first color is: ${RGB.valueOf("RED")}") // prints "The first color is: RED"
}
如果指定的名称与类中定义的任何枚举常量均不匹配, valueOf() 方法会抛出 IllegalArgumentException 异常。
每个枚举常量也都具有这两个属性:name 与 ordinal, 用于在枚举类声明中获取其名称与(自 0 起的)位置:
enum class RGB { RED, GREEN, BLUE }
fun main() {
//sampleStart
println(RGB.RED.name) // prints RED
println(RGB.RED.ordinal) // prints 0
//sampleEnd
}
6.7.3 value class 内联类
它灵感来源于 inline 这个名称:类的数据被 内联 到该类使用的地方(类似于 内联函数中的代码被内联到该函数调用的地方)。
To declare an inline class, use the value modifier before the name of the class:
value class Password(private val s: String)
内联类必须含有唯一的一个属性在主构造函数中初始化。在运行时, 将使用这个唯一属性来表示内联类的实例(关于运行时的内部表达请参阅 下文):
// 不存在 'Password' 类的真实实例对象
// 在运行时,'securePassword' 仅仅包含 'String'
val securePassword = Password("Don't try this in production")
内联类支持普通类中的一些功能。
特别是,内联类可以声明属性与函数, have an init block and secondary constructors:
@JvmInline
value class Person(private val fullName: String) {
init {
require(fullName.isNotEmpty()) {
"Full name shouldn't be empty"
}
}
constructor(firstName: String, lastName: String) : this("$firstName $lastName") {
require(lastName.isNotBlank()) {
"Last name shouldn't be empty"
}
}
val length: Int
get() = fullName.length
fun greet() {
println("Hello, $fullName")
}
}
fun main() {
val name1 = Person("Kotlin", "Mascot")
val name2 = Person("Kodee")
name1.greet() // greet` 函数会作为一个静态方法被调用
println(name2.length) // 属性的 getter 会作为一个静态方法被调用
}
内联类与类型别名
初看起来,内联类似乎与类型别名非常相似。实际上,两者似乎都引入了一种新的类型,并且都在运行时表示为基础类型。
然而,关键的区别在于类型别名与其基础类型(以及具有相同基础类型的其他类型别名)是赋值兼容的,而内联类却不是这样。
换句话说,内联类引入了一个真实的新类型,与类型别名正好相反,类型别名仅仅是为现有的类型取了个新的替代名称 (别名)
6.7.4 sealed class 密封类
在下面的程序中,基类 Expr 有两个派生类 Const (表示一个数字)和 Sum (表示两个表达式的和)。在这里,必须使用 else 分支来处理 when表达式中的默认条件。
现在,如果您从Expr类派生一个新的子类,则编译器将不会检测到任何东西,因为 else 分支会对其进行处理,这可能会导致错误。 如果在添加新的子类时编译器发出错误,那就更好了。
class Expr
class Const(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
fun eval(e: Expr): Int =
when (e) {
is Const -> e.value
is Sum -> eval(e.right) + eval(e.left)
else ->
throw IllegalArgumentException("Unknown expression")
}
要解决此问题,可以使用密封类
- **密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。**在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。
- 要声明一个密封类,需要在类名前面添加 sealed 修饰符。虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。
-
-
密封类的所有子类必须在声明密封类的同一文件中声明。
-
密封类本身是 抽象的,您不能从中实例化对象。
-
不能创建密封类的非私有构造函数;默认情况下,它们的构造函数是 private。
-
请注意,扩展密封类子类的类(间接继承者)可以放在任何位置,而无需在同一个文件中。
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
-
使用密封类的关键好处在于使用 when 表达式 的时候,如果能够验证语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句了。当然,这只有当你用 when 作为表达式(使用结果)而不是作为语句时才有用。
6.8 扩展
Kotlin 能够对一个类或接口扩展新功能而无需继承该类或者使用像 装饰者 这样的设计模式。 这通过叫做 扩展 的特殊声明完成。
例如,你可以为一个你不能修改的、来自第三方库中的类或接口编写一个新的函数。 这个新增的函数就像那个原始类本来就有的函数一样,可以用寻常方式调用。 这种机制称为扩展函数 。此外,也有扩展属性, 允许你为一个已经存在的类添加新的属性。
扩展并不会修改原始类文件,而是一种代理机制 loading
6.8.1 xxClass.xx 扩展函数
声明一个扩展函数需用一个 接收者类型 也就是被扩展的类型来作为他的前缀。
下面代码为 MutableList<Int> 添加一个swap 函数:
- 这个
this关键字在扩展函数内部对应到接收者对象(传过来的在点符号前的对象)
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // "this"对应该列表
this[index1] = this[index2]
this[index2] = tmp
}
可以对任意 MutableList<Int> 调用该函数了:
val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // "swap()"内部的"this"会保存"list"的值
这个函数对任何 MutableList<T> 起作用,可以泛化它:
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // "this"对应该列表
this[index1] = this[index2]
this[index2] = tmp
}
为了在接收者类型表达式中使用泛型,需要在函数名前声明泛型参数。
扩展函数是静态解析的
扩展不能真正的修改他们所扩展的类。通过定义一个扩展,并没有在一个类中插入新成员, 只不过是可以通过该类型的变量用点表达式去调用这个新函数。
-
**扩展函数是静态分派的。因此,根据接收器类型,在编译时已经知道调用哪个扩展函数。**例如:
fun main() {
//sampleStart
open class Shape
class Rectangle: Shape()fun Shape.getName() = "Shape" fun Rectangle.getName() = "Rectangle" fun printClassName(s: Shape) { println(s.getName()) } printClassName(Rectangle())//sampleEnd
}
这个例子会输出 Shape ,因为调用的扩展函数只取决于参数 s 的声明类型,该类型是 Shape 类。
成员函数优先于扩展函数
-
如果一个类定义有一个成员函数与一个扩展函数,而这两个函数又有相同的接收者类型、 相同的名字,并且都适用给定的参数,这种情况 总是取成员函数。 例如:
fun main() {
//sampleStart
class Example {
fun printFunctionType() { println("Class method") }
}fun Example.printFunctionType() { println("Extension function") } Example().printFunctionType()//sampleEnd
}
这段代码输出 Class method。
可空接收者的扩展函数
注意可以为可空的接收者类型定义扩展。这样的扩展可以在对象变量上调用, 即使其值为 null。如果接收者是空的,那么这个也是空的。因此,在定义具有可空接收者类型的扩展时,我们建议在函数体内执行this == null检查,以避免编译器错误。
可以在没有检测 null 的时候调用 Kotlin 中的 toString():检测已发生在扩展函数的内部:
fun Any?.toString(): String {
if (this == null) return "null"
// 空检测之后,"this"会自动转换为非空类型,所以下面的 toString()
// 解析为 Any 类的成员函数
return toString()
}
6.8.2 扩展属性
与扩展函数类似,Kotlin 支持扩展属性:
val <T> List<T>.lastIndex: Int
get() = size - 1
由于扩展没有实际的将成员插入类中,因此对扩展属性来说 幕后字段是无效的。这就是为什么 扩展属性不能有初始化器 。他们的行为只能由显式提供的 getter/setter 定义。
例如:
val House.number = 1 // 错误:扩展属性不能有初始化器
6.8.3 伴生对象的扩展
如果一个类定义有一个伴生对象 ,你也可以为伴生对象定义扩展函数与属性。就像伴生对象的常规成员一样, 可以只使用类名作为限定符来调用伴生对象的扩展成员:
class MyClass {
companion object { } // 将被称为 "Companion"
}
fun MyClass.Companion.printCompanion() { println("companion") }
fun main() {
MyClass.printCompanion()
}
6.8.4 扩展声明为成员
- 可以在一个类内部为另一个类声明扩展。在这样的扩展内部,有多个隐式接收者------ 其中的对象成员可以无需通过限定符访问。
-
-
扩展声明所在的类的实例称为分发接收者 ,扩展方法调用所在的接收者类型的实例称为扩展接收者。
class Host(val hostname: String) {
fun printHostname() { print(hostname) }
}class Connection(val host: Host, val port: Int) {
fun printPort() { print(port) }fun Host.printConnectionString() { printHostname() // 调用 Host.printHostname() print(":") printPort() // 调用 Connection.printPort() } fun connect() { /*......*/ host.printConnectionString() // 调用扩展函数 }}
fun main() {
Connection(Host("kotl.in"), 443).connect()
//Host("kotl.in").printConnectionString() // 错误,该扩展函数在 Connection 外不可用
}
-
对于分发接收者与扩展接收者的成员名字冲突的情况,扩展接收者优先 。要引用分发接收者的成员你可以使用 限定的this语法。
class Connection {
fun Host.getConnectionString() {
toString() // 调用 Host.toString()
this@Connection.toString() // 调用 Connection.toString()
}
}
6.8.5 扩展的作用域
|------|------------------|------------------|-----------------------------------------------|
| 作用域 | 定义位置 | 调用方式 | 特点 |
| 顶级扩展 | 文件顶层,不在任何类或对象内部。 | 直接通过接收者类型的实例调用。 | - 全局可用。 - 适合工具类功能(如字符串处理、集合操作等)。 - 避免不必要的类包装。 |
| 成员扩展 | 类或对象内部。 | 必须通过类或对象的实例调用。 | - 与类的状态密切相关。 - 仅限于类的范围内使用。 - 提供更细粒度的封装。 |
| 局部扩展 | 函数或其他局部作用域内。 | 仅限于定义它的局部作用域内使用。 | - 私密性高。 - 适用于临时逻辑。 - 不会污染全局命名空间。 |
顶级扩展(Top-Level Extensions)
- 定义位置:
-
- 定义在文件顶层,不在任何类或对象内部。
- 调用方式:
-
- 可以直接通过接收者类型的实例调用。
- 特点:
-
-
全局可用,适合工具类功能。
kotlin
// 文件:StringExtensions.kt
fun String.isPalindrome(): Boolean {
return this == this.reversed()
}// 文件:Main.kt
fun main() {
val word = "madam"
println(word.isPalindrome()) // 输出:true
}
-
大多数情况都在顶层定义扩展------直接在包里:
package org.example.declarations
fun List<String>.getLongestString() { /*......*/}
如需使用所定义包之外的一个扩展,只需在调用方导入它:
package org.example.usage
import org.example.declarations.getLongestString
fun main() {
val list = listOf("red", "green", "blue")
list.getLongestString()
}
成员扩展(Member Extensions)
- 定义位置:
-
- 定义在类或对象内部。
- 调用方式:
-
- 必须通过类或对象的实例调用。
- 特点:
-
-
与类的状态密切相关,仅限于类的范围内使用。
kotlin
class StringUtils {
fun String.isPalindrome(): Boolean {
return this == this.reversed()
}
}fun main() {
val utils = StringUtils()
println(utils.run { "madam".isPalindrome() }) // 输出:true
}
-
局部扩展(Local Extensions)
- 定义位置:
-
- 定义在函数或其他局部作用域内。
- 调用方式:
-
- 仅限于定义它的局部作用域内使用。
- 特点:
-
-
私密性高,适用于临时逻辑。
kotlin
fun main() {
fun String.isPalindrome(): Boolean {
return this == this.reversed()
}val word = "madam" println(word.isPalindrome()) // 输出:true}
-
6.9 泛型:in、out、where
Kotlin 中的类可以有类型参数,与 Java 类似:
class Box<T>(t: T) {
var value = t
}
创建这样类的实例只需提供类型参数即可:
val box: Box<Int> = Box<Int>(1)
但是如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径, 就可以省略类型参数:
val box = Box(1) // 1 具有类型 Int,所以编译器推算出它是 Box<Int>
6.9.1 Java范型通配符存在的意义--适应协变
java 类型系统中最棘手的部分之一是通配符类型(参见 Java Generics FAQ)
首先,Java 中的泛型是 不型变的 , 这意味着 List<String> 并不是 List<Object> 的子类型。
- 但是,
Collection<String>表示为Collection<? extends Object>的子类型。 简而言之,带 extends 限定( 上界 )的通配符类型使得类型是 协变的(covariant) 。 如果只能从集合中获取元素, 那么使用String的集合 并且从其中读取Object也没问题 - 如果只能向集合中 放入 元素 , 就可以用
Object集合并向其中放入String:in Java there isList<? super String>, which acceptsStrings or any of its supertypes.
PECS 代表生产者-Extends、消费者-Super(Producer-Extends, Consumer-Super)。
extends关键字 可读
List<? extends T>:这表示一个包含T或T的子类的列表。使用extends关键字限定了类型的上界。这种声明方式使得列表只能读取元素,而不能添加新的元素。原因是编译器无法确定要添加的元素类型是否与列表中的类型兼容。例如:
List<? extends Number> numbers = new ArrayList<>();
Number number = numbers.get(0); // 可以读取元素
numbers.add(10); // 编译错误,无法确定要添加的元素类型
- 无法向
List<? extends T>添加新元素,因为编译器无法确定列表的具体子类型,为了维持类型安全性,禁止添加操作。 - 这样做是为了避免类型不一致的问题,即将错误类型的元素放入列表中,导致在读取元素时发生类型错误
1.1 应用场景
假设我们有一个通用的数据存储类List,我们希望这个类能够存储任何类型的对象,但是又想对存储的对象进行类型约束,以确保存储的对象必须是某个特定类的实例。这时,我们可以使用extends关键字来限制泛型类型参数。
1.2 实用技巧
使用extends关键字可以实现多态,即一个泛型类可以有多个子类,每个子类都可以指定不同的类型参数。
当使用extends关键字时,要注意子类和父类的关系,确保泛型类型参数的继承范围正确。
1.3 案例分析
public class GenericList<T extends Number> {
private T[] data;
public GenericList(int size) {
data = (T[]) new Number[size];
}
public void add(T item) {
data[size++] = item;
}
public T get(int index) {
return data[index];
}
}
在这个案例中,GenericList是一个泛型类,它的类型参数T被限制为Number类的子类。这意味着GenericList可以存储Integer、Double、Float等Number类的子类对象,但不能存储非Number类的对象。
super关键字 可存
List<? super T>:这表示一个包含T或T的超类的列表。使用super关键字限定了类型的下界。这种声明方式使得列表可以添加T类型的元素,但读取元素时只能作为Object类型处理。例如:
List<? super Integer> integers = new ArrayList<>();
integers.add(10); // 可以添加Integer及其子类的元素
Object obj = integers.get(0); // 只能读取为Object类型
Integer integer = integers.get(0); // 编译错误,无法确定列表中的元素类型
2.1 应用场景
假设我们有一个通用的数据存储类List,我们希望这个类能够存储任何类型的对象,但是又想对存储的对象进行类型约束,以确保存储的对象必须是某个特定类的实例。这时,我们可以使用super关键字来限制泛型类型参数。
2.2 实用技巧
使用super关键字可以实现多态,即一个泛型类可以有多个父类,每个父类都可以指定不同的类型参数。
当使用super关键字时,要注意父类和子类的关系,确保泛型类型参数的上界正确。
2.3 案例分析
public class GenericList<T super Number> {
private T[] data;
public GenericList(int size) {
data = (T[]) new Number[size];
}
public void add(T item) {
data[size++] = item;
}
public T get(int index) {
return data[index];
}
}
在这个案例中,GenericList是一个泛型类,它的类型参数T被限制为Number类的父类。
6.9.2 out修饰符
PECS 代表生产者-Extends、消费者-Super(Producer-Extends, Consumer-Super)。
假设有一个泛型接口 Source<T>,该接口中不存在任何以 T 作为参数的方法,只是方法返回 T 类型值:
// Java
interface Source<T> {
T nextT();
}
那么,在 Source <Object> 类型的变量中存储 Source <String> 实例的引用是极为安全的------ 没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!!在 Java 中不允许
// ......
}
为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>。这么做毫无意义, 因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。 但编译器并不知道。
使用 out 修饰符:
interface Source<out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
// ......
}
一般原则是:当一个类 C****的类型参数 T****被声明为 out****时,它就只能出现在 C****的成员的 输出 -位置, 但回报是 **C<Base>**可以安全地作为 **C<Derived>**的超类。
6.9.3 in修饰符
另外除了 out,Kotlin 又补充了一个型变注解:in。它使得一个类型参数 逆变 ,即只可以消费而不可以生产。逆变类型的一个很好的例子是 Comparable**:**
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
// 因此,可以将 x 赋给类型为 Comparable <Double> 的变量
val y: Comparable<Double> = x // OK!
}
6.9.4 泛型函数
不仅类可以有类型参数。函数也可以有。类型参数要放在函数名称之前:
fun <T> singletonList(item: T): List<T> {
// ......
}
fun <T> T.basicToString(): String { // 扩展函数
// ......
}
要调用泛型函数,在调用处函数名之后指定类型参数即可:
val l = singletonList<Int>(1)
可以省略能够从上下文中推断出来的类型参数,所以以下示例同样适用:
val l = singletonList(1)
6.9.5 泛型约束:上界
能够替换给定类型参数的所有可能类型的集合可以由泛型约束 限制。最常见的约束类型是上界 ,与 Java 的 extends 关键字对应:
fun <T : Comparable<T>> sort(list: List<T>) { ...... }
冒号之后指定的类型是上界 ,表明只有 Comparable<T> 的子类型可以替代 T。 例如:
sort(listOf(1, 2, 3)) // OK。Int 是 Comparable<Int> 的子类型
sort(listOf(HashMap<Int, String>())) // 错误:HashMap<Int, String> 不是 Comparable<HashMap<Int, String>> 的子类型
默认的上界(如果没有声明)是 Any?。在尖括号中只能指定一个上界。 如果同一类型参数需要多个上界,需要一个单独的 where-子句:
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}
所传递的类型必须同时满足 where 子句的所有条件。在上述示例中,类型 T 必须 既 实现了 CharSequence 也 实现了 Comparable。
七、协程
八、注解
注解是将元数据附加到代码的方法。要声明注解,请将 annotation 修饰符放在类的前面:
annotation class Fancy
注解的附加属性可以通过用元注解标注注解类来指定:
-
@Target 指定可以用该注解标注的元素的可能的类型(类、函数、属性与表达式);
-
@Retention 指定该注解是否存储在编译后的 class 文件中,以及它在运行时能否通过反射可见 (默认都是 true);
-
@Repeatable 允许在单个元素上多次使用相同的该注解;
-
@MustBeDocumented 指定该注解是公有 API 的一部分,并且应该包含在生成的 API 文档中显示的类或方法的签名中。
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class Fancy
8.1 用法
@Fancy class Foo {
@Fancy fun baz(@Fancy foo: Int): Int {
return (@Fancy 1)
}
}
如果需要对类的主构造函数进行标注,则需要在构造函数声明中添加 constructor 关键字 ,并将注解添加到其前面:
class Foo @Inject constructor(dependency: MyDependency) { ...... }
你也可以标注属性访问器:
class Foo {
var x: MyDependency? = null
@Inject set
}
8.2 构造函数
注解可以有接受参数的构造函数。
annotation class Special(val why: String)
@Special("example") class Foo {}
允许的参数类型有:
- 对应于 Java 原生类型的类型(Int、 Long等)
- 字符串
- 类(
Foo::class) - 枚举
- 其他注解
- 上面已列类型的数组
注解参数不能有可空类型,因为 JVM 不支持将 null 作为注解属性的值存储。
如果注解用作另一个注解的参数,则其名称不以 @ 字符为前缀:
annotation class ReplaceWith(val expression: String)
annotation class Deprecated(
val message: String,
val replaceWith: ReplaceWith = ReplaceWith(""))
@Deprecated("This function is deprecated, use === instead", ReplaceWith("this === other")
8.3 API 要求选择加入
8.3.1 创建选择加入要求的注解
如果想获得使用者使用你的模块 API 的明确同意,请创建一个注解类,作为要求选择加入的注解 。 这个类必须使用 @RequiresOptIn 注解:
@RequiresOptIn
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class MyDateTime
要求选择加入的注解必须满足以下几个要求:
选择加入的要求可以具有以下两个严格级别之一:
RequiresOptIn.Level.ERROR。选择加入是强制性的。 否则,使用标记 API 的代码将无法编译。 默认级别。RequiresOptIn.Level.WARNING。选择加入不是强制性的,而是建议使用的。 没有它,编译器会发出警告。
要设置所需的级别,请指定 @RequiresOptIn 注解的 level 参数。
另外,你可以提供一个 message 来通知用户有关使用该 API 的特定条件。 编译器会将其显示给使用该 API 但未选择加入的用户。
@RequiresOptIn(level = RequiresOptIn.Level.WARNING, message = "This API is experimental. It can be incompatibly changed in the future.")
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class ExperimentalDateTime
如果你发布了多个需要选择加入的独立功能,请为每个功能声明一个注解。 这使你的用户可以更安全地使用 API:他们只能使用其明确接受的功能。 这也使你可以独立地从功能中删除选择加入的要求。
8.3.2 标记 API 元素
要在使用 API 时要求选择加入,请给它的声明添加要求选择加入的注解。
@MyDateTime
class DateProvider
@MyDateTime
fun getTime(): Time {}
Note that for some language elements, an opt-in requirement annotation is not applicable:
- You cannot annotate a backing field or a getter of a property, just the property itself.
- You cannot annotate a local variable or a value parameter.
8.4 稳定前 API 的选择加入要求
如果要求选择加入尚未稳定的特性,请仔细处理 API 由实验状态到稳定状态的转换, 以避免破坏客户端代码。
当稳定前 API 稳定之后并以稳定状态发布后,请从声明中删除其要求选择加入的注解。 客户端将可以不受限制地使用它们。但是,你应该将注解类留在模块中,以便与现有的客户端代码保持兼容。
为了让 API 用户相应地更新其模块(从代码中删除注解并重新编译), 请将注解标记为 @Deprecated 并在弃用 message 中提供说明。
@Deprecated("This opt-in requirement is not used anymore. Remove its usages from your code.")
@RequiresOptIn
annotation class ExperimentalDateTime
九、反射
反射用途
- 在运行时判断任意一个**
对象所属的类** - 在运行时构造任意一个**
类的对象** - 在运行时判断任意一个**
类所具有的成员变量和方法** - 在运行时调用任意一个**
对象的方法**
9.1 反射的引入
要在Gradle或Maven项目中使用反射,请添加kotlin-reflect依赖项:
In Gradle:
【Kotlin】
dependencies {
implementation(kotlin("reflect"))
}
【Groovy】
dependencies {
implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.24"
}
9.2 Kotlin反射基础
9.2.1 类引用
最基本的反射功能是获取 Kotlin 类的运行时引用。要获取对静态已知的 Kotlin 类的引用,可以使用 类字面值 语法:
val c = MyClass::class
该引用是 KClass 类型的值。
对于 JVM 平台:Kotlin 类引用与 Java 类引用不同。要获得 Java 类引用, 请在 KClass 实例上使用 .java 属性。

对象的类引用
通过使用对象作为接收者,可以用相同的 ::class 语法获取指定对象的类的引用:
val widget: Widget = ......
assert(widget is GoodWidget) { "Bad widget: ${widget::class.qualifiedName}" }
你会获得对象的精确类的引用,例如 GoodWidget 或 BadWidget, 尽管接收者表达式的类型是 Widget。
9.2.2 可调用引用 KCallable<out R>
函数、属性以及构造函数的引用可以用于调用或者用作函数类型的实例。
所有可调用引用的公共超类型是 KCallable<out R>, 其中 R 是返回值类型。对于属性是属性类型,对于构造函数是所构造类型。
属性引用KProperty0<Int>和 KMutableProperty0<Int>
要把属性作为 Kotlin中 的一等对象来访问,可以使用 :: 操作符:
val x = 1
fun main() {
println(::x.get())
println(::x.name)
}
表达式 ::x 求值为 KProperty0<Int> 类型的属性对象,可以使用 get() 读取它的值,或者使用 name 属性来获取属性名。更多信息请参见关于KProperty类的文档。
对于可变属性,例如 var y = 1,::y 返回 KMutableProperty0<Int> 类型的一个值, 该类型有一个 set() 方法。
var y = 1
fun main() {
::y.set(2)
println(y)
}
属性引用可以用在预期具有单个泛型参数的函数的地方:
fun main() {
//sampleStart
val strs = listOf("a", "bc", "def")
println(strs.map(String::length))
//sampleEnd
}
要访问属于类的成员的属性,这样限定它:
fun main() {
//sampleStart
class A(val p: Int)
val prop = A::p
println(prop.get(A(1)))
//sampleEnd
}
对于扩展属性:
val String.lastChar: Char
get() = this[length - 1]
fun main() {
println(String::lastChar.get("abc"))
}
函数引用 KFunction<out R>
#当有一个具名函数声明如下
fun isOdd(x: Int) = x % 2 != 0
#您可以将该函数作为函数类型值使用,即将其传递给另一个函数。为此,使用::操作符:
fun main() {
//sampleStart
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd))
//sampleEnd
}
#你可以通过将方法引用存储在具有显式指定类型的变量中来提供必要的上下文:
val predicate: (String) -> Boolean = ::isOdd // 引用到 isOdd(x: String)
这里 ::isOdd 是函数类型 (Int) -> Boolean 的一个值。
函数引用属于 KFunction<out R> 的子类型之一,取决于参数个数。例如 KFunction3<T1, T2, T3, R>。
- 如果需要使用类的成员函数或扩展函数,它需要是限定的,例如
String::toCharArray。
-
-
即使以扩展函数的引用初始化一个变量,其推断出的函数类型也会没有接收者,但是它会有一个接受接收者对象的额外参数。如需改为带有接收者的函数类型,请明确指定其类型:
val isEmptyStringList: List<String>.() -> Boolean = List<String>::isEmpty
-
示例:函数组合 它返回一个传给它的两个函数的组合:compose(f, g) = f(g(*))。 你可以将该函数应用于可调用引用:
fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
return { x -> f(g(x)) }
}
fun isOdd(x: Int) = x % 2 != 0
fun main() {
//sampleStart
fun length(s: String) = s.length
val oddLength = compose(::isOdd, ::length)
val strings = listOf("a", "ab", "abc")
println(strings.filter(oddLength))
//sampleEnd
}
构造函数引用KFunction<out R>
构造函数可以像方法和属性那样引用。可以将其用于程序期待这样函数类型对象的任何地方:它与该构造函数接受相同参数并且返回相应类型的对象。 通过使用 :: 操作符并添加类名来引用构造函数。考虑下面的函数, 它期待一个无参并返回 Foo 类型的函数参数:
class Foo
fun function(factory: () -> Foo) {
val x: Foo = factory()
}
使用 ::Foo,类 Foo 的零参数构造函数,可以这样调用它:
function(::Foo)
构造函数的可调用引用的类型也是 KFunction<out R> 的子类型之一, 取决于其参数个数。
绑定对象的函数与属性引用
你可以引用特定对象的实例方法:
fun main() {
//sampleStart
val numberRegex = "\\d+".toRegex()
println(numberRegex.matches("29"))
val isNumber = numberRegex::matches
println(isNumber("29"))
//sampleEnd
}
取代直接调用方法 matches 的是使用其引用。 这样的引用会绑定到其接收者上。 它可以直接调用(如上例所示)或者用于任何期待一个函数类型表达式的时候:
fun main() {
//sampleStart
val numberRegex = "\\d+".toRegex()
val strings = listOf("abc", "124", "a70")
println(strings.filter(numberRegex::matches))
//sampleEnd
}
9.3 Kotlin常见反射函数

9.3.1 Kotlin反射常用的数据结构
|-----------|-----------------------------------------------------------------------|
| 数据结构 | 概念及使用说明 |
| KType | 描述未擦除的类型或泛型参数等,例如 Map<String,Int>;可通过 typeof 或者以下类型获取对应的父类、属性、函数参数等 |
| KClass | 描述对象的实际类型,不包含泛型参数,例如Map可通过对象、类型名直接获得 |
| KProperty | 描述属性,可通过属性引用、属性所在类的 KClass 获取 |
| KFunction | 描述函数,可通过函数引用、函数所在类的 KClass 获取 |
9.3.2 获取KClass实例
在Kotlin中可以通过以下两种方式获取KClass实例。
//1.通过类::class的方式获取Kclass实例
val clazz1: KClass<*> = HelloWorld::class
//2.通过实例.javaClass.kotlin获取Kclass实例
var hello = HelloWorld()
val clazz2 = hello.javaClass.kotlin
9.3.3 常用API
构造函数Constructor
//返回这个类的所有构造器
public val constructors: Collection<KFunction<T>>
成员变量和成员函数
//返回类可访问的所有函数和属性,包括继承自基类的,但是不包括构造器
override val members: Collection<KCallable<*>>
//返回类声明的所有函数
val KClass<*>.declaredFunctions: Collection<KFunction<*>>
//返回类的扩展函数
val KClass<*>.declaredMemberExtensionFunctions: Collection<KFunction<*>>
//返回类的扩展属性
val <T : Any> KClass<T>.declaredMemberExtensionProperties: Collection<KProperty2<T, *, *>>
//返回类自身声明的成员函数
val KClass<*>.declaredMemberFunctions: Collection<KFunction<*>>
//返回类自身声明的成员变量(属性)
val <T : Any> KClass<T>.declaredMemberProperties: Collection<KProperty1<T, *>>
类相关信息
//1.返回类的名字
public val simpleName: String?
//2.返回类的全包名
public val qualifiedName: String?
//3.如果这个类声明为object,则返回其实例,否则返回null
public val objectInstance: T?
//4.返回类的可见性
@SinceKotlin("1.1")
public val visibility: KVisibility?
//5.判断类是否为final类(在Kotlin中,类默认是final的,除非这个类声明为open或者abstract)
@SinceKotlin("1.1")
public val isFinal: Boolean
//6.判断类是否是open的(abstract类也是open的),表示这个类可以被继承
@SinceKotlin("1.1")
public val isOpen: Boolean
//7.判断类是否为抽象类
@SinceKotlin("1.1")
public val isAbstract: Boolean
//8.判断类是否为密封类,密封类:用sealed修饰,其子类只能在其内部定义
@SinceKotlin("1.1")
public val isSealed: Boolean
//9.判断类是否为data类
@SinceKotlin("1.1")
public val isData: Boolean
//10.判断类是否为成员类
@SinceKotlin("1.1")
public val isInner: Boolean
//11.判断类是否为companion object
@SinceKotlin("1.1")
public val isCompanion: Boolean
//12.返回类中定义的其他类,包括内部类(inner class声明的)和嵌套类(class声明的)
public val nestedClasses: Collection<KClass<*>>
//13.判断一个对象是否为此类的实例
@SinceKotlin("1.1")
public fun isInstance(value: Any?): Boolean
//14.返回这个类的泛型列表
@SinceKotlin("1.1")
public val typeParameters: List<KTypeParameter>
//15.类其直接基类的列表
@SinceKotlin("1.1")
public val supertypes: List<KType>
//16.返回类所有的基类
val KClass<*>.allSuperclasses: Collection<KClass<*>>
//17.返回类的伴生对象companionObject
val KClass<*>.companionObject: KClass<*>?
9.3.4 调用示例
package com.yvan.demo.reflect
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.*
import kotlin.reflect.jvm.isAccessible
//定义注解
annotation class Anno
@Deprecated("该类已经不推荐使用")
@Anno
class ReflectA(val name: String) {
companion object{
const val TAG = "ReflectA"
fun show(){
}
}
var age: Int = 0
constructor() : this("ReflectA_")
constructor(name: String, age: Int) : this(name) {
this.age = age
}
fun print(str: String) {
println("ReflectA print str $str")
}
fun sayHi(): String {
println("ReflectA sayHi")
return "sayHi"
}
class InnerClass
}
// 拓展方法
fun ReflectA.exfun() {
println("exfun")
}
// 拓展属性
val ReflectA.foo: Double
get() = 3.14
fun main() {
println("Hello word")
val clazz = ReflectA::class
println(clazz)
println("ReflectA 的全部构造器如下:")
clazz.constructors.forEach {
println(it)
}
println("ReflectA 的主构造器如下:")
println(clazz.primaryConstructor)
println(" ")
//通过functions属性获取该KClass对象所对应类的全部方法
val funs = clazz.functions
println("ReflectA 的全部方法如下:")
funs.forEach { println(it) }
println(" ")
//通过 declaredFunctions 属性获取该KClass对象声明的全部方法
val funs2 = clazz.declaredFunctions
println("ReflectA 本身声明的全部方法如下:")
funs2.forEach { println(it) }
println(" ")
//通过 memberExtensionFunctions 属性获取全部扩展方法
val exetensionFunctions = clazz.memberExtensionFunctions
println("ReflectA 声明的扩展方法如下:")
exetensionFunctions.forEach { println(it) }
println(" ")
//通过decaredMemberProperties获取全部成员属性
var memberProperties = clazz.declaredMemberProperties
println("ReflectA 本身声明的成员属性如下:")
memberProperties.forEach { println(it) }
println(" ")
//通过memberExtensionProperties属性获取该KClass对象的全部扩展属性
var exProperties = clazz.memberExtensionProperties
println("ReflectA 本身声明的扩展属性如下:")
exProperties.forEach { println(it) }
println(" ")
//通过annotations属性获取该KClass对象所对应类的全部注解
val anns = clazz.annotations
println("ReflectA 的全部注解如下:")
anns.forEach { println(it) }
println("该KClass元素上的@Annot注解为:${clazz.findAnnotation<Anno>()}")
println(" ")
//通过nestedClasses属性获取所对应的全部嵌套类
val inners = clazz.nestedClasses
println("ReflectA 的全部内部类如下:")
inners.forEach { println(it) }
println(" ")
//通过supertypes属性获取该类的所有父类型
println("KClassTest的父类型为:${clazz.supertypes}")
println(" ")
println("---------- companion 对象 ---------") //
val companion = clazz.companionObject // 返回也是一个 KClass
if (companion != null){
println("companion $companion")
companion.declaredMemberProperties.forEach {
println("companion declaredMemberProperties: $it")
}
companion.declaredFunctions.forEach {
println("companion declaredFunctions: $it")
}
}
println(" ")
println("---------- 创建对象 ---------")
println(" ")
println("createInstance 创建实例")
// createInstance() 方法调用无参数的构造器创建实例
val inst2 = clazz.createInstance()
println(inst2.name)
println(inst2.age)
println(" ")
// primaryConstructor 主构造函数
val cons1 = clazz.primaryConstructor
val inst1 = cons1?.call("hello reflect") // 参入参数
println(inst1)
println("inst1 " + inst1?.name)
println(" ")
println("第一个构造函数")
val cons2 = clazz.constructors.first()
println(cons2)
println(" ")
println("-------调用方法------")
val funs3 = clazz.declaredFunctions
val inst3 = clazz.createInstance()
println("ReflectA 本身声明的全部方法如下:")
funs3.forEach { println(it) }
for (f in funs3) {
if (f.name == "sayHi") {
f.call(inst3)
}
if (f.name == "print") {
f.call(inst3, "反射打印")
}
}
println("\n")
println("-------访问属性------")
//通过decaredMemberProperties获取全部成员属性
val memberProperties2 = clazz.declaredMemberProperties
val inst4 = clazz.createInstance()
println("ReflectA 本身声明的成员属性如下:")
memberProperties2.forEach { println(it) }
println("inst4 name: ${inst4.name}")
memberProperties2.forEach {
if (it.name == "age") {
it as KMutableProperty1<ReflectA, Int>
it.isAccessible = true
it.set(inst4, 20)
println(it.get(inst4))
}
}
}
9.4 Kotlin和Java的互操作性


基于Kotlin使用Java反射
在Kotlin中,字节码对应的类是kotlin.reflect.KClass,因为Kotlin百分之百兼容Java,所以Kotlin中可以使用Java中的反射,但是由于Kotlin中字节码.class对应的是KClass类,所以如果想要使用Java中的反射,需要首先获取Class的实例,在Kotlin中可以通过以下两种方式来获取Class实例。
//1.通过实例.javaClass
var hello = HelloWorld()
hello.javaClass
//2.通过类Kclass类的.java属性
HelloWorld::class.java
获取了Class实例,就可以调用Java各种方法,获取各种在Java中定义的类的信息了
val viewId : String by lazy {
val c = R.id()
val fields = c.javaClass.declaredFields
val r = fields.find { it.getInt(c) == view.id }?.name?:"Not found"
r
}
需要注意的是直接调用R.id::class获取的是KClass对象,它代表Kotlin中反射的入口,要获取Java的Class对象,需要其.java属性
{
val c = R.id::class
val fields = c.java.fields
val r = fields.find { it.getInt(c) == view.id }?.name?:"Not found"
r
}
在 JVM 平台上,标准库包含反射类的扩展,它提供了与 Java 反射对象之间映射(参见 kotlin.reflect.jvm 包)。 例如,要查找一个用作 Kotlin 属性 getter 的 幕后字段或 Java方法,可以这样写:
import kotlin.reflect.jvm.*
class A(val p: Int)
fun main() {
println(A::p.javaGetter) // 输出 "public final int A.getP()"
println(A::p.javaField) // 输出 "private final int A.p"
}
基于Java类使用Kotlin反射
要获得对应于 Java 类的 Kotlin 类,请使用 .kotlin 扩展属性:
fun getKClass(o: Any): KClass<Any> = o.javaClass.kotlin
十、Java和Kotlin的相互调用
10.1 与Java比较
Kotlin 解决了一些 Java 中的问题
Kotlin 通过以下措施修复了 Java 中一系列长期困扰我们的问题:
- 空引用由类型系统控制。
- 无原始类型
- Kotlin 中数组是不型变的
- 相对于 Java 的 SAM-转换,Kotlin 有更合适的函数类型
- 没有通配符的使用处型变
- Kotlin 没有受检异常。Kotlin 允许直接调用可能抛出受检异常的 Java 方法,而无需显式处理。但如果运行时发生异常,程序会崩溃,如果需要处理异常,可以使用
try-catch块。 - Separate interfaces for read-only and mutable collections
Java 有而 Kotlin 没有的东西
- 受检异常
- 不是类的原生类型。字节码会尽可能试用原生类型,但不是显式可用的。
- 静态成员以伴生对象、 顶层函数、 扩展函数或者 @JvmStatic 取代。
- 通配符类型以声明处型变与类型投影取代。
- 三目操作符a ? b : c ------ 以 if 表达式取代。
- Records
- Pattern Matching
- package-private visibility modifier
Kotlin 有而 Java 没有的东西
- Lambda 表达式 + 内联函数 = 高性能自定义控制结构
- 扩展函数
- 空安全
- 智能类型转换 (Java 16 : Pattern Matching for instanceof)
- 字符串模板 (Java 21 : String Templates (Preview))
- 属性
- 主构造函数
- 一等公民的委托
- 变量与属性类型的类型推断 (Java 10 : Local-Variable Type Inference)
- 单例
- 声明处型变 & 类型投影
- 区间表达式
- 操作符重载
- 伴生对象
- 数据类
- 协程
- Top-level functions
- Default arguments
- Named parameters
- Infix functions
- Explicit API mode and better control of API surface
尾部逗号
尾部逗号是一系列元素中最后一项之后的逗号符号:
class Person(
val firstName: String,
val lastName: String,
val age: Int, // 尾部逗号
)
使用尾部逗号有以下几点收益:
- 使版本控制差异更清晰------因为所有焦点都集中在变更的值上。
- 使添加及重新排序元素更容易------操作元素时无需再添加或删除逗号。
- 简化了代码生成逻辑(例如对象初始化器)。 最后一个元素也可以有逗号。
expect / actual
https://book.kotlincn.net/text/multiplatform-expect-actual.html
在 Kotlin 中,expect 和 actual 是用于实现多平台开发(Multiplatform Programming)的关键字。它们允许您定义跨平台的公共接口或功能,同时为不同的平台提供具体的实现。这种机制使得代码可以在多个平台上共享逻辑,同时针对每个平台进行优化。
(1) expect
expect关键字用于声明一个跨平台的公共接口、类、函数或属性。- 它告诉编译器:在其他平台模块中需要提供该声明的具体实现(即
actual实现)。 expect本身不包含任何具体实现。
(2) actual
actual关键字用于提供expect声明的具体实现。- 每个目标平台(如 JVM、JS、Native)都需要提供对应的
actual实现。 actual实现必须与expect声明的签名完全匹配。
以下是一个简单的示例,展示如何使用 expect 和 actual 实现跨平台的日志记录功能。
(1) 公共接口(expect)
kotlin
// common module
expect fun logMessage(message: String)
logMessage是一个跨平台的公共接口,用于记录日志。- 不同平台需要提供具体的实现。
(2) JVM 平台实现(actual)
kotlin
// JVM module
actual fun logMessage(message: String) {
println("[JVM Log] $message")
}
- 在 JVM 平台上,使用
println输出日志。
(3) JS 平台实现(actual)
kotlin
// JS module
actual fun logMessage(message: String) {
console.log("[JS Log] $message")
}
- 在 JS 平台上,使用
console.log输出日志。
(4) 调用共享代码
kotlin
// common module
fun performTask() {
logMessage("Task started")
// 执行任务
logMessage("Task completed")
}
fun main() {
performTask()
}
performTask是一个共享函数,调用了logMessage。- 根据运行时所在的平台,会调用相应的
actual实现。
类型比较
Kotlin 特殊处理一部分 Java 类型。这样的类型不是"按原样"从 Java 加载,而是 映射 到相应的 Kotlin 类型。 映射只发生在编译期间,运行时表示保持不变。 Java 的原生类型映射到相应的 Kotlin 类型(请记住平台类型):
|-------------|------------------|
| Java 类型 | Kotlin 类型 |
| byte | kotlin.Byte |
| short | kotlin.Short |
| int | kotlin.Int |
| long | kotlin.Long |
| char | kotlin.Char |
| float | kotlin.Float |
| double | kotlin.Double |
| boolean | kotlin.Boolean |
一些非原生的内置类型也会作映射:
|-----------------------------------|------------------------|
| Java 类型 | Kotlin 类型 |
| java.lang.Object | kotlin.Any! |
| java.lang.Cloneable | kotlin.Cloneable! |
| java.lang.Comparable | kotlin.Comparable! |
| java.lang.Enum | kotlin.Enum! |
| java.lang.annotation.Annotation | kotlin.Annotation! |
| java.lang.CharSequence | kotlin.CharSequence! |
| java.lang.String | kotlin.String! |
| java.lang.Number | kotlin.Number! |
| java.lang.Throwable | kotlin.Throwable! |
Java 的装箱原始类型映射到可空的 Kotlin 类型:
|-----------------------|-------------------|
| Java type | Kotlin type |
| java.lang.Byte | kotlin.Byte? |
| java.lang.Short | kotlin.Short? |
| java.lang.Integer | kotlin.Int? |
| java.lang.Long | kotlin.Long? |
| java.lang.Character | kotlin.Char? |
| java.lang.Float | kotlin.Float? |
| java.lang.Double | kotlin.Double? |
| java.lang.Boolean | kotlin.Boolean? |
可见性比较
Kotlin 的可见性以下列方式映射到 Java:
private成员编译成private成员private的顶层声明编译成private顶层声明。 Package-private accessors are also included, if accessed from within a class.protected保持protected(注意 Java 允许访问同一个包中其他类的受保护成员, 而 Kotlin 不能,所以 Java 类会访问更广泛的代码)internal声明会成为 Java 中的public。internal类的成员会通过名字修饰,使其更难以在 Java 中意外使用到,并且根据 Kotlin 规则使其允许重载相同签名的成员而互不可见public保持public
10.2 在 Kotlin 中调用 Java
Kotlin 在设计时就考虑了 Java 互操作性。可以从 Kotlin 中自然地调用现存的 Java 代码, 并且在 Java 代码中也可以很顺利地调用 Kotlin 代码。几乎所有 Java 代码都可以使用而没有任何问题。
读取Java返回Void函数
- 如果一个 Java 方法返回
void,那么从 Kotlin 调用时中返回Unit。 万一有人使用其返回值,它将由 Kotlin 编译器在调用处赋值, 因为该值本身是预先知道的(是Unit)
读取Java受检异常函数
在 Kotlin 中,所有异常都是非受检的,这意味着编译器不会强迫你捕获其中的任何一个。 因此,当你调用一个声明受检异常的 Java 方法时,Kotlin 不会强迫你做任何事情:
fun render(list: List<*>, to: Appendable) {
for (item in list) {
to.append(item.toString()) // Java 会要求我们在这里捕获 IOException
}
}
读取Java的Getter和Setter方法
-
遵循 Java 约定的 getter 与 setter 的方法(名称以
get开头的无参数方法及以set开头的单参数方法)在 Kotlin 中表示为属性import java.util.Calendar
fun calendarDemo() {
val calendar = Calendar.getInstance()
if (calendar.firstDayOfWeek == Calendar.SUNDAY) { // 调用 getFirstDayOfWeek()
calendar.firstDayOfWeek = Calendar.MONDAY // 调用ll setFirstDayOfWeek()
}
if (!calendar.isLenient) { // 调用 isLenient()
calendar.isLenient = true // 调用 setLenient()
}
}
读取Java对象的Object方法
当 Java 类型导入到 Kotlin 中时,类型 java.lang.Object 的所有引用都成了 Any。 而因为 Any 不是平台指定的,它只声明了 toString()、hashCode() 和 equals() 作为其成员, 所以为了能用到 java.lang.Object 的其他成员,Kotlin 要用到扩展函数。
类型 Any 的引用没有提供 wait() 与 notify() 方法。通常不鼓励使用它们,而建议使用 java.util.concurrent。 如果确实需要调用这两个方法的话,那么可以将引用转换为 java.lang.Object:
(foo as java.lang.Object).wait()
读取Java的native方法
要声明一个在本地(C 或 C++)代码中实现的函数,你需要使用 external 修饰符来标记它:
external fun foo(x: Int): Double
[平台类型] 读取Java引用引发的空异常
- Java 中的任何引用都可能是 null**,这使得 Kotlin 对来自 Java 的对象要求严格空安全是不现实的。 Java 声明的类型在 Kotlin 中会以特殊方式对待并称为** 平台类型 。对这种类型的空检测会放宽, 因此它们的安全保证与在 Java 中相同( 也就是需要研发自己保障 )
-
-
当调用平台类型变量的方法时,Kotlin 不会在编译时报告可空性错误,
-
但在运行时调用可能会失败异常,因为空指针异常或者 Kotlin 生成的阻止空值传播的断言:
val list = ArrayList<String>() // 非空(构造函数结果)
list.add("Item")
val size = list.size // 非空(原生 int)
val item = list[0] // 推断为平台类型(普通 Java 对象)#当调用平台类型变量的方法时,Kotlin 不会在编译时报告可空性错误,
但在运行时调用可能会失败异常,因为空指针异常或者 Kotlin 生成的阻止空值传播的断言:
item.substring(1) // 允许,如果 item == null 会抛出异常
#当把一个平台值赋值给一个 Kotlin 变量时,
#可以依赖类型推断(该变量会具有推断出的的平台类型,
#如上例中 item 所具有的类型),或者可以选择所期望的类型(可空或非空类型均可):
val nullable: String? = item // 允许,没有问题
val notNull: String = item // 允许,运行时可能失败
-
-
一些 Kotlin 关键字在 Java 中是有效标识符:
in、object、is等。 如果一个 Java 库使用了 Kotlin 关键字作为方法,你仍然可以通过反引号(`)字符转义它来调用该方法:foo.
is(bar)
读取带有可空性注解的Java字段为安全空检测
具有可空性注解的 Java 类型并不表示为平台类型,而是表示为实际可空或非空的 Kotlin 类型。编译器支持多种可空性注解,包括:
- JetBrains (
org.jetbrains.annotations包中的@Nullable和@NotNull) - JSpecify (
org.jspecify.nullness) - Android(
com.android.annotations和android.support.annotations) - JSR-305(
javax.annotation,详见下文) - FindBugs(
edu.umd.cs.findbugs.annotations) - Eclipse(
org.eclipse.jdt.annotation) - Lombok(
lombok.NonNull) - RxJava 3 (
io.reactivex.rxjava3.annotations)
覆盖或实现Java带非null注解的函数
在 Kotlin 中,如果要覆盖包含 @NotNull 作为参数的 Java 方法,则需要 Kotlin 的绝对不可为空类型
import org.jetbrains.annotations.*;
public interface Game<T> {
public T save(T x) {}
@NotNull
public T load(@NotNull T x) {}
}
在 Kotlin 中成功覆盖 load() 方法,您需要将 T1 声明为绝对不可空(T1 和 Any):
interface ArcadeGame<T1> : Game<T1> {
override fun save(x: T1): T1
// T1 is definitely non-nullable
override fun load(x: T1 & Any): T1 & Any
}
读取Java私有属性
- 从Kotlin 1.8.20开始,您可以创建对Java私有属性的引用。
-
-
To enable this feature, set the
-language-version 2.1compiler option. In a Gradle project, you can do so by adding the following to yourbuild.gradle(.kts):public class Person {
private String name;
private int age;public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; }}
#Kotlin has always allowed you to write person.age, where age is a synthetic property.
#Now, you can also create references to Person::age and person::age. The same applies for name, as well.val persons = listOf(Person("Jack", 11), Person("Sofie", 12), Person("Peter", 11))
persons
// Call a reference to Java synthetic property:
.sortedBy(Person::age)
// Call Java getter via the Kotlin property syntax:
.forEach { person -> println(person.name) }
-
访问Java的静态成员
Java 类的静态成员会形成该类的"伴生对象"。无法将这样的"伴生对象"作为值来传递,但可以显式访问其成员,例如:
if (Character.isLetter(a)) { ...... }
读取Java 泛型
Kotlin 的泛型与 Java 有点不同(参见泛型)。 当将 Java 类型导入 Kotlin 时,有以下转换:
- Java 的通配符转换成类型投影:
-
Foo<? extends Bar>转换成Foo<out Bar!>!Foo<? super Bar>转换成Foo<in Bar!>!
- Java的原始类型转换成星投影:
-
List转换成List<*>!,也就是List<out Any?>!
和 Java 一样,Kotlin 在运行时不保留泛型:对象不携带传递到他们构造器中的那些类型参数的实际类型。 例如 ArrayList<Integer>() 和 ArrayList<Character>() 是不能区分的。 这使得执行 is-检测不可能照顾到泛型。 Kotlin 只允许 is-检测星投影的泛型类型:
if (a is List<Int>) // 错误:无法检测它是否真的是一个 Int 列表
// but
if (a is List<*>) // OK:不保证列表的内容
10.3 在 Java 中调用 Kotlin
Java 可以轻松调用 Kotlin 代码。 例如,可以在 Java 方法中无缝创建与操作 Kotlin 类的实例。 然而,在将 Kotlin 代码集成到 Java 中时, 需要注意 Java 与 Kotlin 之间的一些差
[私有+getset] 读取Kotlin属性
Kotlin 属性会编译成以下 Java 元素:
- 一个 getter 方法,名称通过加前缀 get****算出
- 一个 setter 方法,名称通过加前缀 set****算出(只适用于 var****属性)
- 一个私有字段,与属性名称相同(仅适用于具有幕后字段的属性)
例如,var firstName: String 编译成以下 Java 声明:
private String firstName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
如果属性的名称以 is 开头,则使用不同的名称映射规则:getter 的名称与属性名称相同,并且 setter 的名称是通过将 is 替换为 set 获得。 例如,对于属性 isOpen,其 getter 会称做 isOpen(),而其 setter 会称做 setOpen()。 这一规则适用于任何类型的属性,并不仅限于 Boolea
[@JvmField]读取Kotlin实例的属性为字段,也就是直接用.访问实例变量
如果需要在 Java 中将 Kotlin 属性作为字段暴露,那就使用 @JvmField注解对其标注 。 该字段将具有与底层属性相同的可见性, 也就是说转Java代码后不会调用get\set而是直接访问字段。
如果一个属性具备这几点,那么可以用 @JvmField 注解该属性:
-
有幕后字段(backing field)
-
非私有
-
没有
open、override或者const修饰符 -
不是被委托的属性
//kotlin
class User(id: String) {
@JvmField val ID = id
val nID = id
}
使用 @JvmField****的属性
- ID****是一个只读属性,但由于使用了 @JvmField****注解,它被编译为一个公共的、不可变的字段( final****字段)。
- 在 Java 中,可以直接通过对象实例访问 ID****字段。
普通的 val****属性
-
nID****是一个普通的只读属性,Kotlin 编译器会为其生成一个私有字段和一个公共的 getter 方法。
-
在 Java 中,需要通过 getter 方法访问 nID**。**
public final class User {
// 公共字段 ID,对应 Kotlin 的 @JvmField
public final String ID;// 私有字段 nID,对应 Kotlin 的普通 val 属性 private final String nID; // 构造函数 public User(String id) { this.ID = id; // 初始化公共字段 ID this.nID = id; // 初始化私有字段 nID } // Getter 方法,对应 Kotlin 的 nID 属性 public String getNID() { return nID; }}
// Java
class JavaClient {
public String getID(User user) {
return user.ID;
}
}
延迟初始化的属性(在Java中)也会暴露为字段。 该字段的可见性与 lateinit 属性的 setter 相同。
[@JvmField] 读取Kotlin的伴生对象的属性为静态字段
伴生对象翻译为Java代码后,其实是构建一个静态类Companion,同时在当前类中持有这个静态类的静态实例 static Companion companion,并封装为get\set+实例变量。通常这些实例变量字段是私有的,但可以通过以下方式之一暴露出来:
- @JvmField 注解
lateinit修饰符const修饰符
使用 @JvmField****标注这样的属性使其成为与属性本身具有相同可见性的静态字段。
class Key(val value: Int) {
companion object {
@JvmField
val COMPARATOR: Comparator<Key> = compareBy<Key> { it.value }
}
}
- Key****类:
-
Key是一个不可变类,包含一个final字段value和一个构造函数。- 提供了一个
getValue()方法来访问value属性。
- COMPARATOR****静态字段:
-
-
Kotlin 的
@JvmField注解将COMPARATOR编译为 Java 中的公共静态字段。 -
compareBy { it.value }被翻译为一个实现了Comparator<Key>接口的匿名类。 -
匿名类的
compare方法使用Integer.compare比较两个Key对象的value值。import java.util.Comparator;
public final class Key {
private final int value;// 构造函数 public Key(int value) { this.value = value; } // 获取 value 属性的方法 public int getValue() { return value; } // 静态字段 COMPARATOR,对应 Kotlin 的 @JvmField public static final Comparator<Key> COMPARATOR = new Comparator<Key>() { @Override public int compare(Key key1, Key key2) { return Integer.compare(key1.getValue(), key2.getValue()); } };}
// Java
Key.COMPARATOR.compare(key1, key2);
-
[@JvmStatic] 读取Kotlin伴生对象函数为静态方法
如上所述,Kotlin 将包级函数表示为静态方法。 Kotlin 还可以为具名对象或伴生对象中定义的函数生成静态方法,如果你将这些函数标注为 @JvmStatic 的话。 如果你使用该注解,编译器既会在相应对象的类中生成静态方法, 也会在对象自身中生成实例方法。例如:
class C {
companion object {
@JvmStatic fun callStatic() {}
fun callNonStatic() {}
}
}
- Companion****静态内部类:
-
- Kotlin 的
companion object被编译为一个名为Companion的静态内部类。
- Kotlin 的
-
-
Companion类有一个私有构造函数,防止外部实例化
-
-
Companion类中包含了所有未使用@JvmStatic注解的方法(如callNonStatic)。
-
-
- 非静态方法:
-
-
-
-
- 没有使用
@JvmStatic注解的方法(如callNonStatic)需要通过Companion实例调用。 - 在 Java 中调用方式为:
C.Companion.callNonStatic()。
- 没有使用
-
-
- 静态方法( @JvmStatic**)**:
-
-
使用
@JvmStatic注解的方法(如callStatic)被直接编译为静态方法。 -
在 Java 中可以直接通过类名调用:
C.callStatic()。public final class C {
// 静态内部类 Companion,对应 Kotlin 的 companion object
public static final class Companion {
// 非静态方法 callNonStatic
public void callNonStatic() {
// 方法体为空
}// 私有构造函数,防止实例化 private Companion() {} } // 静态实例 Companion,Kotlin 编译器自动生成 public static final Companion Companion = new Companion(); // 使用 @JvmStatic 注解的方法被编译为静态方法 public static void callStatic() { // 方法体为空 }}
-
现在,callStatic() 在 Java 中是静态的,而 callNonStatic() 不是:
C.callStatic(); // 没问题
C.callNonStatic(); // 错误:不是一个静态方法
C.Companion.callStatic(); // 保留实例方法
C.Companion.callNonStatic(); // 唯一的工作方式
[研发if处理] 读取Kotlin的非空安全性
当从 Java 中调用 Kotlin 函数时,没人阻止我们将 null 作为非空参数传递。 这就是为什么 Kotlin 给所有期望非空参数的公有函数生成运行时检测。 这样我们就能在 Java 代码里立即得到 NullPointerException。
[研发trycatch处理]读取Koltin函数抛出受检异常
Kotlin 没有受检异常。 所以,通常 Kotlin 函数的 Java 签名不会声明抛出异常。 于是,如果有一个这样的 Kotlin 函数:
// example.kt
package demo
fun writeToFile() {
/*......*/
throw IOException()
}
然后想要在 Java 中调用它并捕捉这个异常:
// Java
try {
demo.Example.writeToFile();
} catch (IOException e) {
// 错误:writeToFile() 未在 throws 列表中声明 IOException
// ......
}
因为 writeToFile() 没有声明 IOException,从 Java 编译器得到了一个报错消息。 为了解决这个问题,要在 Kotlin 中使用 @Throws 注解。
@Throws(IOException::class)
fun writeToFile() {
/*......*/
throw IOException()
}
[研发读Appkt] 读取Kotlin包级函数
在 org.example 包内的 app.kt 文件中声明的所有的函数和属性,包括扩展函数, 都编译成一个名为 org.example.AppKt 的 Java 类的静态方法。
// app.kt
package org.example
class Util
fun getTime() { /*......*/ }
// Java
new org.example.Util();
org.example.AppKt.getTime();
可以使用 @JvmName 注解自定义生成的 Java 类的类名:
@file:JvmName("DemoUtils")
package org.example
class Util
fun getTime() { /*......*/ }
// Java
new org.example.Util();
org.example.DemoUtils.getTime();
读取Kotlin的接口中函数
自 JDK 1.8 起,Java 中的接口可以包含默认方法。 To make all non-abstract members of Kotlin interfaces default for the Java classes implementing them, compile the Kotlin code with the -Xjvm-default=all compiler option.
这是一个带有默认方法的 Kotlin 接口的一个示例:
// compile with -Xjvm-default=all
interface Robot {
fun move() { println("~walking~") } // will be default in the Java interface
fun speak(): Unit
}
默认实现对于实现该接口的 Java 类都可用。
//Java 实现
public class C3PO implements Robot {
// 来自 Robot 的 move() 实现隐式可用
@Override
public void speak() {
System.out.println("I beg your pardon, sir");
}
}
C3PO c3po = new C3PO();
c3po.move(); // 来自 Robot 接口的默认实现
c3po.speak();
用 @JvmName 解决签名冲突
有时我们想让一个 Kotlin 中的具名函数在字节码中有另外一个 JVM 名称。 最突出的例子是由于类型擦除引发的:
fun List<String>.filterValid(): List<String>
fun List<Int>.filterValid(): List<Int>
这两个函数不能同时定义,因为它们的 JVM 签名是一样的:filterValid(Ljava/util/List;)Ljava/util/List;。 如果我们真的希望它们在 Kotlin 中用相同名称,我们需要用 @JvmName 去标注其中的一个(或两个),并指定不同的名称作为参数:
fun List<String>.filterValid(): List<String>
@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>
在 Kotlin 中它们可以用相同的名称 filterValid 来访问,而在 Java 中,它们分别是 filterValid 和 filterValidInt。
[ @JvmOverloads ] 读取Kotlin带默认参数的函数生成重载
通常,如果你写一个有默认实参值的 Kotlin 函数,在 Java 中只会有一个所有参数都存在的完整参数签名的方法可见 ,如果希望向 Java 调用者暴露多个重载,可以使用 @JvmOverloads 注解。
该注解也适用于构造函数、静态方法等。它不能用于抽象方法,包括在接口中定义的方法。
class Circle @JvmOverloads constructor(centerX: Int, centerY: Int, radius: Double = 1.0) {
@JvmOverloads fun draw(label: String, lineWidth: Int = 1, color: String = "red") { /*......*/ }
}
**对于每一个有默认值的参数,都会生成一个额外的重载,这个重载会把这个参数和它右边的所有参数都移除掉。**在上例中,会生成以下代码 :
// 构造函数:
Circle(int centerX, int centerY, double radius)
Circle(int centerX, int centerY)
// 方法
void draw(String label, int lineWidth, String color) { }
void draw(String label, int lineWidth) { }
void draw(String label) { }
请注意,如次构造函数中所述,如果一个类的所有构造函数参数都有默认值,那么会为其生成一个公有的无参构造函数。这就算没有 @JvmOverloads 注解也有效。
读取Kotlin的Nothing 类型
类型 Nothing 是特殊的,因为它在 Java 中没有自然的对应。确实,每个 Java 引用类型,包括 java.lang.Void 都可以接受 null 值,但是 Nothing 不行。因此,这种类型不能在 Java 世界中准确表示。这就是为什么在使用 Nothing 参数的地方 Kotlin 生成一个原始类型:
fun emptyList(): List<Nothing> = listOf()
// 会翻译成
// List emptyList() { ...... }
调用Kotlin中的泛型函数
fun <U : KUltronEngine?> getUltronEngine(): U? {
try {
return mUltronEngine as U
} catch (e: Exception) {
e.printStackTrace()
KUltronSDKLog.e("try-catch", e.message?: "")
return null
}
}
lazTradeEngine.<LazDeliveryStatusUltronService>getUltronEngine()
获取Kotlin中类的KClass对象
java中
JvmClassMappingKt.getKotlinClass(KDeliveryFeedbackComponent.class)