文章目录
1 重载算术运算符
1.1 重载二元算术运算
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point { // plus方法是约定的运算符"+"
return Point(x + other.x, y + other.y)
}
}
>>val p1 = Point(10, 20)
>>val p2 = Point(30, 40)
>>println(p1 + p2)
输出:
Point(x=40, y=60)
使用 operator
关键字来声明 plus
函数。用于重载运算符的所有函数都需要用该关键字标记,用来表示你打算把这个函数作为相应的约定的实现,并且不是碰巧地定义一个同名函数。
在使用了 operator
修饰符声明了 plus
函数之后,你就可以直接使用 +
号来求和了。
// 把运算符定义为扩展函数
operator fun Point.plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
可重载的二元算符运算符如下表格:
表达式 | 函数名 |
---|---|
a * b | times |
a / b | div |
a % b | rem(旧版本为mod,已废弃) |
a + b | plus |
a - b | minus |
自定义类型的运算符,基本上和与标准数字类型的运算符有着相同的优先级。
当你在定义一个运算符的时候,不要求两个运算数是相同的类型。
operator fun Point.times(scale: Float): Point {
return Point((x * scale).toInt(), (y * scale).toInt)
}
>>val p = Point(10, 20)
>>println(p * 1.5)
输出:
Point(x=15, y=30)
注意,Kotlin运算符不会自动支持交换性(交换运算符的左右两边)。如果希望用户能够使用 1.5 * p
以外,还能使用 p * 1.5
,你需要为它定义一个单独的运算符:operator fun Float.times(p: Point): Point
:
operator fun Float.times(p: Point): Point {
return Point((p.x * this).toInt(), (p.y * this).toInt())
}
>>val p = Point(10, 20)
>>println(1.5f * p)
输出:
Point(x=15, y=30)
运算符函数的返回类型也可以不同于任一运算数类型。
operator fun Char.times(count: Int): String {
return toString().repeat(count)
}
>>println('a' * 3)
输出:
aaa
注意,和普通函数一样,可以重载 operator
函数:可以定义多个同名的,但参数类型不同的方法。
1.2 重载复合赋值运算符
通常情况下,当你在定义像 plus
这样的运算符函数时,Kotlin不止支持 +
号运算,也支持 +=
。像 +=
、-=
等这些运算符被称为复合赋值运算符。
data class Point(val x: Int, val y: Int) {
operator fun plus(p: Point): Point {
return Point(x + p.x, y + p.y)
}
}
>>var point = Point(1, 2)
>>point += Point(3, 4)
>>println(point)
输出:
Point(x=4, y=6)
在一些情况下,定义 +=
运算可以修改使用它的变量所引用的对象,但不会重新分配引用。
>>val numbers = ArrayList<Int>()
>>numbers += 42
>>println(numbers[0])
输出:
42
如果你定义了一个返回值为 Unit
,名为 plusAssign
的函数,Kotlin将会在用到 +=
运算符的地方调用它。其他二元算术运算符也有命名相似的对应函数。
operator fun <T> MutableCollection<T>.plusAssign(element: T) {
this.add(element)
}
当你在代码中用到 +=
的时候,理论上 plus
和 plusAssign
都可能被调用。如果在这种情况下,两个函数都有定义且适用,编译器会报错。
一般来说,最好在设计新类时保持(可变性)一致:尽量不要同时给一个类添加 plus
和 plusAssign
运算。像前面的一个示例中的Point,这个类是不可变的,那么就应该只提供返回一个新值(如 plus
)的运算。如果一个类是可变的,比如构建器,那么只需要提供 plusAssign
和类似的运算就够了。
// 运算符 += 可以被转换为plus或者plusAssign函数的调用
a += b -> a = a.plus(b)
a += b -> a.plusAssign(b)
Kotlin标准库支持集合的这两种方法。+
和 -
运算符总是返回一个新的集合。+=
和 -=
运算符用于可变集合时,始终就地修改它们;而它们用于只读集合时,会返回一个修改过的副本(这意味着只有当引用只读集合的变量被声明为 var
的时候,才能使用 +=
和 -=
)。
>>val list = arrayListOf(1, 2)
>>list += 3
>>val newList = list + listOf(4, 5)
>>println(list)
>>println(newList)
输出:
[1, 2, 3]
[1, 2, 3, 4, 5]
1.3 重载一元运算符
可重载的一元算法的运算符:
表达式 | 函数名 |
---|---|
+a | unaryPlus |
-a | unaryMinus |
!a | not |
++a, a++ | inc |
–a, a– | dec |
operator fun Point.unaryMinus(): Point {
return Point(-x, -y)
}
>>val p = Point(10, 20)
>>println(-p)
输出:
Point(x=-10, y=-20)
operator fun BigDecimal.inc() = this + BigDecimal.ONE
>>var bd = BigDecimal.ZERO
>>println(bd++)
>>println(++bd)
输出:
0
2
2 重载比较运算符
2.1 等号运算符:“equals”
使用 ==
运算符,它将被转换成 equals
方法的调用。
使用 !=
运算符也会被转换成 equals
函数的调用,明显的差异在于,它们的结果是相反的。注意,和所有其他运算符不同的是,==
和 !=
可以用于可空运算数,因为这些运算符事实上会检查运算数是否为null。
// 等式校验==被转换成equals函数的调用,以及null的校验
a == b -> a?.equals(b) ?: (b == null)
如果类被标记为数据类(即标记了 data
),equals
的实现将会由编译器自动生成。
// 在不是data class数据类情况下,手动实现equals函数
class Point(val x: Int, val y: Int) {
override fun equals(obj: Any?): Boolean {
if (obj === this) return true
if (obj !is Point) return false
return obj.x == x && obj.y == y
}
}
>>println(Point(10, 20) == Point(10, 20))
>>println(Point(10, 20) != Point(5, 5))
>>println(null == Point(1, 2))
输出:
true
true
false
使用恒等运算符 ===
来检查参数与调用 equals
的对象是否相同。恒等运算符与java中的 ==
运算符是完全相同的:检查两个参数是否是同一个对象的引用(如果是基本数据类型,检查它们是否是相同的值)。在实现了 equals
之后,通常会使用这个运算符来优化调用代码。注意,===
运算符不能被重载。
equals
函数之所以被标记为 override
,那是因为与其他约定不同的是,这个方法的实现是在 Any
类中定义的(Kotlin中的所有对象都支持等式比较)。这也解释了为什么不需要将它标记为 operator:Any
中的基本方法就已经标记了,而且函数的 operator
修饰符也适用于所有实现或重写它的方法。还要注意,equals
不能实现为扩展函数,因为继承自 Any
类的实现始终优先于扩展函数。
2.2 排序运算符:compareTo
Kotlin支持相同的 Comparable
接口。但是接口中定义的 compareTo
方法可以按约定调用,比较运算符(>
,<
,<=
,>=
)的使用将被转换成 compareTo
。
// 两个对象的比较被转换成compareTo的函数调用,然后结果与零比较
a >= b -> a.compareTo(b) >= 0
// 手动实现compareTo
class Person(val firstName: String, val lastName: String): Comparable<Person> {
override fun compareTo(other: Person): Int {
// compareValuesBy()接收用来计算比较值的一系列回调,按顺序依次调用回调方法,两两一组分别做比较,并返回结果
// 如果值不同,则返回比较结果
// 如果它们相同,则继续调用下一个
// 如果没有更多回调来调用,则返回0
return compareValuesBy(this, other, Person::lastName. Person::firstName)
}
}
>>val p1 = Person("Alice", "Smith")
>>val p2 = Person("Bob", "Johnson")
>>println(p1 < p2)
输出:
false
在java中实现了 Comparable
接口的类,都可以在Kotlin中使用简洁的运算符语法,不用再增加扩展函数:
>>println("abc" < "bac")
输出:
true
3 集合与区间的约定
3.1 通过下标来访问元素:“get"和"set”
// 相当于java中map.get(key)
val value = map[key]
// 相当于java中map.put(key, value)
mutableMap[key] = newValue
在Kotlin中,下标运算符是一个约定。使用下标运算符读取元素会被转换为 get
运算符方法的调用,并且写入元素将调用 set
。
// 方括号的访问会被转换为get函数的调用
x[a, b] -> x.get(a, b)
data class Point(val x: Int, val y: Int)
operator fun Point.get(index: Int): Int {
return when(index) {
0 -> x
1 -> y
else -> throw IndexOutOfBoundsException("Invalid corrdinate $index")
}
}
>>val p = Point(10, 20)
>>println(p[1])
输出:
20
注意,get
的参数可以是任何类型,而不只是 Int
。还可以定义具有多个参数的 get
方法。如果需要使用不同的键类型访问集合,也可以使用不同的参数类型定义多个重载的 get
方法。
// 方括号赋值操作将会转换为set函数的调用
x[a, b] = c -> x.set(a, b, c)
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 corrdinate $index")
}
}
>>val p = MutablePoint(10, 20)
>>p[1] = 42
>>println(p)
输出:
MutablePoint(x=10, y=42)
3.2 "in"的约定
集合支持的另一个运算符是 in
运算符,用于检查某个对象是否属于集合。相应的函数叫做 contains
。
// in操作将会转换为contains函数的调用
a in c -> c.contains(a)
data class Rectangle(val upperLeft: Point, val lowerRight: Point)
operator fun Rectangle.contains(p: Point): Boolean {
// until函数构建一个开区间,开区间不包括最后一个点的区间,如[a, b)
return p.x in upperLeft.x until lowerRight.x &&
p.y in upperRight.y until lowerRight.y
}
>>val rect = Rectangle(Point(10, 20), Point(50, 50))
>>println(Point(20, 30) in rect)
>>println(Point(5, 5) in rect)
输出:
true
false
3.3 rangeTo的约定
// ..运算符将被转换为rangeTop函数的调用
start..end -> start.rangeTo(end)
如果该类实现了 Comparable
接口,那么不需要了:你可以通过Kotlin标准库创建一个任意可比较元素的区间,这个库定义了可以用于任何可比较元素的 rangeTo
函数:
>>val now = LocalDate.now()
>>val vacation = now..now.plusDays(10) // now.rangeTo(now.plusDay(10))
>>println(now.plusWeeks(1) in vacation)
输出:
true
rangeTo
运算符的优先级低于算术运算符,但是最好把参数括起来以免混淆:
>>val n = 9
>>println(0..(n + 1))
输出:
0..10
表达式 0..n.forEach{}
不会被编译,因为必须把区间表达式括起来才能调用它的方法:
>>(0..n).forEach { print(it) }
输出:
0123456789
3.4 在"for"循环中使用"iterator"的约定
iterator
方法可以被定义为扩展函数。这解释了为什么可以遍历一个常规的java字符串:标准库已经为CharSequence定义了一个扩展函数iterator,而它是String的父类:
>>for (c in "abc") {}
// 实现自定义迭代器
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
object: Iterator<LocalDate> {
var current = start
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
4 解构声明和组件函数
解构声明,这个功能允许你展开单个复合值,并使用它来初始化多个单独的变量。
data class Point(val x: Int, val y: Int)
>>val p = Point(10, 20)
>>val (x, y) = p
>>println(x)
>>println(y)
输出:
10
20
一个解构声明看起来像一个普通的变量声明,但它在括号中有多个变量。
要在解构声明中初始化每个变量,将调用名为 componentN
的函数,其中 N
是声明中变量的位置。
// 解构声明被转换为componentN函数的调用
val (a, b) = p -> val a = p.component1()
val (a, b) = p -> val b = p.component2()
对于数据类,编译器为每个在主构造方法中声明的属性生成一个 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) {
companion object {
fun splitFilename(fullName: String): NameComponents {
val result = fullName.split(".", limit = 2)
return NameComponents(result[0], result[1])
}
}
}
>>val (name, ext) = NameComponents.splitFilename("example.kt")
>>println(name)
>>println(ext)
输出:
example
kt
不可能无限数量的 componentN
函数,标准库只允许使用此语法来访问一个对象的前五个元素。
4.1 解构声明和循环
// 用解构声明来遍历map
fun printEntries(map: Map<Stirng, Stirng>) {
for ((key, value) in map) {
println("$key -> $value")
}
}
>>val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
>>printEntries(map)
输出:
Orcale -> Java
JetBrains -> Kotlin
Kotlin标准库给map增加了一个扩展的iterator函数,用来返回map条目的迭代器。因此,与java不同的是,可以直接迭代map。它还包含Map.Entry上的扩展函数component1和component2,分别返回它的键和值。
5 重用属性访问的逻辑:委托属性
5.1 委托属性的基本操作
// 伪代码
class Foo {
private val delegate = Delegate()
var p: Type
set(value: Type) = delegate.setValue(..., value)
get() = delegate.getValue(...)
}
按照约定,Delegate
类必须具有 getValue
和 setValue()
方法。
class Delegate {
// 包含getter的实现逻辑
operator fun getValue(...) { ... }
// 包含setter的实现逻辑
operator fun setValue(..., value: Type) { ... }
}
class Foo {
// 关键字by把属性关联上委托对象
var p: Type by Delegate()
}
>>val foo = Foo()
>>val oldValue = foo.p // 访问Foo对象的属性p,实际调用的是Delegate.getValue()
>>foo.p = newValue // 实际调用的是Delegate.setValue()
5.2 使用委托属性:惰性初始化和"by lazy()"
惰性初始化
是一种常见的模式,直到在第一次访问该属性的时候,才根据需要创建对象的一部分。
class Email { /*...*/ }
fun loadEmails(person: Person): List<Email> {
println("load emails for ${person.name}")
return listOf(/*...*/)
}
class Person(val name: String) {
private var _emails: List<Email>? = null
val emails: List<Emial>
get() {
if (_email == null) {
_emails = loadEmails(this)
}
return _emails!!
}
}
>>val p = Person("Alice")
>>p.emails // 第一次访问会加载邮件loadEmails()
>>p.emails
这里使用了所谓的 支持属性技术
。你有一个属性_emails用来存储这个值,而另一个emails,用来提供对属性的读取访问。你需要使用两个属性,因为属性具有不同的类型:_emails可以为空,而emails为非空。
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
lazy
函数返回一个对象,该对象具有一个名为 getValue
且签名正确的方法,因此可以把它与 by
关键字一起使用来创建一个委托属性。lazy
的参数是一个lambda,可以调用它来初始化这个值。默认情况下,lazy
函数是线程安全的,如果需要,可以设置其他选项来告诉它要使用哪个锁,或者完全避开同步,如果该类永远不会在多线程环境中使用。
5.3 委托属性的变换规则
总结委托属性的工作原理:
class C {
var prop: Type by MyDelegate()
}
val c = C()
MyDelegate
实例会被保存到一个隐藏的属性中,它被称为 <delegate>
。编译器也将用一个 KProperty
类型的对象来代表这个属性,它被称为 <property>
。
class C {
private val <delegate> = MyDelegate()
var prop: Type
get() = <delegate>.getValue(this, <Property>)
set(value: Type) = <delegate>.setValue(this, <property>, value)
}
// 当访问属性时,会调用<delegate>的getValue和setValue
val x = c.prop -> val x = <delegate>.getValue(c, <property>)
c.prop = x -> <delegate>.setValue(c, <property>, x)
5.5 在map中保存属性值
委托属性发挥作用的另一种常见用法,是用在有动态定义的属性集的对象中。这样的对象有时被称为自订(expando)对象。
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String
get() = _attributes["name"]!!
}
>>val p = Person()
>>val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
>>for ((attrName, value) in data) p.setAttribute(attrName, value)
>>println(p.name)
输出:
Dmitry
可以直接将map放在关键字 by
后面:
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
// 因为标准库已经在标准Map和MutableMap接口上定义了getValue和setValue扩展函数,所以这里可以直接这样用
val name: String by _attributes
}
6 中缀表达式
运算符的重载有一个特点,就是它必须是 kotlin 语法中已经定义好的运算符,不能凭空重载一个运算符,或许在后续的 kotlin 迭代更新会有更多的运算符可以被重载,所以这就有一个限制:运算符是有上限的。为了解决该问题,所以就有了中缀表达式扩展运算符。
中缀表达式通过关键字 infix
声明函数:
sealed class CompareResult {
object LESS : CompareResult() {
override fun toString(): String {
return "小于"
}
}
object MORE : CompareResult() {
override fun toString(): String {
return "大于"
}
}
object EQUAL : CompareResult() {
override fun toString(): String {
return "等于"
}
}
}
// infix 关键字声明一个中缀表达式
infix fun Int.vs(num: Int): CompareResult =
if (this - num < 0) {
CompareResult.LESS
} else if (this - num > 0) {
CompareResult.MORE
} else {
CompareResult.EQUAL
}
infix fun Double.vs(num: Int): CompareResult =
if (this - num < 0) {
CompareResult.LESS
} else if (this - num > 0) {
CompareResult.MORE
} else {
CompareResult.EQUAL
}
infix fun Long.vs(num: Int): CompareResult =
if (this - num < 0) {
CompareResult.LESS
} else if (this - num > 0) {
CompareResult.MORE
} else {
CompareResult.EQUAL
}
fun main(args: Array<String>) {
println(5 vs 6) // 输出 小于
// println(5.vs(6))
}
一个函数只有用于两个角色类似的对象时才将其声明为中缀函数,如果一个方法会改动其接收者,那么不要声明为中缀形式。例如 and、to、zip 可以声明为中缀形式,add 会改动接收者所以不建议声明为中缀形式。