一、重载算术运算符
1)重载二元运算符
使用operator关键字来声明plus函数,然后可以直接使用+号来求和。+号运算将会自动转换为plus函数的调用。
可重载的二元运算符如下
自定义类型的运算符,基本上和标准数字类型的运算符有着相同优先级,运算符*、/和%具有相同的优先级,高于+和-运算符的优先级。
Kotlin运算符不会自动支持交换性,需单独定义。
可以重载operator函数:可以定义多个同名的,但参数类型不同的方法。
Kotlin没有为标准数字类型定义任何位运算符,也不允许你为自定义类型定义它们。
2)重载复合赋值运算符
复合赋值运算符有+=、-=。+和-运算符总是返回一个新的集合。二元算术运算符有命名相似的对应函数如plusAssign、minusAssign、timesAssign等。
+=和-=运算符用于可变集合时,始终在一个地方修改它们;而它们用于只读集合时,会返回一个修改过的副本(这意味着只有当引用只读集合的变量被声明为var时,才能使用+=和-=)。作为它们的运算数,可以使用单个元素,也可以使用元素类型一致的其他集合。
3)重载一元运算符
可重载的一元运算符有,
| 表达式 | 函数名 |
|---|---|
| +a | unaryPlus |
| -a | unaryMinus |
| !a | not |
| ++a, a++ | inc |
| --a, a-- | dec |
当定义inc和dec函数来重载自增和自减的运算符时,编译器自动支持与普通数字类型的前缀和后缀自增运算符相同的语义。
二、重载比较运算符
在Kotlin中,可以对任何对象使用比较运算符(==、!=、>、<等),而不仅仅限于基本数据类型。
1)等号运算符:“equals”
和其他运算符不同的是,==和!=可以用于可空运算数,因为这些运算符事实上会检查运算数是否为null。
===运算符检查两个参数是否是同一个对象,该运算符不能重载。
equals的实现是在Any类中定义的,不能实现为扩展函数,因为继承自Any类的实现优先于扩展函数。
2)排序运算符:compareTo
比较运算符(<,>,《=和>=)的使用将被转换为compareTo,compareTo的返回类型必须为Int。p1 < p2表达式等价于p1.compareTo(p2) < 0。如下
class Person(val firstName: String, val lastName: String) : Comparable<Person> {
override fun compareTo(other: Person): Int {
return compareValuesBy(this, other, Person::lastName, Person::firstName)
}
}
compareValuesBy函数接收用来计算比较值的一系列回调,按顺序依次调用回调方法,两两一组分别做比较,并返回结果。如果值不同,则返回比较结果;如果它们相同,则继续调用下一个;如果没有更多回调来调用,则返回0.这些回调函数可以像lambda一样传递,或者像这里做的一样,作为属性引用传递。
所有Java中实现了Comparable接口的类,都可以在Kotlin中使用简洁的运算符语法,不用再增加扩展函数,如
println("abc" < "bac")
三、集合与区间的约定
1)通过下标来访问元素:“get”和“set”
在Kotlin中可以使用类似Java中数组的方式来操作map中的额元素,如下
val value = map[key]
mutableMap[key] = newValue
使用下标运算符读取元素会被转换为get运算符方法的调用,写入元素将调用set。Map和MutableMap的接口已经定义了这些方法。get的参数可以是任何类型,而不只是Int,还可以定义具有多个参数的get方法。
2)“in”的约定
in运算符,用于检查某个对象是否属于集合,相应的函数叫做contains。
3)rangeTo的约定
..运算符是调用rangeTo函数的一个简洁方法。优先级低于算术运算符。
4)在“for”循环中使用“iterator”的约定
在Kotlin中,for循环中也可以使用in运算符,for(x in list) {...}将被转换成list.iterator()的调用。
四、解构声明和组件函数
解构声明用到了约定的原理,要在解构声明中初始化每个变量,将调用名为componentN的函数,其中N是声明中变量的位置。componentN为编译器为数据类主构造方法中声明的属性生成的函数。
解构声明主要使用场景之一,是从一个函数返回多个值,定义一个数据类来保存返回所需的值,并将它作为函数的返回类型。在调用函数后,可以用解构声明的方式,来轻松地展开它,使用其中的值。componentN函数在数组和集合上也有定义。如下
data class NameComponents(val name: String, val extension: String)
fun splitFilename(fullName: String): NameComponents {
val (name, extension) = fullName.split(',', limit = 2)
return NameComponents(name, extension)
}
标准库只允许使用此语法来访问一个对象的前五个元素。
解构声明不仅可以用作函数中的顶层语句,还可以用在其他可以声明变量的地方。
五、类委托
继承的脆弱性是指当你扩展一个类并重写某些方法时,你的代码就变得依赖你自己继承的那个类的实现细节,当系统不断演进并且基类的实现被修改或者新方法被添加进去时,你做出的关于类行为的假设会失效,所以你的代码也许最后就以不正确的行为而告终。
由于继承的脆弱性,Kotlin默认将类视为final,默认无法被继承,Kotlin引入类委托来替代继承功能。无论什么时候实现一个接口,都可以使用by关键字将接口的实现委托到另一个对象,如
class CountingSet<T>(val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet {
var objectsAdded = 0
override fun add(element: T): Boolean {
objectsAdded++
return innerSet.add(element)
}
override fun addAll(c: Collection<T>): Boolean {
objectsAdded += c.size
return innerSet.addAll(c)
}
}
将MutableCollection的实现委托给innerSet,具有了innerSet的所有方法并覆盖了innerSet中的add和addAll方法。
六、委托属性
委托属性处理起来比把值存储在支持字段中更复杂,却不用在每个访问器中都重复这样的逻辑。例如,这些属性可以把它们的值存储在数据库表中,在浏览器会话中,在一个map中等。
1)委托属性的基本操作
委托属性的基本语法为
class Foo {
var p: Type by Delegate()
}
属性p将它的访问器逻辑委托给了另一个对象,这里是Delegate类的一个新的实例,通过关键字by对其后的表达式求值来获取这个对象。
编译器创建一个隐藏的辅助属性,并使用委托对象的实例进行初始化,初始属性p会委托给该实例。我们把它称为delegate,如下
class Foo {
private val delegate = Delegate()
var p: Type
set(value: Type) = delegate.setValue(..., value)
get() = delegate.getValue(...)
编译器会自动生成一个辅助属性delegate,Delegate类必须具有getValue和setValue方法(后者仅适用于可变属性)。“p”的访问都会调用对应的“delegate”的getValue和setValue方法。Delegate类的简单实现大致如下,
class Delegate {
operator fun getValue(...) { ... }
operator fun setValue(..., value: Type)
}
class Foo {
var p: Type by Delegate()
}
通过val foo = Foo(),val oldValue = foo.p调用delegate.getValue(...)来实现属性的修改,通过foo.p = newValue调用delegate.setValue(..., newValue)来实现属性的修改。
2)使用委托属性:惰性初始化和“by lazy()”
惰性初始化在第一次访问属性时,才根据需要创建对象的一部分。当初始化过程消耗大量资源并且在使用对象时并不总是需要数据时,这个非常有用。
举个例子,一个Person类,可以用来访问一个人写的邮件列表,邮件存储在数据库中,访问比较耗时。你希望只有在首次访问时才加载邮件,并只执行一次。如下为loadEmails函数,用来从数据库中检索电子邮件:
class Email { /*...*/}
fun loadEmails(person: Person): List<Email> {
return listOf(/*...*/)
}
可以使用支持属性来实现邮件列表的初始化,但是代码较繁琐且不是线程安全的。使用委托属性来实现会让代码变得简单,可以封装用于存储值的支持属性和确保该值只被初始化一次的逻辑。在这里可以使用标准库函数lazy返回的委托,
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
lazy函数返回一个对象,该对象具有一个名为getValue且签名正确的方法,把它与by关键字一起使用来创建一个委托属性。lazy的参数是一个lambda,可以调用它来初始化这个值。默认情况下,lazy函数是线程安全的,如果需要,可以设置其他选项来告诉它要使用哪个锁,或者完全避开同步。
3)实现委托属性
先看一个例子:当一个对象的属性更改时通知监听器,如对象显示在UI时,对象变化UI自动更新。
下面有一个监听器工具,PropertyChangeSupport类维护了一个监听器列表,并向它们发送PropertyChangeEvent事件。要使用它,你通常需要把这个类的一个实例存储为bean类的一个字段,并将属性更改的处理委托给它。
创建一个工具类PropertyChangeAware,用来存储PropertyChangeSupport的实例并监听属性更改。之后继承这个工具类,以访问changeSupport,如下,
open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.removePropertyChangeListener(listener)
}
}
Kotlin标准库中Delegates.observable已经实现了可观察的属性逻辑,再使用Delegates.observable来实现属性修改的通知,如下,
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
private val observer = {
prop: KProperty<*>, oldValue:Int, newValue: Int ->
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
var age: Int by Delegates.observable(age, observer)
var salary: Int by Delegates.observable(salary, observer)
}
by右边的表达式不一定是新创建的实例,也可以是函数调用、另一个属性或任何其他表达式,只要这个表达式的值,是能够被编译器用正确的参数类型来调用getValue和setValue的对象。与其他约定一样,getValue和setValue可以是对象自己声明的方法或扩展函数。
4)委托属性的变换规则
如下有一个具有委托属性的类:
class C {
var prop: Type by MyDelegate()
}
val c = C()
MyDelegate实例会被保存到一个隐藏的属性中,它被称为。编译器也将用一个KProperty类型的对象来代表这个属性,它被称为。编译器生成的代码如下,
class C {
private val <delegate> = MyDelegate()
val prop: Type
get() = <delegate>.getValue(this, <property>)
set(value: Type) = <delegate>.setValue(this, <property>, value)
}
因此,在每个属性访问器中,编译器都会生成对应的getValue和setValue方法。
5)在map中保存属性值
如下使用委托属性把值存到map中,
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String by _attributes
}
因为标准库已经在标准Map和MutableMap接口上定义了getValue和setValue扩展函数,所以可以把map作为委托属性。属性的名称将自动用作在map中的键,属性值作为map中的值,则val name: String by _attributes会将_attributes["name"]赋给name属性。
6)框架中的委托属性
假设数据库中Users的表包含两列数据:字符串类型的name和整型的age。可以在Kotlin中定义Users和User类。在Kotlin代码中,所有存储在数据库中的用户实体的加载和更改都可以通过User类的实例来操作。
class User(id: EntityID): Entity(id) {
var name: String by Users.name
var age: Int by Users.age
}
object Users : IdTable() {
val name: Column<String> = varchar("name", 50).index()
val age: Column<Int> = interger("age")
}
框架已经对Column类定义了getValue和setValue方法,满足Kotlin的委托约定:
operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
//从数据库中获取值
}
operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T) {
//更新值到数据库
}
可以使用Column属性(Users.name)作为被委托属性(name)的委托。当在代码中写入user.age += 1时,代码的执行将类似于user.ageDelegate.setValue(user.ageDelegate.getValue() + 1)(省略了属性和对象实例的参数)的操作。getValue和setValue方法负责检索和更新数据库中的信息。