¶主要内容
- 操作符重载
- 指名(special-named)函数:convension的一种实现
- 属性委派
类似于Java语言有好几样特性一样,譬如对象实现了java.lang.Iterable接口的可以使用for循环,实现了java.lang.AutoCloseable接口的可以使用try-with-resources语句(Java 8 之后)。Kotlin提供了一种 convertions 技术,实现对操作符的重载。
¶Overloading arithmetic operators
Kotlin中最直接的公约是算术运算符。在Java中,算术运算符仅可以用于原生类型,额外地+号可以用于连结String类型。但对于其它类如BigInteger,使用+比起使用add方法会更加直观。
¶Overloading binary arithmetic operations
Kotlin的公约提供了操作符重载,
1 | data class Point(val x: Int, val y: Int) { |
这里用到operator关键字来声明plus函数。所有需要重载运算符都需要使用该关键字。表示显式地实现相应的约定。

另外一种写法是结合扩展函数定义,因为很多情况下你并不希望或不能修改原有的类。
1 | operator fun Point.plus(other: Point): Point { |
下标罗列的约定的所有算术符号对应的函数名,
| Expression | Function name |
|---|---|
a * b |
times |
a / b |
div |
a % b |
mod |
a + b |
plus |
a - b |
minus |
运算符的定义并没有严格要求两个对象的类型必须一致,
1 | operator fun Point.times(scale: Double): Point { |
但是Kotlin的operator不支持交换律 commutativity 。所以你需要重新再定义以满足交换律。operator fun Double.times(p: Point): Point。
返回类型也可以自定义,
1 | operator fun Char.times(count: Int): String { |
** No special operators for bitwise operations**
Kotlin并没有定义位运算符。相反,位运算被定义为常规的中缀调用语法(infix call syntax)。如,
shl,左移shr,右移ushr,无符号右移and,按位与or,按位或xor,按位异或inv,按位反譬如,
1
2
3
4
5
6 >>> println(0x0F and 0xF0)
0
>>> println(0x0F or 0xF0)
255
>>> println(0x1 shl 4)
16
¶Overloading compound assignment operators
复合赋值操作符( compound assignment operator ),跟算术操作符一样,
1 | >>> var point = Point(1, 2) |
对于可变集合来说,+= 操作相当于添加新的元素,
1 | >>> val numbers = ArrayList<int>() |
对于可变集合,Kotlin标准库中定义了plusAssign函数公约,
1 | operator fun <T> MutableCollection<T>.plusAssign(element: T) { |
代码中如果用到+=,理论上会调用到plus和plusAssign函数,

如果定义的类是不可变的,相应定义返回新值的函数plus;如果是可变的,则定义返回可变实例的plusAssign函数。但请尽量不要同时都定义这两个函数。
而操作符+和-的集合运算总是返回新的集合,其中,对于可变集合,直接修改其值;对于不可变集合,返回新的集合copy实例。
1 | >>> val list = arrayListOf(1, 2) |
¶Overloading unary operators
Kotlin中支持一元操作( unary )。
1 | operator fun Point.unaryMinus(): Point { |
一元操作函数重载不需要任何参数,

列表如下,
| Expression | Function name |
|---|---|
+a |
unaryPlus |
-a |
unaryMinus |
!a |
not |
++a a++ |
inc |
--a a-- |
dec |
对于自增inc或自减dec函数来说,编译器会自动支持前置或后置的语义识别,
1 | operator fun BigDecimal.inc() = this + BigDecimal.ONE |
¶Overloading comparison operators
Kotlin约定函数中也相应定义了比较操作符(==、!=、>、<等)。
¶Equality operators: “equals”
== 操作在Kotlin中真正被调用为equals方法。!=实际上也是被翻译为equlas的调用,只不过为相反方式。不同的是==和!=可以用于对null的比较,因为它底层可以检测null值。

¶Ordering operators: compareTo
compareTo方法被定义在Comparable接口中。但是Java中只有原生类型采用使用比较操作符>、<;其它类型只能显式地使用element1.compareTo(element2)来比较两个对象。
Kotlin支持同样的Comparable接口。但是compareTo方法定义的接口在Kotlin中无法进行重载,因为它底层被翻译为compareTo的调用,

和equlas一样,你需要继承Comparable接口对其方法进行重载,
1 | class Person(val firstName: String, val lastName: String): Comparable<Person> { |
compareValuesBy为Kotlin的内联函数,该函数接收一系列回调并比较返回值。
¶Conventions used for collections and ranges
¶Accessing elements by index: “get” and “set”
Kotlin约定支持 index operator 下标操作符,语法和Java的数组访问类似,
1 | val value = map[key] |
你也可以直接对其进行赋值操作,
1 | mutableMap[key] = newValue |
Kotlin中的下标操作符,对于读取调用了get方法,对于写操作调用了set。该方法在Map和MutableMap接口早已定义。你可以在自定义类中实现,
1 | operator fun Point.get(index: Int): Int { // Defines an operator function named "get" |
当调用p[1]时,将被翻译为get方法的调用。
需要注意的是,get方法的参数可以任意,例如你可以实现一个二维数组的下标读operator fun get(rowIndex: Int, colIndex: Int),然后通过matrix[row, col]。

类似地,下标的写操作要求对象可变的,因此你需要声明可变的对象属性,否则更改是无意义的,
1 | data class MutablePoint(var x: Int, var y: Int) |

¶The “in” convention
Kotlin集合中另一个支持的操作符为in,用于检测对象是否被包含在集合中。对应被调用的函数是contains。
1 | data class Rectangle(val upperLeft: Point, val lowerRight: Point) |

¶The rangeTo convention
..操作符对应于rangeTo函数的调用,用于常见Range对象的实例,

你可以为自定义类扩展该公约。但如果你的函数继承了Comparable,则不需要了:因为标准库为任意的Comparable实例定义了Range的方法。标准库中定义的rangeTo函数带有泛型参数,
1 | operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T> |
我们以LocalDate类为例,
1 | >>> val now = LocalDate.now() |
表达式now..now.plusDays(10)被编译器翻译为now.rangeTo(now.plusDays(10))。rangeTo函数并不是LocalDate的成员,但是Comparable的扩展函数。
在算术操作符中,rangeTo操作符的优先级最低,所以最好用括号括起来以避免歧义:
1 | >>> val n = 9 |
注意表达式0..n.forEach {}不会被编译成功,因为你必须用括号括起来才能作为表达式:
1 | >>> (0..n).forEach { print(it) } // Put a range in parentheses to call a method on it. |
¶The “iterator” convention for the “for” loop
迭代表达式for (x in list) { ... }编译时被翻译为list.iterator(),和Java一样,不停地重复调用hasNext和next方法。
在Kotlin中它被作为convention约定,意味着iterator方法可以扩展定义。其中,标准库中就有对字符串的iterator的扩展定义,
1 | operator fun CharSequence.iterator(): CharIterator |
你可以在自己的类定义iterator方法,例如,
1 | operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> = object : Iterator<LocalDate> { |
¶Destructuring declarations and component functions
destructuring declaractions ,Kotlin的声明解构作为feature实现。
1 | >>> val p = Point(10, 20) |
声明解构在Kotlin中也是作为convension约定,它的底层原理如下,

对于data class而言,编译器会为每个属性在primary constructor声明的属性创建相应的componentN函数。
1 | class Point(val x: Int, val y: Int) { |
声明解构的一个好处在于可以从一个函数中返回多个值。
1 | data class NameComponents(val name: String, val extension: String) // Declares a data class to hold the values |
¶Destructuring declarations and loops
声明解构常常用于map的迭代,
1 | fun printEntries(map: Map<String, String>) { |
¶Resuing property accessor logic: delegated properties
delegated properties仅属于Kotlin中的独特的强大的特性。该特性让你以最简单的方式实现属性,让其原本以更复杂的方式存储,但不会重复实现逻辑。例如,属性可以存储在数据库表、浏览器session、map中等。
该特性的基础是委派( delegation ):一种设计模式。
¶Delegated properties: the basics
常规的委派属性语法如下:
1 | class Foo { |
语法格式为 val /var <property name>: <Type> by <expression>。by之后的表达式是一个委派,因为属性对应的get()和set()会通过约定委派给它的getValue()和setValue()方法。属性委派不需要实现接口,但需要提供getValue()函数(和setValue() 如果是var可变属性)。
例如,
1 | class Foo { |
按照约定,Delegate类必须要有getValue和setValue方法(setValue仅为可变属性时被要求)。通常它们可以是成员或扩展。为了简化解析,我们省略它们的参数,Delegate类看起来会像,
1 | class Delegate { |
foo.p被作为常规属性使用,但底层调用了帮助类Delegate的方法调用。Kotlin标准库针对不同的委派模式提供了工厂方法调用。官方之为Standard delegates,包含两部分,lazy properties和observable properties。
¶Using delegated properties: lazy initialization and “by lazy()”
Lazy initialization 仅在对象第一次访问时才被初始化。
例如,假设有一个类Person允许你访问他的邮箱列表。其中邮箱被存储在数据库中需要花费一段时间才能访问。你希望数据库的访问操作仅一次也第一次属性访问时才触发。
1 | class Email { /*...*/ } |
下面通过额外属性_emails存储null方式实现lazy加载的目的,
1 | class Person(val name: String) { |
但这种写法有点啰嗦,如果有好几个lazy properties需要实现会显得繁琐。另外,上述代码也不是线程安全的。
Kotlin标准库提供了by lazy的函数调用,
1 | class Person(val name: String) { |
lazy函数返回包含getValue方法签名的一个对象,因此你可以结合by关键一起创建委派属性。这里的lazy的入参是一个lambda用于初始化。lazy函数默认是线程安全的。
¶Implementing delegated properties
下面以示例方式,阐述委派属性是如何实现的。我们希望实现最常见的事件通知类,
1 | open class PropertyChangeAware { |
编写Person类,希望属性在发生变更时触发listener,
1 | class Person(val name: String, age: Int, salary: Int): PropertyChangeAware() { |
这里的setter部分代码有许多重复的地方。我们通过委派方式将其分离出单独一个类,
1 | class ObservableProperty(val propName: String, var propValue: Int, val changeSupport: PropertyChangeSupport) { |
现在已经接近理解kotlin的委派属性是如何工作的了。但仍然有少许的样板代码,kotlin的委派属性的特性让你可以移除掉这部分样板代码(boilerplate)。在此之前,你需要按照约定,更改ObservableProperty相关方法的签名。
1 | class ObservableProperty(var porpValue: Int, val changeSupport: PropertyChangeSupport) { |
对比上一个版本,有如下改变,
getValue和setValue函数标记有operator,按照约定要求。- 这些函数有两个参数:一个是该属性的实例对象,另一个是属性自身。该属性用
KProperty的一个对象表示。你可以通过KProperty.name访问属性名。 name属性从第一构造函数(primary constructor)移除掉了,因为你可以从KProperty直接访问。
最终的委派属性的使用将变得非常简短,
1 | class Person(val name: String, age: Int, salary: Int): PropertyChangeAware() { |
通过by关键字,kotlin编译器自动地做了前面版本本应手动要做的部分。对比前面的Person类:编译器生成的代码非常相似。by右边的对象被称为 delegate 。kotlin将委派存储在一个隐藏属性,通过委派上的getValue和setValue的调用来修改或访问原来的属性。
如其手写这个ObservableProperty类,不妨使用Kotlin的标准库,
1 | class Person (val name: String, age: Int, salary: Int): PropertyChangeAware() { |
by右边的表达式不需要创建新的实例。可以是一个函数调用。只要该表达式的值对象是能被编译器调用getValue和setValue即可。
¶Delegated-property translation rules
让我们总结下委派属性是如何工作的。假设你的类包含如下委派属性:
1 | class C { |
MyDelegate()的实例将被存储在一个隐藏属性,我们将其作为 <delegate>引用。编译器将使用一个KProperty类型的对象来表示该属性。我们将其作为 <property>引用。
编译器会生成如下代码,
1 | class C { |
因此,每个属性访问器(property accessor)内部,编译器生成相应的getValue和setValue方法进行调用,结构如下,

¶Storing property values in a map
委派属性带来的另外一个常见模式是,对象拥有了动态定义的属性。这类对象有时被称为 expando objects 。例如,假设有一个联系人管理系统存储了联系人的任意信息。系统中的每个人仅包含一少部分属性,也包含有一少部分额外的属性(attribute)(例如,孩提期生日)。
实现该系统的一种方式是将联系人的所有attribute都存储在一个map,并提供访问该信息的对应属性(property)。
1 | class Person { |
简化代码的实现,使用by关键字委派属性,
1 | class Person { |
这里是可以工作的,因为标准库为Map和MutableMap接口提供了getValue和setValue的扩展函数。
¶Delegated properties in frameworks
对于一个object内部的property修改也极其简单。假设数据库表Users包含两列:字符串类型name、整型age。Kotlin
的定义如下,
1 | import org.jetbrains.exposed.dao.IntEntity |
框架exposed对于实体类属性(attribute)实现了委派属性,使用了列对象(Users.name, Users.age)进行委派:
1 | class User(id: EntityID): IntEntity(id) { |
而对于现实指定的数据库列对象,
1 | object Users : IntIdTable() { |
框架底层定义了convention进行委派,
1 | operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T { |
你可以使用Column属性(Users.name)作为被委派的属性(name)。
¶Summary
- Kotlin的约定(convention)定义了一些标准函数,可以让你对其进行重载以实现自定义类的operations。
- comparison operator对应会调用到
equals和compareTo方法。 - 通过定义
get、set和contains函数,你可以在自定义的类进行类似于集合的[]和in的操作。 - Kotlin的约定(convention)也支持集合Range的迭代语法。
- 声明解构(destructuring declarations)可以让你初始化多个值,它的底层也是一种约定,对应调用了代码编译器生成的
componentN函数。 - 委派属性(delegated property)允许属性部分逻辑的重用,譬如存储、初始化、访问、修改等。
lazy标准库函数提供了属性的lazily initialized。Delegates.observalbe函数定义在标准库中,实现了观察者模式。