kotlin学习
数据类型
Kotlin 基本数据类型
Kotlin 的基本数值类型包括 Byte、Short、Int、Long、Float、Double 等。
不同于 Java 的是,字符不属于数值类型,是一个独立的数据类型。
整数类型
● Byte: 8 位,范围从 -128 到 127。
● Short: 16 位,范围从 -32,768 到 32,767。
● Int: 32 位,范围从 -2^31 到 2^31 - 1。
● Long: 64 位,范围从 -2^63 到 2^63 - 1。
浮点数类型
● Float: 32 位,单精度,带有 6-7 位有效数字。
● Double: 64 位,双精度,带有 15-16 位有效数字。
字符类型
● Char: 16 位的 Unicode 字符。
布尔类型
● Boolean: 有两个值:true 和 false。
字符串类型
● String: 一系列字符的序列。
数组类型
Kotlin 提供了数组类型来存储同种类型的元素,例如:
● IntArray: 存储 Int 类型的数组。
● DoubleArray: 存储 Double 类型的数组。
● Array: 泛型数组,可以存储任意类型。
位操作符
对于Int和Long类型,还有一系列的位操作符可以使用,分别是:
shl(bits) – 左移位 (Java’s <<)
shr(bits) – 右移位 (Java’s >>)
ushr(bits) – 无符号右移位 (Java’s >>>)
and(bits) – 与
or(bits) – 或
xor(bits) – 异或
inv() – 反向
数组
数组用类 Array 实现,并且还有一个 size 属性及 get 和 set 方法,由于使用 [] 重载了 get 和 set 方法,所以我们可以通过下标很方便的获取或者设置数组对应位置的值。
数组的创建两种方式:一种是使用函数arrayOf();另外一种是使用工厂函数。如下所示,我们分别是两种方式创建了两个数组:
fun main(args: Array<String>) {
//[1,2,3]
val a = arrayOf(1, 2, 3)
//[0,2,4]
val b = Array(3, { i -> (i * 2) })
//读取数组内容
println(a[0]) // 输出结果:1
println(b[1]) // 输出结果:2
}
原生数组与类型数组
● 原生类型数组是 Kotlin 中的内置数组,它们是不可变的,并且只能存储基本数据类型(如 Int, Double, Float, Long, Short, Char, Byte, Boolean 等)的值。
● 常规的类型化数组在 Kotlin 中是通过 Array 类实现的,它们可以存储任何类型的数据(包括对象和基本数据类型),并且是可变的。
Array 类可变,是内容可变,不是可以变长
val primitiveArr:IntArray=intArrayOf(3,4,5)//原生数组
var simpleArray:Array<Int> = arrayOf(1, 2, 3)//类型数组
var empty:Array<Int> = emptyArray<Int>()//空类型数组
arrayOf创建的数组是不可变长的的,变长数组使用MutableArray或MutableList
var empty:Array<Int> = emptyArray<Int>()
var mutbleEmpty:MutableList<Int > =empty.toMutableList()
println("变为可变长数组并添加元素")
mutbleEmpty.addAll(0, listOf(5,4,3,2,1))
println(mutbleEmpty.toString())
println("转为类型数组")
empty=mutbleEmpty.toTypedArray()
println(empty.joinToString("->"))
控制与流程
条件跳转
if作为表达式
在 Kotlin 中, if 是一个表达式:它会返回一个值。
max = if (a > b) a else b
if 表达式可以嵌套使用
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
}
when表达式
when可以定义多分支条件语句,形似switch结构,定义
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> {
print("x is neither 1 nor 2")
}
}
一般情况必须有else
分支,除非条件是枚举类型,切全部覆盖取值
高级用法
1. 可以使用表达式作为分支条件
when (x) {
s.toInt() -> print("s encodes x")
else -> print("s does not encode x")
}
2. 检测一个值在( in )或者不在( !in )一个区间或者集合中
when (x) {
in 1..10 -> print("x is in the range")
in validNumbers -> print("x is valid")
!in 10..20 -> print("x is outside the range")
else -> print("none of the above")
}
3. 检测一个值是( is )或者不是( !is )一个特定类型的值。
fun hasPrefix(x: Any) = when(x) {
is String -> x.startsWith("prefix")
else -> false
}
4. when 也可以用来取代 if - else if 链。
when {
x.isOdd() -> print("x is odd")
y.isEven() -> print("y is even")
else -> print("x+y is odd")
}
循环
for循环
遍历如下集合
for (item in collection) print(item)
在数字区间迭代
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
}
或者你可以用库函数 withIndex :
fun main() {
val array = arrayOf("a", "b", "c")
//sampleStart
for ((index, value) in array.withIndex()) {
println("the element at $index is $value")
}
//sampleEnd
}
while循环
包括while
和 do-while
在循环中跳转
Break 与 Continue 标签
在 Kotlin 中任何表达式都可以用标签来标记。 标签的格式为标识符后跟 @ 符号,例
如: abc@ 、 fooBar@ 。 要为一个表达式加标签,我们只要在其前加标签即可。
loop@ for (i in 1..100) {
for (j in 1..100) {
if (……) break@loop
}
}
标签限定的 break 跳转到刚好位于该标签指定的循环后面的执行点。 continue 继续标签指
定的循环的下一次迭代。
return到标签
//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()
}
kotlin特性
空安全
定义可以为null的引用
类型系统区分一个引用可以容纳 null (可空引用)还是不能容纳(非空引用)。 例如,String 类型的常规变量不能容纳 null
,如果引用可以为null
,使用String?
使用为null值的变量的时候,编译器会报错
var b: String? = "abc" // 可以设置为空
b = null // ok
val l = b.length // 错误:变量“b”可能为空
val l=b?.length //ok
- 在条件中使用
在条件中判断不为空后可以使用变量
if (b != null && b.length > 0) {
print("String of length ${b.length}")
}
- 访问可空变量的属性的第二种选择是使用安全调用操作符 ?.
fun main() {
//sampleStart
val a = "Kotlin"
val b: String? = null
println(b?.length)
println(a?.length) // 无需安全调用
//sampleEnd
}
Elvis 操作符
当有一个可空的引用 b 时,可以说“如果 b 不是 null ,就使用它;否则使用某个非空的值”:
val l: Int = if (b != null) b.length else -1
//除了写完整的 if 表达式,还可以使用 Elvis 操作符 ?: 来表达:
val l = b?.length ?: -1
如果 ?: 左侧表达式不是 null ,Elvis 操作符就返回其左侧表达式,否则返回右侧表达式。
请注意,当且仅当左侧为 null 时,才会对右侧表达式求值。
因为 throw 和 return 在 Kotlin 中都是表达式,所以它们也可以用在 elvis 操作符右侧。这
可能会很方便,例如,检测函数参数:
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ……
}
- !! 操作符
第三种选择是为 NPE 爱好者准备的:非空断言运算符( !! )将任何值转换为非空类型,若
该值为 null 则抛出异常。可以写 b!! ,这会返回一个非空的 b 值 (例如:在我们示例
中的 String )或者如果 b 为 null ,就会抛出一个 NPE 异常:
val l = b!!.length
相等性
Kotlin 中有两类相等性:
结构相等( == ——用 equals() 检测)
引用相等( === ——检测两个引用指向同一对象)
如需提供自定义的相等检测实现,请覆盖 equals(other: Any?): Boolean 函数:
class Point(val x: Int, val y: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Point) return false
// Compares properties for structural equality
return this.x == other.x && this.y == other.y
}
}
异常
使用 try …… catch 表达式来捕获异常:
try {
// 一些代码
} catch (e: SomeException) {
// 处理程序
} finally {
// 可选的 finally 块
}
可以有零到多个 catch 块, finally 块可以省略。 但是 catch 与 finally 块至少需有一个。
Try 是一个表达式
try 是一个表达式,意味着它可以有一个返回值:
val a: Int? = try { input.toInt() } catch (e: NumberFormatException) { null }
try -表达式的返回值是 try 块中的最后一个表达式或者是(所有) catch 块中的最后一个
表达式。 finally 块中的内容不会影响表达式的结果。
受检异常
Kotlin 没有受检异常。
类与对象
单例对象
在 Kotlin 中,单例对象是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点。Kotlin 提供了几种方式来实现单例模式:
\1. 对象声明(Object Declaration):
Kotlin 允许你使用 object
关键字来声明一个单例对象。这种方式是最简单和最常用的实现单例的方法。
object MySingleton {
fun doSomething() {
// 实现细节
}
}
// 使用单例对象
MySingleton.doSomething()
在这个例子中,MySingleton
是一个单例对象,它在第一次被引用时被创建,并且之后所有的引用都指向同一个实例。
\2. 伴生对象(Companion Object):
在类内部,你可以使用 companion object
来创建一个单例对象。这个对象与外部类共享相同的名称,并且它也是单例的。
class MyClass {
companion object {
fun doSomething() {
// 实现细节
}
}
}
// 使用单例对象
MyClass.doSomething()
在这个例子中,MyClass
的伴生对象 companion object
充当了单例的角色。
\3. 枚举单例:
Kotlin 枚举类型默认是单例的,每个枚举常量都是唯一的实例。
enum class MySingleton {
INSTANCE;
fun doSomething() {
// 实现细节
}
}
// 使用单例对象
MySingleton.INSTANCE.doSomething()
在这个例子中,MySingleton
是一个枚举,INSTANCE
是它的唯一实例。
-
懒汉式单例(Lazy Initialization):
虽然 Kotlin 没有内置的懒汉式单例实现,但你可以通过
lazy
函数来实现。object MySingleton { private val instance: MySingleton by lazy MySingleton( fun getInstance(): MySingleton return instanc fun doSomething() // 实现细节
}
}
// 使用单例对象
MySingleton.getInstance().doSomething()
在这个例子中,`MySingleton` 使用 `lazy` 函数来延迟实例的创建,直到第一次被调用时。
## **继承**
所有类继承自Any, 有三个方法: equals() 、 hashCode() 与 toString() 。因此,为所有 Kotlin 类都定义
了这些方法。
类默认不可以继承,使类可继承,使用
```kotlin
open class Base // 该类开放继承
成员函数需要继承,也需要添加open关键字,子类函数添加override关键字
open class Shape {
open fun draw() { /*……*/ }
fun fill() { /*……*/ }
}
class Circle() : Shape() {
override fun draw() { /*……*/ }
}
子类如果需要禁止向下传递继承性,需要添加final关键字
final override fun draw()
属性
Kotlin 类中的属性既可以用关键字 var 声明为可变的, 也可以用关键字 val 声明为只读
的。
为属性定义getter和setter方法
var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value) // 解析字符串并赋值给其他属性
}
延迟初始化属性与变量
一般地,属性声明为非空类型必须在构造函数中初始化。 然而,这经常不方便。例如:属性
可以通过依赖注入来初始化, 或者在单元测试的 setup 方法中初始化。 这种情况下,你不能
在构造函数内提供一个非空初始器。 但你仍然想在类体中引用该属性时避免空检测。
lateinit var subject: TestSubject
检测一个 lateinit var 是否已初始化
要检测一个 lateinit var 是否已经初始化过,请在该属性的引用上使用.isInitialized
:
if (foo::bar.isInitialized) {
println(foo.bar)
}
接口
可以在接口中定义属性。在接口中声明的属性要么是抽象的,要么提供访问器的实现。在接
口中声明的属性不能有幕后字段(backing field)。
一个接口可以从其他接口派生,意味着既能提供基类型成员的实现也能声明新的函数与属
性。很自然地,实现这样接口的类只需定义所缺少的实现:
函数式接口
只有一个抽象方法的接口称为函数式接口或 单一抽象方法(SAM)接口。函数式接口可以有
多个非抽象成员,但只能有一个抽象成员。
可以用 fun 修饰符在 Kotlin 中声明一个函数式接口。
fun interface KRunnable {
fun invoke()
}
函数式接口使用lambda实现
如果不使用 SAM 转换,那么你需要像这样编写代码:
// 创建一个类的实例
val isEven = object : IntPredicate {
override fun accept(i: Int): Boolean {
return i % 2 == 0
}
}
通过利用 Kotlin 的 SAM 转换,可以改为以下等效代码:
// 通过 lambda 表达式创建一个实例
val isEven = IntPredicate { it % 2 == 0 }
SAM转换
SAM转换就是将lambda显示转换为函数式接口实例,但要求Kotlin的函数类型和该SAM(单一抽象方法)的函数类型一致。SAM转换一般都是自动发生的。
SAM构造方法是编译器为了将lambda显示转换为函数式接口实例而生成的函数。SAM构造函数只接收一个参数 —— 被用作函数式接口单抽象方法体的lambda,并返回该函数式接口的实例。
泛型
在泛型编程中,逆变(Contravariance)和协变(Covariance)是两种不同的类型变化方式,它们描述了泛型类型参数在继承结构中如何与子类型和父类型相互作用。以下是它们的主要区别:
协变(Covariance):
● 定义:协变允许泛型类型参数与其子类型保持一致。如果 B 是 A 的子类型,那么 List 是 List 的子类型。
● 使用场景:当你需要从泛型类型中读取值时,可以使用协变。例如,一个返回特定类型对象的函数可以返回该类型的任何子类型对象。
● 关键字:在 Kotlin 中,协变通过 out 关键字来表示。
● 例子:一个函数返回一个 Animal 类型的对象,它可以返回任何 Animal 的子类型,如 Dog 或 Cat。
逆变(Contravariance):
● 定义:逆变允许泛型类型参数与其父类型保持一致。如果 B 是 A 的子类型,那么 List 是 List 的子类型。
● 使用场景:当你需要向泛型类型中写入值时,可以使用逆变。例如,一个接受特定类型对象的函数可以接受该类型的任何父类型对象。
● 关键字:在 Kotlin 中,逆变通过 in 关键字来表示。
● 例子:一个函数接受一个 Animal 类型的参数,它可以接收任何 Animal 的父类型,如 Pet(如果 Pet 是 Animal 的父类型)。
区别总结:
向性协变是“输出”方向的,意味着泛型类型参数可以被用来产生(返回)对象。逆变是“输入”方向的,意味着泛型类型参数可以被用来消费(接受)对象。
类型关系协变保持了子类型关系,即如果 B 继承自 A,那么 List 继承自 List。逆变则相反,它允许你将更泛化的类型用作参数。
全性协变是安全的,因为它不会允许你将错误类型的数据写入泛型容器。逆变也是安全的,因为它确保了你可以将任何子类型的对象传递给期望父类型参数的函数。
在 Kotlin 中,协变和逆变都是通过声明处型变(declaration-site variance)来实现的,这意味着它们在泛型类型的声明处指定,而不是在使用处。这与 Java 中的使用处型变(use-site variance)不同,后者通过通配符(如 ? extends T
和 ? super T
)来实现。
伴生对象
在 Kotlin 中,伴生对象(Companion Object)是一种特殊的单例对象,它与类紧密相关联,并且可以访问类的私有成员。伴生对象通常用于实现以下功能:
\1. 提供访问类静态成员的实例方式:在 Kotlin 中没有静态字段或方法的概念,但可以通过伴生对象来模拟静态成员的行为。
\2. 创建工厂方法:伴生对象可以包含创建类实例的工厂方法,这些方法可以访问类的私有构造函数。
\3. 实现 Singleton 模式:伴生对象本身就是一个单例,因此可以用它来实现单例模式。
\4. 存储与类相关的常量:伴生对象中可以定义常量,这些常量与类相关联,并且可以在类的外部访问。
\5. 扩展函数和属性:伴生对象可以包含扩展函数和属性,这些函数和属性可以被类的所有实例共享。
伴生对象的声明使用 companion
关键字,如下所示:
class MyClass {
companion object {
var someProperty: String = "Some value"
fun someFunction() {
// 可以访问类的私有成员
}
}
}
在上面的例子中,MyClass
有一个名为 someProperty
的伴生对象属性,以及一个名为 someFunction
的伴生对象方法。这些成员可以通过类名直接访问,如下所示:
val propertyValue = MyClass.someProperty
MyClass.someFunction()
伴生对象也可以使用 Companion
命名,这是 companion object
的一个实例,可以通过它访问伴生对象的成员:
val propertyValue = MyClass.Companion.someProperty
MyClass Companion.someFunction()
在 Kotlin 1.2 之后,还可以使用 object
声明类的实例属性和函数,这使得伴生对象的使用变得更加灵活。例如:
class MyClass {
object Instance {
var someProperty: String = "Some value"
}
fun someFunction() {
// 可以访问伴生对象的成员
println(Instance.someProperty)
}
}
在这个例子中,Instance
是 MyClass
的一个实例属性,它是一个对象,可以包含属性和方法。这种方式提供了另一种实现单例或静态成员的方式。
在 JVM 平台,如果使用 @JvmStatic 注解,你可以将伴生对象的成员生成为真正的静
态方法和字段
委托
委托(Delegation)是一种设计模式,它允许一个对象(称为委托者)将其某些职责或行为“委托”给另一个对象(称为被委托者)。这种模式通常用于实现代码的复用、减少重复代码、增强代码的模块化以及提高灵活性和可扩展性。
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()
}
覆盖由委托实现的接口成员
编译器会使用 override 覆盖的实现而不是委托对象中的。
属性委托
● 延迟属性(lazy properties): 其值只在首次访问时计算。
● 可观察属性(observable properties): 监听器会收到有关此属性变更的通知。
● 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。
为了涵盖这些(以及其他)情况,Kotlin 支持 委托属性:
class Example {
var p: String by Delegate()
}
语法是: val/var <属性名>: <类型> by <表达式> 。在 by 后面的表达式是该 委托, 因为属性对
应的 get() (与 set() )会被委托给它的 getValue() 与 setValue() 方法。 属性的委托不
必实现接口,但是需要提供一个 getValue() 函数(对于 var 属性还有 setValue() )。
延迟属性
lazy()接受一个 lambda 并返回一个 Lazy 实例的函数,返回的实例可以作为实现延
迟属性的委托。 第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果。
后续调用 get() 只是返回记录的结果。
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main() {
println(lazyValue)
println(lazyValue)
}
默认情况下,对于 lazy 属性的求值是同步锁的(synchronized):该值只在一个线程中计算,
但所有线程都会看到相同的值。如果初始化委托的同步锁不是必需的,这样可以让多个线程
同时执行,那么将 LazyThreadSafetyMode.PUBLICATION 作为参数传给 lazy() 。
可观察属性 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 或者执行
其他“动态”任务的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属
性。
局部委托属性
你可以将局部变量声明为委托属性。 例如,你可以使一个局部变量惰性初始化:
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
memoizedFoo 变量只会在第一次访问时计算。 如果 someCondition 失败,那么该变量根本不
会计算。
可见性
类、对象、接口、构造函数、方法与属性及其 setter 都可以有可见性修饰符。 getter 总是与属
性有着相同的可见性。
在 Kotlin 中有这四个可见性修饰符: private 、 protected 、 internal 和 public 。 默认
可见性是 public 。
包
kotlin函数和属性可以不依赖类而定义,所以可以使用可见性修饰符
函数、属性和类、对象和接口可以直接在包内的顶层声明:
// 文件名:example.kt
package foo
fun baz() { …… }
class Bar { …… }
如果你不使用任何可见性修饰符,默认为 public ,这意味着你的声明将随处可见。
如果你声明为 private ,它只会在声明它的文件内可见。
如果你声明为 internal ,它会在相同模块内随处可见。
扩展函数
声明一个扩展函数需用一个接收者类型也就是被扩展的类型来作为他的前缀。 下面代码为MutableList 添加一个 swap 函数:
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // “this”对应该列表
this[index1] = this[index2]
this[index2] = tmp
}
如果一个类定义有一个成员函数与一个扩展函数,而这两个函数又有相同的接收者类型、 相
同的名字,并且都适用给定的参数,这种情况总是取成员函数。
可空接收者
注意可以为可空的接收者类型定义扩展。这样的扩展可以在对象变量上调用, 即使其值为
null。
扩展属性
与扩展函数类似,Kotlin 支持扩展属性:
val <T> List<T>.lastIndex: Int
get() = size - 1
对象表达式与对象声明
对象表达式
使用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) { /*……*/ }
})
- 匿名对象作为返回值和局部变量的类型
class C {
private fun getObject() = object {
val x: String = "x"
}
fun printX() {
println(getObject().x)
}
}
- 对象表达式中的代码可以访问来自包含它的作用域的变量
对象声明
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ……
}
}
在 object 关键字后跟一个名称。 就像变量声明一样,对象声明不是一个表达式,不能用在赋值语句的右边。
对象声明的初始化过程是线程安全的并且在首次访问时进行。如需引用该对象,直接使用其名称即可:
DataProviderManager.registerDataProvider(……)
函数
Kotlin 函数使用 fun 关键字声明:
fun double(x: Int): Int {
return 2 * x
}
参数
函数参数使用 Pascal 表示法定义——name: type。参数用逗号隔开, 每个参数必须有显式类
型:
fun powerOf(number: Int, exponent: Int): Int { /*……*/ }
默认实参
函数参数可以有默认值,当省略相应的参数时使用默认值。这可以减少重载数量:
fun read(
b: ByteArray,
off: Int = 0,
len: Int = b.size,
) { /*……*/ }
如果一个有默认值参数在一个无默认值的参数之前,那么该默认值只能通过使用具名实参调
用该函数来使用:
fun foo(
bar: Int = 0,
baz: Int,
) { /……/ }
foo(baz = 1) // 使用默认值 bar = 0
如果在默认实参之后的最后一个参数是 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
返回 Unit 的函数
如果一个函数并不返回有用的值,其返回类型是 Unit 。 Unit 是一种只有一个值—— Unit
的类型。 这个值不需要显式返回
单表达式函数
当函数体由单个表达式构成时,可以省略花括号并且在 = 符号之后指定代码体即可:
fun double(x: Int): Int = x * 2
当返回值类型可由编译器推断时,显式声明返回类型是可选的:
fun double(x: Int) = x * 2
具有代码块的函数必须指定返回值类型,除非返回的是Unit
在Kotlin中,内联函数(inline functions)是一种特殊的函数类型,它们在编译时会被直接嵌入到调用点处,而不是像普通函数那样在运行时通过函数调用来执行。内联函数的主要目的是避免函数调用的开销,尤其是在高频率调用或者性能敏感的场景下。
内联函数
内联函数的关键字
内联函数使用inline
关键字定义:
inline fun myInlineFunction() {
// 函数体
}
内联函数的特点
- 消除函数调用开销:内联函数在编译阶段被展开,因此在运行时不会产生额外的栈帧和参数传递等开销。
- 局部变量捕获:如果内联函数内部需要访问外部作用域的变量,这些变量必须声明为
val
(不可变)或使用reified
类型参数。这是因为内联后,外部作用域的变量需要在其作用域之外保持有效。 - Lambda表达式的内联:当一个函数接受lambda作为参数时,可以使用
inline
和crossinline
关键字来优化lambda的性能。crossinline
保证了lambda的内联性,并且不允许lambda捕获外部变量。
使用示例
假设我们有一个简单的例子,其中包含一个接受lambda作为参数的函数:
fun repeat(times: Int, block: () -> Unit) {
for (i in 0 until times) {
block()
}
}
如果我们频繁地调用这个repeat
函数,每次调用都会产生额外的开销。为了提高性能,我们可以将它改为内联函数:
inline fun repeat(times: Int, crossinline block: () -> Unit) {
for (i in 0 until times) {
block()
}
}
在这个例子中,block
参数前的crossinline
关键字确保了即使在多个不同的上下文中调用此函数,lambda也会被正确地内联。
注意事项
虽然内联可以提高性能,但过度使用可能会导致代码膨胀和编译时间增加。因此,在考虑是否使用内联时,应该权衡其带来的性能提升与潜在的代码复杂度和编译时间成本。
总的来说,Kotlin中的内联函数是一个强大的工具,用于减少运行时开销并提高代码效率,特别是在处理大量小规模、高频调用的情况下。然而,合理使用这一特性对于保持代码质量和构建速度至关重要。
协程
协程(Coroutine)是一种程序组件,它在执行过程中可以被暂停和恢复,而不会像线程那样占用过多的资源。协程提供了一种更轻量级的并发机制,适用于执行异步操作和并发任务,特别是在 I/O 密集型和高级别的结构化并发任务中。
在 Kotlin 中,协程是通过 Kotlin 协程库实现的,它提供了构建器(如 launch
、async
、runBlocking
等)来启动和管理协程。Kotlin 协程库也提供了对异常处理、取消操作和调度器的支持。
runBlocking
和 launch
是 Kotlin 协程中的两个不同的构建器,它们用于以不同的方式启动协程:
runBlocking
runBlocking
是一个阻塞当前线程的函数,直到协程执行完成。- 它用于在特定的线程(例如主线程)中同步执行协程代码。
runBlocking
会暂停调用它的线程,直到协程体执行完毕。- 在协程内部使用
runBlocking
可能会导致死锁,特别是在主线程中使用时,因为它会阻塞主线程直到runBlocking
内的协程完成。 - 通常用于主线程中需要等待异步结果的场景,或者在测试中同步执行协程。
示例:
fun main() = runBlocking {
// 这个代码块会在主线程中同步执行
println("Hello, ")
delay(1000)
println("World!")
}
launch
launch
是一个非阻塞的函数,它在协程的上下文中启动一个新的协程。launch
不会阻塞调用它的线程,允许程序继续执行。- 它通常用于启动不需要立即结果的后台任务,例如异步操作或者并发任务。
launch
返回一个Job
对象,可以用来取消协程或者检查协程的状态。
launch
构建器用于启动一个新的协程,并且它确实不会阻塞调用它的线程。launch
需要一个协程作用域(CoroutineScope
)来运行。在全局范围内(如 main
函数),你不能直接使用 launch
,因为它需要一个明确的协程作用域。
示例:
fun main()=runBlocking{
val job = launch {
// 这个代码块会在一个新的协程中异步执行
println("Hello, ")
delay(1000)
println("World!")
}
println("Job is still running...")
job.join() // 等待协程完成
}
取消协程的执行
使用下面代码取消一个协程
job.cancel() // 取消该作业
job.join() // 等待作业执行结束
两者可以合并为一个函数job.cancelAndJoin() // 取消一个作业并且等待它结束
计算可取消
我们有两种方法来使执行计算的代码可以被取消。第一种方法是定期调用挂起函数来检查取
消。另一种方法是显式的检查取消状态。让我们试试第二种方法。
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // 可以被取消的计算循环
// 每秒打印消息两次
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 等待一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消该作业并等待它结束
println("main: Now I can quit.")
//sampleEnd
}
在 finally 中释放资源
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} finally {
println("job: I'm running finally")
}
运行不能取消的代码块
将相应的代码包装在 withContext(NonCancellable) {……} 中,并使用withContext 函数以及 NonCancellable 上下文,如释放数据库连接关闭文件,及时协程被取消也应该可以使用资源
finally {
withContext(NonCancellable) {
println("job: I'm running finally")
delay(1000L)
println("job: And I've just delayed for 1 sec because I'm non-cancella
ble")
}
}
在 Android应用
在 Android 或其他 Kotlin 应用程序中,你通常会使用 lifecycleScope
或 viewModelScope
来提供协程作用域:
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launch {
println("Running in a coroutine with lifecycle scope")
// 执行后台任务
}
}
}
区别总结:
runBlocking
用于同步执行,它会阻塞当前线程。launch
用于异步执行,它不会阻塞当前线程。- 在需要等待结果或者需要同步执行协程代码时使用
runBlocking
。 - 在需要并发执行任务且不需要立即结果时使用
launch
。
重要提示:避免在 Android 的主线程中使用 runBlocking
,因为这会导致死锁。在主线程中处理异步任务时,应该使用 lifecycleScope.launch
或 viewModelScope.launch
等,这些作用域与生命周期相关联,可以避免死锁。
使用 async 并发
在概念上,async 就类似于 launch。它启动了一个单独的协程,这是一个轻量级的线程并与其
它所有的协程一起并发的工作。不同之处在于 launch 返回一个 Job 并且不附带任何结果
值,而 async 返回一个 Deferred —— 一个轻量级的非阻塞 future, 这代表了一个将会在稍后
提供结果的 promise。你可以使用 .await() 在一个延期的值上得到它的最终结果, 但是
Deferred 也是一个 Job ,所以如果需要的话,你可以取消它。
标准库函数
also函数
它接收一个接收者对象和一个 lambda 表达式。also 函数的目的是将 lambda 表达式应用于接收者对象,并且返回接收者对象本身。在链式调用中执行一些操作,而不改变接收者对象的值。also 是一个用于在不改变对象值的情况下执行附加操作的函数,它使得代码更加清晰和流畅。
以下的例子,copy一个对象,并打印一个一段话
val person = Person("John").apply {
name = "Jane"
age = 30
}
val copyOfPerson = person.also {
println("Copying person with name: ${it.name}")
}.copy()