Kotlin语法手册(四)
在使用kotlin时,由于掌握的不够牢靠,好多时候也还是Java编程的习惯,浪费了kotlin提供的语言特性,方便性,间接性,在阅读一些Android开源库的时候,由于好多都是kotlin语法编写的,看的比较费劲,还得去查阅kotlin的语法,比较不方便,故把kotlin的语法记录下来,方便查阅温故,巩固自己的基础知识。
异常
kotlin 中异常处理的基本形式和 Java 类似,和 Java 不同的是,kotlin 中 throw 结构是一个表达式,可以作为另一个表达式的一部分来使用
//如果条件不满足,则将抛出异常,status 变量也不会初始化
val status = if (index in 0..10) index else throw IllegalArgumentException("参数错误")
在 Java 中对于受检异常必须显式地处理,通过 try/catch 语句捕获异常或者是抛给其调用者来处理。而 kotlin 不区分受检异常和未受检异常,不用指定函数抛出的异常,可以处理也可以不处理异常。
在 kotlin 中 ,try 关键字引入了一个表达式,从而可以把表达式的值赋给一个变量。如果一个 try 代码块执行正常,代码块中最后一个表达式就是结果,如果捕获到了一个异常,则相应 catch 代码块中最后一个表达式就是结果。
fun main() {
compute(5) //fun end : true
compute(100) //fun end : null
}
fun compute(index: Int) {
val status = try {
if (index in 0..10) true else throw IllegalArgumentException("参数错误")
} catch (e: Exception) {
null
}
println("fun end : " + status)
}
如果在 catch 语句中使用 return 结束了 compute 函数,则没有任何输出
中缀调用与解构声明
中缀调用
fun main() {
val maps = mapOf(1 to "leavesC", 2 to "ye", 3 to "https://juejin.cn/user/923245496518439")
maps.forEach { key, value -> println("key is : $key , value is : $value") }
}
使用 “to” 来声明 map 的 key 与 value 之间的对应关系,这种形式的函数调用被称为中缀调用。
kotlin 标准库中对 to 函数的声明如下所示,其作为扩展函数存在,且是一个泛型函数,返回值 Pair 最终再通过解构声明分别将 key 和 value 传给 Map
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
中缀调用只能与只有一个参数的函数一起使用,无论是普通的函数还是扩展函数。中缀符号需要通过 infix 修饰符来进行标记
解构声明
把一个对象拆解成多个变量的需求,在 kotlin 中这种语法称为解构声明
data class Person(val name: String, val age: Int)
fun main() {
val (name, age) = Person("leavesC", 24)
println("Name: $name , age: $age")
//Name: leavesC , age: 24
}
例子中将 Person 变量结构为了两个新变量:name 和 age
一个解构声明会被编译成以下代码:
val name = person.component1()
val age = person.component2()
需要注意的是,componentN()
函数需要用 operator
关键字标记,以允许在解构声明中使用它们,对于数据类来说,其自动生成了 componentN()
函数,而对非数据类,为了使用解构声明,需要我们自己来手动声明函数
class Point(val x: Int, val y: Int) {
operator fun component1() = x
operator fun component2() = y
}
fun main() {
val point = Point(100, 200)
val (x, y) = point
println("x: $x , y: $y")
//x: 100 , y: 200
}
此外,解构声明也可以用在 for 循环中
val list = listOf(Person("leavesC", 24), Person("leavesC", 25))
for ((name, age) in list) {
println("Name: $name , age: $age")
}
对于遍历 map 同样适用
val map = mapOf("leavesC" to 24, "ye" to 25)
for ((name, age) in map) {
println("Name: $name , age: $age")
}
同样也适用于lambda表达式
val map = mapOf("leavesC" to 24, "ye" to 25)
map.mapKeys { (key, value) -> println("key : $key , value : $value") }
如果在解构声明中不需要某个变量,那么可以用下划线取代其名称,此时不会调用相应的 componentN()
操作符函数
val map = mapOf("leavesC" to 24, "ye" to 25)
for ((_, age) in map) {
println("age: $age")
}
Object 关键字
对象声明
在 kotlin 的世界中,可以通过对象声明这一功能来实现 Java 中的单例模式,将类声明与该类的单一实例声明结合到一起。与类一样,一个对象声明可以包含属性、方法、初始化语句块等的声明,且可以继承类和实现接口,唯一不被允许的是构造方法。与普通类的实例不同,对象声明在定义的时候就被立即创建了,不需要在代码的其它地方调用构造方法,因此为对象声明定义构造方法是没有意义的。
interface Fly {
fun fly()
}
open class Eat {
fun eat() {
println("eat")
}
}
object Animal : Eat(), Fly {
override fun fly() {
println("fly")
}
}
fun main() {
Animal.fly()
Animal.eat()
}
kotlin 中的对象声明被编译成了通过静态字段来持有它的单一实例的类,这个字段名字始终都是 INSTANCE
伴生对象
如果需要一个可以在没有类实例的情况下调用但是需要访问类内部的函数(类似于 Java 中的静态变量/静态函数),可以将其写成那个类中的对象声明的成员
通过关键字 companion ,就可以获得通过容器类名称来访问这个对象的方法和属性的能力,不再需要显式地指明对象的名称
class Test {
companion object {
const val NAME = ""
fun testFun() {
}
}
}
fun main() {
Test.NAME
Test.testFun()
}
工厂模式
可以利用伴生对象来实现工厂模式
private class User private constructor(val name: String) {
companion object {
fun newById(id: Int) = User(id.toString())
fun newByDouble(double: Double) = User(double.toString())
}
}
fun main() {
//构造函数私有,无法创建
//val user1 = User("leavesC")
val user2 = User.newById(10)
val user3 = User.newByDouble(1.3)
}
实现接口
伴生对象也可以实现接口,且可以直接将包含它的类的名字当做实现了该接口的对象实例来使用
private class User private constructor(val name: String) {
companion object UserLoader : Runnable {
override fun run() {
}
}
}
fun newThread(runnable: Runnable) = Thread(runnable)
fun main() {
//User 会直接被当做 Runnable 的实例
val thread = newThread(User)
val thread2 = newThread(User.UserLoader)
}
委托
委托模式
委托模式是一种基本的设计模式,该模式下有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。kotlin 原生支持委托模式,可以零样板代码来实现,通过关键字 by 实现委托
interface Printer {
fun print()
}
class DefaultPrinter : Printer {
override fun print() {
println("DefaultPrinter print")
}
}
class CustomPrinter(val printer: Printer) : Printer by printer
fun main() {
val printer = CustomPrinter(DefaultPrinter())
printer.print() //DefaultPrinter print
}
CustomPrinter 的 by 子句表示将会在 CustomPrinter 中存储 printer 变量,并且编译器将为 CustomPrinter 隐式生成 Printer 接口的所有抽象方法,并将这些方法的调用操作转发给 printer
此外,CustomPrinter 也可以决定自己实现部分方法或全部自己实现,但重写的成员不会在委托对象的成员中调用 ,委托对象的成员只能访问其自身对接口成员实现
interface Printer {
val message: String
fun print()
fun reprint()
}
class DefaultPrinter : Printer {
override val message: String = "DefaultPrinter message"
override fun print() {
println(message)
}
override fun reprint() {
println("DefaultPrinter reprint")
}
}
class CustomPrinter(val printer: Printer) : Printer by printer {
override val message: String = "CustomPrinter message"
override fun reprint() {
println("CustomPrinter reprint")
}
}
fun main() {
val printer = CustomPrinter(DefaultPrinter())
printer.print() //DefaultPrinter message
printer.reprint() //CustomPrinter reprint
}
属性委托
kotlin 支持通过委托属性将对一个属性的访问操作委托给另外一个对象来完成,对应的语法格式是:
val/var <属性名>: <类型> by <表达式>
在 by 后面的表达式是该 委托, 因为属性对应的 get()
(与 set()
)会被委托给它的 getValue()
与 setValue()
方法。
属性的委托不必实现任何的接口,但是需要提供一个 getValue()
函数(与 setValue()
——对于 var 属性)。
class Delegate {
private var message: String? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("${thisRef?.javaClass?.name}, thank you for delegating '${property.name}' to me!")
return message ?: "null value"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {\
println("$value has been assigned to '${property.name}' in ${thisRef?.javaClass?.name}.")
message = value
}
}
class Example {
var strValue: String by Delegate()
}
fun main() {
val example = Example()
println(example.strValue)
// test.Example, thank you for delegating 'strValue' to me!
// null value
example.strValue = "leaveC"
// leaveC has been assigned to 'strValue' in test.Example.
println(example.strValue)
// test.Example, thank you for delegating 'strValue' to me!
// leaveC
}
延迟属性
lazy() 是接受一个 lambda 并返回一个 Lazy < T > 实例的函数,返回的实例可以作为实现延迟属性的委托,第一次调用 get() 会执行已传递给 lazy() 函数的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果
class Example {
val lazyValue1: String by lazy {
println("lazyValue1 computed!")
"Hello"
}
val lazyValue2: String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
println("lazyValue2 computed!")
computeLazyValue()
}
private fun computeLazyValue() = "leavesC"
}
fun main() {
val example = Example()
println(example.lazyValue1) //lazyValue1 computed! Hello
println(example.lazyValue1) //Hello
println(example.lazyValue2) //lazyValue2 computed! leavesC
}
默认情况下,对于 lazy 属性的求值是带同步锁的(synchronized),即带有 LazyThreadSafetyMode.SYNCHRONIZED 参数,此时该值只允许同一时刻只能有一个线程对其进行初始化,并且所有线程会看到相同的初始化值。如果初始化委托的同步锁不是必需的,即如果允许多个线程同时执行,那么可以将 LazyThreadSafetyMode.PUBLICATION 作为参数传递给 lazy() 函数。 而如果你确定初始化将总是发生在单个线程,那么可以使用 LazyThreadSafetyMode.NONE 模式, 此时不会有任何线程安全的保证以及相关的资源开销
可观察属性
Delegates.observable() 接受两个参数:初始值以及修改属性值时的回调函数。当为属性赋值后就会调用该回调函数,该回调函数包含三个参数:被赋值的属性、旧值与新值
fun main() {
val example = Example()
example.age = 24 //kProperty.name: age , oldValue: -100 , newValue: 24
example.age = 27 //kProperty.name: age , oldValue: 24 , newValue: 27
}
class Example {
var age: Int by Delegates.observable(-100) { kProperty: KProperty<*>, oldValue: Int, newValue: Int ->
println("kProperty.name: ${kProperty.name} , oldValue: $oldValue , newValue: $newValue")
}
}
如果想要拦截一个赋值操作并判断是否进行否决,可以使用 vetoable() 函数,通过返回一个布尔值来决定是否进行拦截,该判断逻辑是在属性被赋新值生效之前进行
fun main() {
val example = Example()
example.age = 24 //kProperty.name: age , oldValue: -100 , newValue: 24
example.age = -10 //kProperty.name: age , oldValue: 24 , newValue: -10
example.age = 30 //kProperty.name: age , oldValue: 24 , newValue: 30 (oldValue 依然是 24,说明第二次的赋值操作被否决了)
}
class Example {
var age: Int by Delegates.vetoable(-100) { kProperty: KProperty<*>, oldValue: Int, newValue: Int ->
println("kProperty.name: ${kProperty.name} , oldValue: $oldValue , newValue: $newValue")
age <= 0 //返回true 则表示拦截该赋值操作
}
}
把属性储存在映射中
可以在一个 map 映射里存储属性的值,然后把属性的存取操作委托给 map 进行管理
fun main() {
val student = Student(
mapOf(
"name" to "leavesC",
"age" to 24
)
)
println(student.name)
println(student.age)
}
class Student(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
在上述示例中,属性 name 和 age 都是不可变的(val),因此 map 的类型也是 Map 而非 MutableMap(MutableMap 在赋值后可以修改),因此如果为了支持 var 属性,可以将只读的 Map 换成 MutableMap
局部委托属性
可以将局部变量声明为委托属性
class Printer {
fun print() {
println("temp.Printer print")
}
}
fun getPrinter(): Printer {
println("temp.Printer getPrinter")
return Printer()
}
//局部委托
fun example(getPrinter: () -> Printer) {
val lPrinter by lazy(getPrinter)
val valid = true
if (valid) {
lPrinter.print()
}
}
fun main() {
example { getPrinter() }
//temp.Printer getPrinter
//temp.Printer print
}
委托变量只会在第一次访问时才会进行初始化,因此如果 valid 为 false 的话,getPrinter() 方法就不会被调用
注解
注解是将元数据附加到代码元素上的一种方式,附件的元数据就可以在编译后的类文件或者运行时被相关的源代码工具访问
注解的语法格式如下所示:
annotation class AnnotationName()
注解的附加属性可以通过用元注解标注注解类来指定:
- @Target 指定该注解标注的允许范围(类、函数、属性等)
- @Retention 指定该注解是否要存储在编译后的 class 文件中,如果要保存,则在运行时可以通过反射来获取到该注解值
- @Repeatable 标明允许在单个元素上多次使用相同的该注解
- @MustBeDocumented 指定该注解是公有 API 的一部分,并且应该包含在生成的 API 文档中显示的类或方法的签名中
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Repeatable
@MustBeDocumented
annotation class AnnotationName()
注解可以声明包含有参数的构造函数
annotation class OnClick(val viewId: Long)
允许的参数类型有:
- 原生数据类型,对应 Java 原生的 int 、long、char 等
- 字符串
- class 对象
- 枚举
- 其他注解
- 以上类型的数组
注解参数不能包含有可空类型,因为 JVM 不支持将 null 作为注解属性的值来存储
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class OnClick(val viewId: Long)
class AnnotationsTest {
@OnClick(200300)
fun onClickButton() {
println("Clicked")
}
}
fun main() {
val annotationsTest = AnnotationsTest()
for (method in annotationsTest.javaClass.methods) {
for (annotation in method.annotations) {
if (annotation is OnClick) {
println("method name: " + method.name) //method name: onClickButton
println("OnClick viewId: " + annotation.viewId) //OnClick viewId: 200300
}
}
}
}