java在标准库中有一些与特定的类相关联的语言特性,如实现了java.lang.Iterable接口的对象可以在for循环中使用,实现了java.lang.AutoCloseable接口的对象可以在try-with-resources语句中使用。
但在kotlin中,一些功能是与特定的函数名相关,而不是与特定的类型绑定。kotlin使用约定的原则,不像java依赖类型。kotlin可以通过扩展函数机制来为现有的类增添新的方法,可以把任意约定方法定义为扩展函数。
一. 重载算术运算符
java中,算术运算符只能用于基本数据类型,+运算符可以与String值一起使用。如果给集合添加元素时,想要能够用 += 运算符就完美。在kotlin中,是可以这样做的。
1. 重载二元算术运算
先来看一个例子:定义Point类(代表一个点),把点的(X, Y)坐标分别加到一起。
data class Point(val x: Int, val y: Int) {
// 定义一个名为 "plus" 的方法
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
val p1 = Point(1, 2)
val p2 = Point(3, 4)
println(p1 + p2) // 通过使用 + 号 来调用 "plus" 方法
//输出结果>>> Point(x=4, y=6)
operator关键字声明plus函数。所有的重载运算符函数都需要使用该关键字标记,表示这个函数作为约定实现。
使用operator修饰符声明plus函数后,可以直接使用 + 号来求和。其实就是调用plus函数。
除了可以把运算符声明为一个成员函数外,还可以把它定义为一个扩展函数
operator fun Point.plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
kotlin限定了能够重载哪些运算符,以及在类中定义对应名字函数。下表就是可重载的二元运算符:
表达式 | 函数名 |
---|---|
a * b | times |
a / b | div |
a % b | mod |
a + b | plus |
a - b | minus |
在定义运算符时,两个运算数可以是不同的类型
operator fun Point.times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}
val p1 = Point(10, 20)
println(p1 * 1.5) // 不会自定支持交换性,不能 1.5 * p1
//输出结果>>> Point(x=15, y=30)
kotlin运算符不会自定支持交换性,不能 1.5 * p1。如果希望可以,需要单独定义一个运算符
operator fun Double.times(p: Point): Point {...}
运算符函数的返回类型也可以是任意一个运算数类型。
这个运算符,接收一个Char作为左值,Int作为右值,然后返回一个String类型。
operator fun Char.times(count: Int) : String {
return toString().repeat(count)
}
println('b' * 3)
//输出结果>>> bbb
2. 重载复合赋值运算符
+= 、-=等这些运算符称为复合赋值运算符。
var p = Point(1, 2)
p += Point(3, 4) // 等同于 p = p + Point(3, 4)写法
println(p)
//输出结果>>> Point(x=4, y=6)
+=运算符可以修改变量所引用的对象,但不会重新分配引用,如:将一个元素添加到可变集合中
val numbers = ArrayList<Int>()
numbers += 12
println(numbers[0])
//输出结果>>> 12
如果定义了一个返回值为Unit,名为plusAssign函数,kotlin会在用到 += 运算符的地方调用它。二元运算符对应函数,如:minusAssign、timesAssign
kotlin标准库为可变集合定义了plusAssign函数:
operator fun <T> MutableCollection<T>.plusAssign(element: T) {
this.add(element)
}
在代码中使用 += 时,理论上 plus 和 plusAssign都有可能被调用,所以尽量不要同时给一个类添加 plus 和 plusAssign 运算。
如:例子中的Point类,是一个不可变的,那么应该只提供返回一个新值plus运算,如果一个类是可变的,那么只需要提供plusAssign和类似的运算。
kotlin标准库支持集合的两种方法,+ 和 - 运算符总是返回一个新的集合,+= 和 -= 运算符用于可变集合时,始终在一个地方修改它们。而用于只读集合时,返回一个修改过的副本,意味着只有当引用只读集合的变量声明为var时,才能使用+=和-=。
val list = arrayListOf(1, 2)
list += 3 // += 修改list
val newList = list + listOf(4, 5) // 返回一个包含所有元素的新列表
println(list)
//输出结果>>> [1, 2, 3]
println(newList)
//输出结果>>> [1, 2, 3, 4, 5]
3. 重载一元运算符
预先定义一个名称来声明函数(成员函数或者扩展函数),并用修饰符operator标记。
// 一元运算符无参数
operator fun Point.unaryMinus(): Point {
return Point(-x, -y) // 坐标取反,然后返回
}
可重载的一元算法的运算符:
表达式 | 函数名 |
---|---|
+a | unaryPlus |
-a | unaryMinus |
!a | not |
++a,a++ | inc |
--a, a-- | dec |
自增运算符案例:
operator fun BigDecimal.inc() = this + BigDecimal.ONE
var bd = BigDecimal.ZERO
println(bd++) //后缀运算:在执行后增加(先返回bd变量当前值,然后执行++)
//输出结果>>> 0
println(++bd) //前缀运算:在执行前增加(与后缀运算相反)
//输出结果>>> 2
二. 重载比较运算符
在kotlin中,可以对于任何对象使用比较运算符(==、!=、>、< 等),不仅仅限于基本数据类型,可以直接使用比较运算符。不像java需要调用equals或者compareTo函数。
1. 等号运算符:"equals"
如果在kotlin中使用 == 运算符,会将被转换成equals方法的调用。
== 和 != 可以用于可空运算符,因为这些运算符事实上会检查运算数是否为null。比较 a == b 会检查a是否为非空,如果不是,就调用a.equals(b),否则,只有两个参数都是空引用,结果才是true
案例中Point类,被标记为数据类(data),equals的实现会由编译器自动生成。如果需要手动实现,如下:
class Point(val x: Int, val y: Int) {
override fun equals(other: Any?): Boolean {
// 优化:检查参数是否与this是同一个对象
if (this === other) return true
// 检查参数类型
if (other !is Point) return false
// other智能转换为Point来访问x,y属性
return other.x == x && other.y == y
}
}
println(Point(1, 2) == Point(1, 2)) //输出结果>>> true
println(Point(2, 3) != Point(3, 4)) //输出结果>>> true
println(null == Point(1, 2)) //输出结果>>> false
恒等运算符(===)来检查两个参数是否是同一个对象的引用(如果是基本数据类型,检查是否是相同的值)。在实现了equals函数后,通常使用这个(===)运算符来优化调用代码,但是===运算符不能被重载。
equals方法是在Any类中定义的,所以equals方法不需要标记为operator,因为Any类中基本方法已经标记了。但是equals不能实现为扩展方法,因为继承自Any类的实现始终优先于扩展函数。
public open class Any {
// ...
public open operator fun equals(other: Any?): Boolean
}
!=运算符也会转换为equals方法调用,编译器会自动对返回值取反。
2. 排序运算符:compareTo
在java中,类可以实现Comparable接口,接口中定义的compareTo方法用于确定一个对象是否大于另一个对象。但是在java中,只有基本数据类型可以使用< 和 > 来比较,其它类型都需要element1.compareTo(element2)。
而在kotlin中,可以使用比较运算符(< 、> 、<=、>=),会被转换为compareTo,compareTo的返回类型必须为Int。
定义Person类实现compareTo方法:先比较firstName,如果相同,再比较lastName
class Person(
val firstName: String, val lastName: String
) : Comparable<Person> {
override fun compareTo(other: Person): Int {
return compareValuesBy( // 按顺序调用给定的方法,并比较它们的值
this, other,
Person::firstName, Person::lastName
)
}
}
val p1 = Person("a", "b");
val p2 = Person("a", "c");
println(p1 < p2)
//输出结果>>> true
可以使用kotlin标准库中的compareValuesBy函数来简洁地实现compareTo方法。所有java中实现了Comparable接口的类,都可以在kotlin使用简洁的运算符语法,不用再增加扩展函数。如:
println("abc" < "cba")
//输出结果>>> true
三. 集合与区间的约定
集合的操作通常都是通过下标。kotlin中所有这些操作都支持运算符语法:通过下标获取或者设置元素,可以使用语法a[b](称为下标运算符);可以使用in运算符来检查元素是否在集合区间内,也可以迭代集合。
1. 通过下标来访问元素:“get”和“set”
kotlin中,访问map中元素,可以通过方括号的方式:
val value = map[key]
也可以用同样的运算符来改变一个可变map的元素
mutable[key] = newValue
如何工作的呢?
在kotlin中,下标运算符是一个约定。使用下标运算符读取元素会被转换为get运算符方法的调用,写入元素调用set。Map和MutableMap的接口都已经定义了这些方法。
如何给自定义的类添加类似的方法呢?
实现get约定:还是以自定义Point类为例,使用方括号来引用点的坐标,p[0]访问X坐标,p[1]访问Y坐标
operator fun Point.get(index: Int): Int {
return when(index) {
0 -> x
1 -> y
else ->
throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
val p = Point(10, 20)
println(p[1])
//输出结果>>> 20
只需要定义一个get函数,并标记operator后,p[1]就会被转换为get方法的调用。
注意:get的参数可以是任意类型,而不只是Int。还可以定义具有多个参数的get方法。如果需要使用不同的健类型访问集合,也可以使用不同的参数类型定义多个重载的get方法。
实现set约定:上例中Point类是不可变的(变量是val修改),所以实现set约定没有意义。
接下来定义一个可变的点MutablePoint
data class MutablePoint(var x: Int, var y: Int)
operator fun MutablePoint.set(index: Int, value: Int) {
when(index) {
0 -> x = value
1 -> y = value
else ->
throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
val p = MutablePoint(10, 23)
p[1] = 24
println(p)
//输出结果>>> MutablePoint(x=10, y=24)
只需要定义一个set函数,并标记operator后,p[1]=24就会被转换为set方法的调用。
2. "in"的约定
集合支持的另一个运算符是in运算符:用来检查某个对象是否属于集合,对于的函数是contains。
实现in的约定:检查点是否属于一个矩形
data class Rectangle(val upperLeft: Point, val lowerRight: Point)
operator fun Rectangle.contains(p: Point): Boolean {
// 使用until函数来构建一个区间
return p.x in upperLeft.x until lowerRight.x &&
p.y in upperLeft.y until lowerRight.y
}
val rect = Rectangle(Point(10, 20), Point(50, 50))
println(Point(20, 30) in rect)
//输出结果>>> true
in右边的对象将会调用contains函数,in左边的对象将会作为函数入参。
3. rangeTo的约定
创建一个区间,使用 .. 语法。如:1..10表示从1到10的数字。 ..运算符是调用rangeTo函数的一个简洁方法。
rangeTo函数返回一个区间。可以为自定义的类定义这个运算符,但是如果该类实现了Comparable接口,就不需要了。可以通过kotlin标准库创建一个任意可比较元素的区间:
operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
例如:
val now = LocalDate.now();
val vacation = now..now.plusDays(10) // 创建一个从今天开始的10天的区间
println(now.plusWeeks(1) in vacation) // 检测一个特定的日期是否属于这个区间
//输出结果>>> true
now..now.plusDays(10)会被编译器转换为now.rangeTo(now.plusDays(10))。其中rangeTo并不是LocalDate的成员函数,而是Comparable的一个扩展函数。
rangeTo运算符的优先级低于算术运算符,最好把参数扩起来以免混淆:
val n = 9
println(1..(n + 1)) // 可以写成1..n + 1,但括起来更清晰一点
//输出结果>>> 1..10
表达式1..n.forEach { print(it) }不会被编译,必须把区间表达式括起来才能调用forEach方法
val n = 9
(1..n).forEach { print(it) }
//输出结果>>> 123456789
4. 在"for"循环中使用"iterator"的约定
在kotlin中,for循环中也可以使用in运算符,和做区间检查一样。但是在这种情况下它的含义是不同的:它被用来执行迭代。如:for(x in list) {...} 将被转换成list.iterator()的调用。
在kotlin中,iterator方法可以被定义为扩展函数,所以可以遍历一个常规的java字符串,标准库已经为CharSequence定义了一个扩展函数iterator
operator fun CharSequence.iterator(): CharIterator
for(c in "abc"){}
可以为自定义的类定义iterator方法:实现日期区间的迭代器
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
// 这个对象实现了遍历LocalDate元素的Iterator
object : Iterator<LocalDate> {
var current = start
// 日期用到了compareTo约定
override fun hasNext() =
current <= endInclusive
// 在修改前返回当前日期作为结果
override fun next() = current.apply {
// 把当前日期增加一天
current = plusDays(1)
}
}
val newYear = LocalDate.ofYearDay(2017, 1)
val daysOff = newYear.minusDays(1)..newYear
for (dayOff in daysOff) { println(dayOff) }
//输出结果>>> 2016-12-31
// 2017-01-01
四. 解构声明和组件函数
相信大家对数据类已经很熟悉了。
接下来解构声明,它是怎么工作的?
// 数据类
data class Point(val x: Int, val y: Int)
val p = Point(10, 20)
val (x, y) = p // 声明变量x,y,然后用p的组件来初始化
println(x)
//输出结果>>> 10
println(y)
//输出结果>>> 20
解构声明就像普通的变量声明,但它在括号中有多个变量。
解构声明也用到了约定原理。要在解构声明中初始化每个变量,将调用名为componentN的函数,其中N是声明中变量的位置。
对于数据类,编译器为每个在主构造方法中声明的属性生成一个componentN函数。
我们也可以手动为非数据类型声明这些功能:
class Point(val x: Int, val y: Int) {
operator fun component1() = x;
operator fun component2() = y;
}
讲这么多,那解构声明有哪些使用场景呢?
解构声明主要使用场景之一:是从一个函数返回多个值,可以定义一个数据类来保存返回所需的值,并将它作为函数的返回类型。然后用解构声明的方式,就可以轻松的展开它,使用其中的值。
举一个例子:将文件名分割成文件名和扩展名
// 声明一个数据类来持有值
data class NameComponents(
val name: String,
val extension: String
)
fun splitFilename(fullName: String): NameComponents {
val result = fullName.split(".", limit = 2)
// 返回一个数据类型的实例
return NameComponents(result[0], result[1])
}
val (name, ext) = splitFilename("example.kt")
println(name)
//输出结果>>> example
println(ext)
//输出结果>>> kt
componentN函数在数组和集合中也有定义。当已知大小的集合时,可以使用解构声明来处理集合。
改造一下splitFilename函数:
fun splitFilename(fullName: String): NameComponents {
val (name, ext) = fullName.split(".", limit = 2)
return NameComponents(name, ext)
}
componentN在标准库只允许使用此语法来访问一个对象的前五个元素。
接收一个函数返回多个值,可以使用标准库中的 Pair 和 Triple 类。
1. 解构声明和循环
解构声明不仅可以用作函数中的顶层语句,还可以在其它可以声明变量的地方,如:in 循环
fun printEntries(map: Map<String, String>) {
// 在in 循环中用解构声明
for ((key, value) in map) {
println("$key -> $value")
}
}
val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
printEntries(map)
//输出结果>>> Oracle -> Java
// JetBrains -> Kotlin
其中Map.Entry上扩展函数component1和component2,分别返回它们的健和值
for (entry in map.entries) {
val key = entry.component1()
val value = entry.component2()
// ...
}
五. 重用属性访问的逻辑:委托属性
1. 委托属性的基本操作
委托属性的基本语法:
class Foo {
var p: Type by Delegate()
}
属性p将它的访问器逻辑委托给了另一个对象,这里是Delegate类的一个新的实例。通过关键字by对其后的表达式求值来获取这个对象。
编译器创建一个隐藏的辅助属性,并使用委托对象的实例进行初始化,初始化属性p会委托给实例
class Foo {
// 编译器会自动生成一个辅助属性
private val delegate = Detegate()
// p的访问都会调用对应的delegate的getValue和setValue
var p: Type
set(value: Type) = delegate.setValue(...,value)
get() = delegate.getValue(...)
}
Detegate类必须具有setValue和getValue方法,可以是成员函数,也可以是扩展函数。
class Detegate {
// getValue包含了实现getter的逻辑
operator fun getValue(...) {...}
// setValue包含了实现setter的逻辑
operator fun setValue(..., value: Type) {...}
}
class Foo {
// 关键字by把属性关联上委托对象
var p: Type by Delegate()
}
val foo = Foo()
val oldValue = foo.p // 通过调用delegate.getValue(...)来实现属性的修改
foo.p = newValue // 通过调用delegate.setValue(..., newValue)来实现属性的修改
2. 使用委托属性:惰性初始化和“by lazy()“
惰性初始化:当第一次访问该属性的时候,才根据需要创建对象的一部分。
例如:一个Person类,用来访问一个人写的邮件列表。邮件存储在数据库中,访问耗时。但是只希望在首次访问时才加载邮件,并只执行一次
class Person {
// _emails属性用来保存数据,关联委托
private var _emails: List<String>? = null
val emails: List<String>
get() {
if (_emails == null) {
// 访问时加载邮件
_emails = loadEmails();
}
// 如果已经加载,直接返回
return _emails!!
}
private fun loadEmails(): List<String>? {
// 耗时
return listOf("1", "2");
}
}
val p = Person()
println(p.emails)
//输出结果>>> [1, 2]
如果有几个属性怎么办呢?且这个实现也不是线程安全的。kotlin提供了更好的解决方案:
使用委托属性会让代码变得简单,可以封装用于存储值的支持属性和确保该值只被初始化一次的逻辑。
可以使用标准库函数lazy返回委托
使用委托属性来实现惰性初始化:
class Person {
val emails by lazy { loadEmails() }
}
lazy函数返回一个对象,该对象具有一个名为getValue且签名正确的方法,因此可以把它与by关键字一起使用来创建一个委托属性。默认情况下,lazy函数是线程安全的。
3. 实现委托属性
在java中当一个对象的属性发生更改时通知监听器,具有用于此类通知的标准机制:PropertyChangeSupport和PropertyChangeEvent类。但是在kotlin不使用属性委托,怎么实现的呢?
PropertyChangeSupport类维护了一个监听器列表,并向它们发送PropertyChangeEvent事件。要使用它,通常需要把这个类的一个实例存储为bean类的一个字段,并将属性更改的处理委托给它。
为了避免在每个类中都创建这个字段,创建一个工具类,然后bean类继承这个工具类。
open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this);
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.removePropertyChangeListener(listener)
}
}
写一个Person类,定一个只读属性name和一个可写属性age,当age发生改变时,通知它的监听器
class Person(
val name: String,
age: Int
) : PropertyChangeAware() {
var age: Int = age
set(newValue) {
// field标识符允许访问属性背后支持字段
val oldValue = field
field = newValue
// 当属性变化时,通知监听器
changeSupport.firePropertyChange("age", oldValue, newValue)
}
}
val p = Person("kerwin", 30)
p.addPropertyChangeListener(PropertyChangeListener { event ->
println("Property ${event.propertyName} change from ${event.oldValue} to ${event.newValue}")
})
p.age = 31;
//输出结果>>> Property age change from 30 to 31
接下来通过辅助类实现属性变化的通知
class ObservableProperty(
val propertyName: String,
var propertyValue: Int,
val changeSupport: PropertyChangeSupport
) {
fun getValue() = propertyValue
fun setValue(newValue: Int) {
val oldValue = propertyValue
propertyValue = newValue
changeSupport.firePropertyChange(propertyName, oldValue, newValue)
}
}
class Person(
val name: String,
age: Int
) : PropertyChangeAware() {
val _age = ObservableProperty("age", age, changeSupport)
var age: Int
get() = _age.getValue()
set(newValue) = _age.setValue(newValue)
}
这样我们还是需要为每个属性创建ObservableProperty实例,并把setter和getter委托给它。kotlin中的委托功能不用这样写,但是需要更改下ObservableProperty方法的签名,匹配kotlin约定所需的方法
class ObservableProperty(
var propertyValue: Int,
val changeSupport: PropertyChangeSupport
) {
operator fun getValue(p: Person, prop: KProperty<*>) = propertyValue
operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
val oldValue = propertyValue
propertyValue = newValue
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
}
ObservableProperty这个类做了更改的地方:
- getValue和setValue函数都被标记了operator
- 这些函数增加了两个参数:一个用于接收属性的实例,用来设置或读取属性;另一个用于表示属性本身,这个属性类型为KProperty
- 把propertyName属性从主构造中移除
然后使用委托属性来绑定更该通知:
class Person(
val name: String,
age: Int
) : PropertyChangeAware() {
var age: Int by ObservableProperty(age, changeSupport)
}
通过by关键字,kotlin编译器会自动执行之前手动编写的代码。右边的对象被称为委托。kotlin会自动将委托存储在隐藏的属性中,并在访问或修改属性时调用委托的getValue和setValue。
你不用手动去实现可观察的属性逻辑。kotlin标准库中已经包含类似ObservableProperty的类。标准库与PropertyChangeSupport类没有耦合。
使用Delegates.observable来实现属性修改的通知:
class Person(
val name: String,
age: Int
) : PropertyChangeAware() {
private val observer = { property: KProperty<*>, oldValue: Int, newValue: Int ->
changeSupport.firePropertyChange(property.name, oldValue, newValue)
}
var age: Int by Delegates.observable(age, observer)
}
by右边的表达式不一定是新创建的实例。也可以是函数调用,另一个属性或者任何其它表达式。只要这个表达式的值,是能够被编译器用正确的参数类型来调用getValue和setValue的对象。
4. 委托属性的变化规则
接下来总结一下委托属性是怎么工作的?
假设有一个委托属性的类:
class C {
val prop: Type by MyDelegate()
}
MyDelegate实例会被保存到一个隐藏的属性中,它被称为<delegate>。编译器也将用一个KProperty类型的对象来代表这个属性,它被称为<property>
class C {
private val <delegate> = MyDelegate()
val prop: Type
get() = <delegate>.getValue(this, <property>)
set(value: Type) = <delegate>.setValue(this, <property>, value)
}
5. 在map中保存属性值
委托属性另一种常见用法,是用在有动态定义的属性集的对象中,这种对象有时被称为自订对象。
举一个例子:定义一个属性,把值存到map中
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, arrtValue: String) {
_attributes[attrName] = arrtValue
}
val name: String
get() = _attributes["name"]!! // 从map中手动检索属性
}
那么把它修改为委托属性非常简单,可以直接将map放在by关键字后面
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, arrtValue: String) {
_attributes[attrName] = arrtValue
}
// 将map作为委托属性
val name: String by _attributes
}
因为标准库已经在标准Map和MutableMap接口上定义了getValue和setValue扩展函数。
如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。