kotlin运算符重载及约定
kotlin中功能与特定函数命名相关,而不是与特定类型绑定,这种技术称为约定。kotlin使用约定的原则,并不像Java那样需要依赖类型,因为它允许开发人员适应现有的java类,来满足kotlin语言的特性要求。由类实现的接口是接口集是固定的,而kotlin不能为了实现其它接口而修改现有类。同时,kotlin也可以通过扩展函数的机制来未现有的类增添新方法。可以把任意约定方法定义为扩展函数。
重载算术运算符
算术运算符最能直观的反映kotlin的约定。在kotlin中,可以使用+=等基本运算符对集合或者对象进行操作。
data class Point(val x:Int,val y:Int) {
//使用operator关键字声明plus函数
//用于重载运算符的所有函数都需要用它标记,表示约定而不是同名函数
operator fun plus(other:Point): Point {
//坐标分别相加返回新的Point
return Point(x+other.x,y+other.y)
}
}
val p1 = Point(10, 20)
val p2 = Point(20, 30)
//等同 p1.plus(p2)
println(p1 + p2) //Point(x=30, y=50)
也可以定义为扩展函数
//定义名为plus方法
operator fun Point.plus(other:Point): Point {
//坐标分别相加返回新的Point
return Point(x+other.x,y+other.y)
}
使用扩展函数来定义约定是常用的模式。
在kotlin中不能定义自己的运算符。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)//Point(x=15, y=30)
然而需要注意的是,kotlin运算符不自动支持交换性(交换运算符左右)。如果想使用p1.5和1.5p,需要定义单独的运算符operator fun Double.times(p:Point):Point。
运算符函数返回类型也可以不同于任一运算数类型。
operator fun Char.times(count:Int):String{
//repeat是扩展函数,重复搞事情
return toString().repeat(count)
}
println('A'*3) //AAA
函数名 | 位运算 |
---|---|
shl | 带符号左移 |
shr | 带符号右移 |
ush | 无符号右移 |
and | 按位与 |
or | 按位或 |
xor | 按位异或 |
inv | 按位取反 |
和普通函数一样,可以重载operator函数:定义多个同名函数,但参数类型不一样的函数。
kotlin没有用于位运算的特殊运算符,所以也不存在自定义去定义。但是它支持中缀调用。
kotlin提供位运算的完整函数表(笔者暂时不懂位运算,权作记录)。
函数名 | 位运算 |
---|---|
shl | 带符号左移 |
shr | 带符号右移 |
ush | 无符号右移 |
and | 按位与 |
or | 按位或 |
xor | 按位异或 |
inv | 按位取反 |
重载复合赋值运算符
通常情况下,定义运算符函数时,也支持复合赋值运算符(+=,-=)。
var p1=Point(10,20)
val p2=Point(20,30)
//仅对可变变量有效
p1+=p2
println(p1)//Point(x=30, y=50)
在一些情况下,+=运算符可以修改使用它的变量所引用的对象,但不会重新分配引用。比如将一个元素添加到可变集合。
val numbers=ArrayList<Int>()
numbers+=42
println(numbers[0])//42
如果定义返回值为Unit,名为plusAssign的函数会在用到+=运算符的地方调用它。其它二元算术运算符也有相对应的函数。
kotlin标准库可变集合定义了plusAssign函数。
//简化
numbers.plusAssign(42)
值得注意的是当在代码调用+=的时候,理论上plus和plusAssign都可能被调用。这种情况下编译器会抛异常。用val去替代var,那么plusAssign不再适用。但是总的来说尽量不要同时使用这两个函数。
kotlin+和-运算符总是会返回一个新的集合。+=,-=用于可变集合时候始终会在一个地方修改它们,它们用于只读集合时候,会返回一个修改过的副本(即引用集合必须声明为var的时侯),作为运算数,可以使用单个元素,也可以使用元素类型一致其它集合。
val list = arrayListOf(1, 2)
// list被修改
list += 3
//返回包含所有元素的新列表
val newList = list + listOf(4, 5)
println(list)//[1, 2, 3]
println(newList)//[1, 2, 3, 4, 5]
重载一元运算符
重载一元运算符实现方式相同
//一元运算符无参数
operator fun Point.unaryMinus():Point{
//坐标去反
return Point(-x,-y)
}
val p1 =Point(10,20)
println(-p1)//Point(x=-10, y=-20)
可重载的一元算法的运算符
表达式 | 函数名 |
---|---|
+a | unaryPlus |
-a | unaryMinus |
!a | not |
++a,a++ | inc |
--a,a-- | dec |
前缀运算符(先计算在取值)和后缀运算符(先取值在计算)也与基本数据类型运算符相同。
重载比较运算符
与算术运算符相同,在kotlin中可以对任何对象使用比较运算符(==,+=,>,<等)。
等号运算符
kotlin中使用==,!=运算符,都将被转换成equals方法调用(约定原则),同时都可以用于可空的运算数(即会检查运算数是否为null),不同的是结果相反。
p1==p2
//实际上是
p1.equals(p2)?:(p2==null)
数据类会自动实现equals等函数,非数据类手动实现
override fun equals(other: Any?): Boolean {
//===恒等运算符(无法重载),等同Java的==,通常equals之后使用来优化代码
if (other===this) return true //检查参数与thi是否同一个对象
if (other !is Point) return false //检查参数类型
return other.x==x&&other.y==y //智能转换为Point来访问
}
val p1 = Point(10, 20)
val p2 = Point(20, 30)
println(p1== Point(10,20)) //true
println(p2!= Point(5,5)) //true
println(null==p1) //false
equals函数之所以被标记为override是因为与约定不同的是,它是实现在Any类中定义的(kotlin中所有对象都支持等式比较),operator修饰符也适用所有实现或重写它的方法。equals不能实现为扩展函数,因为继承自Any类的实现始终优先于扩展函数。
排序运算符 :compaerTo
kotlin支持和Java相同的Comparable接口。但是接口中定义的compareTo方法可以按照约定调用,比较运算符(<,>,<=,>=)的使用将被转换为compareTo,它的返回类型必须为Int。
p1 < p2
//等价于
p1.compareTo(p2)<0
//换言之
a>=b
//等价于
a.compareTo(b)>=0
以之前Person为例子(先比较姓氏,如果姓氏相同,比较名字)
class Person(
val firstName:String,val lastName:String
) :Comparable<Person>{
override fun compareTo(other: Person): Int {
//按顺序调用给定方法并比较它们值
return compareValuesBy(this,other,Person::lastName,Person::firstName)
}
}
val p1 = Person("JoJo", "小白")
val p2 = Person("Bob", "小黑")
println(p1<p2) //true
compareValuesBy可以简洁的实现 compareTo方法。该函数用来计算比较值一系列回调,按顺序依次调用回调(两两比较,相同继续,不同返回值,没有回调返回0)。
实现Comparable接口的对象不仅在kotlin中用来比较,也可以被Java函数(如对集合进行排序)进行比较,与equals一样,operator修饰符在基类接口已经实现,无需重复。
//直接比较字符串
println("abc"<"cbb")//true
集合与区间的约定
处理集合最常见一些操作是通过下标来获取设置元素,以及检查元素是否属于当前集合。所有这些操作都支持运算符语法:要通过下标获取或者设置元素,可以使用语法ab。可以使用in运算符来检查元素是否在集合或区间内,也可以迭代集合
下标访问:“get”,"set"
如前所述,在kotlin中可以用类似Java中数组的方式来访问map中的元素。
val map = mutableMapOf("1" to "one","2" to "two")
//根据key取值
println(map["1"] ) //one
//改变map的值
map["1"]="一";
println(map["1"]) //一
下标运算符也是一个约定。读取元素它会转变成get,写入元素会转变成set。Map和MutableMap接口定义这些方法。我们也可以给自定义的类添加类似方法。
//定义名为"get"的运算符函数
operator fun Point.get(index:Int):Int{
//根据索引返回对应坐标
return when(index){
0->x
1->y
else ->
throw IndexOutOfBoundsException("Invalid coordinate $index")
}
val p1 =Point(10,20)
println(p1[0]) //10
println(p1[1]) //20
仅仅只需要定义一个名为get(参数类型可以是任何类型)的函数,并使用operator修饰符修饰即可。
x[a,b]
//等价于
x.get(a,b)
set函数同理,不过需要的是它是可变的。
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,20)
p[0] = 40;
p[1]=80
println(p) //MutablePoint(x=40, y=80)
同理,set的最后一个参数用来接收赋值语句中(等号)右边的值,其它参数则作为下标。
x[a,b] = c
//等价于
x.set(a,b,c)
"in"的约定
集合支持的另一个运算符是in运算符,用于检查某个对象是否属于集合。相应的函数叫contains。
举个例子,使用in运算符检查点是否属于一个矩形。
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 rectangle = Rectangle(Point(10, 20), Point(30, 60))
println(Point(20,40) in rectangle) // true
println(Point(5,10) in rectangle) //false
in右边的对象会调用contains,in左边的对象将会作为函数入参。
a in c
//等价于
c.contains(a)
这里值得一提的是,开区间(Until)是不包括最后一个元素的。闭区间(10..20)构建的区间则是包括所有。
rangeTo的约定
创建区间,使用..语法,实际上它是rangeTo函数的约定。
start..end
//等价于
start.rangeTo(end)
rangTo函数返回一个区间。可以为自己的类定义运算符,但是如果该类实现了Comparable接口,那么就不需要:可以通过kotlin标准库创建一个任意可以比较元素的区间。
val now = LocalDate.now()
//创建一个从今天开始到10天的区间
val vacation = now..now.plusDays(10)
//检测一个特定日期是否属于这个区间
println(now.plusWeeks(1) in vacation) //true
now..now.plusDays(10)会被编译成now.rangeTo(now.plusDays(10))。如前所述,rangTo并不是LocalDate的成员函数,而是Comparable的扩展函数。
rangeTo运算符优先级低于算术运算符,建议把参数括起来避免混淆。
val n = 9;
println(0..(n+1)) //0..10
还需要注意的是,0..n.foreach{}不会被编译,因为必须把区间表达式括起来才能调用。
val n = 9;
println((0..n).forEach{ print(it)})//0123456789
在“for”循环中使用"iterator"的约定
在kotlin中,for循环中也可以使用in运算符,和做区间检查一样。但是这种情况下它的含义不同:它被用来执行迭代。即for(x in list){...}被转换成list.iterator()调用,然后像在Java一样,在它上面重复hashNext和Next方法。
这在kotlin中也是一种约定(可以被定义为扩展函数)。这使得字符串也可以遍历。
//源码
public operator fun CharSequence.iterator(): CharIterator = object : CharIterator() {
private var index = 0
public override fun nextChar(): Char = get(index++)
public override fun hasNext(): Boolean = index < length
}
也可以自己定义iterator方法。举个例子,遍历日期。
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
//这个对象实现了遍历LocalDate>元素的iterator
object : Iterator<LocalDate> {
var current = start
override fun hasNext(): Boolean =
//使用了compareTo约定
current <= endInclusive
//修改前返回当前日期作为结果
override fun next(): LocalDate = current.apply {
//日期增加一天
current = plusDays(1) }
}
val newYear = LocalDate.ofYearDay(2018,4)
val daysoff = newYear.minusDays(1)..newYear
for (dayoff in daysoff){
println(dayoff)}
//2018-01-03
//2018-01-04
注意如何自定义区间类型上定义iterator方法:使用LocalDate作为参数。rangeTo库函数返回一个ClosedRange实例,并且ClosedRange的扩展iterator扩展允许在for循环中使用区间实例。
结构声明和组件函数
解构声明允许展开单个符合值,并使它来初始化多个单独变量。
val p =Point(10,20)
//声明变量x,y,然后用p组件来初始化
val (x,y) = p
println(x) //10
println(y) //20
一个解构声明看起来像普通变量声明,但是它有多个变量。实际上解构声明也用到了约定原理。要在结构声明中初始化每个变量,将调用componentN函数,其中N是声明中变量的位置。
val (x,y) = p
//等价
val x =p.component1()
val y = p.component2()
数据类会自动生成该函数,非数据类手动声明。
class Point(val x:Int,val y:Int) {
operator fun component1() = x
operator fun component2() = y
}
解构声明主要使用场景之一,是从一个函数返回多个值。如此一来,可以定义一个数据类保存返回所需值,并将它作为函数返回类型。调用函数后,可以使用解构声明方式,轻松展开它,使用其中值。
举个例子,将文件名分割成名字和扩展名。
//声明数据类持有值
data class NameComponent (val name:String,val extension:String){
}
fun splitFileName(fullName:String):NameComponent{
val result =fullName.split(".",limit = 2)
return NameComponent(result[0],result[1])
}
val file = "Main.kt"
val (name, ext) = splitFileName(file)
println("name:$name ext:$ext") //name:Main ext:kt
可简化
fun splitFileName(fullName:String):NameComponent{
val (name,ext) =fullName.split(".",limit = 2)
return NameComponent(name,ext)
}
componentN函数不可能无限声明,最多只能访问前五个元素。让一个函数能返回多个值有更简单办法,使用标准库中的Pair和Triple类。
解构声明和循环
解构声明不仅可以用作函数的顶层语句,还可以用在其它可以声明变量地方。如in循环。
//解构声明遍历map
fun printEntries(map:Map<String,String>){
//in循环中使用解构声明
for ((key,vaule) in map){
println("$key,$vaule")
}
val map = mapOf("Oracle" to "java", "jb" to "kotlin")
printEntries(map)
//Oracle,java
//jb,kotlin
上述代码用到kotlin两个约定(iterator和解构)。实际代码转换如下。
for (entry in map){
val key = entry.component1()
val value = entry.component2()
}
这也恰好说明了扩展函数对约定及其重要。
重用属性访问的逻辑:委托属性(重点)
委托属性是kotlin中最独特和最强大的功能之一。它可以轻松实现这样的属性,它们处理起来比把值存储在支持字段中更复杂,却不用在每个访问器中都重复这样的逻辑。
委托属性依赖于约定功能。委托是一种设计模式,操作对象不用自己执行,而是把工作委托给另一个辅助对象。
//委托属性基本语法
class Test {
var t by Delegate()
}
属性P将它的访问逻辑委托给另一个对象:这里是Delegate类的一个新实例。通过关键字by对其后的表达式求值来获取这个对象,by可以用于任何符合属性约定规则的对象。
//翻译后代码
class Delegate {
operator fun getValue(v1:Type,v2:KProperty<*>){
}
operator fun setValue(v1:Type,v2:KProperty<*>,v3:Type){
}
}
class Test {
private val delegate = Delegate()
var t:Type
get() = delegate.getValue(....)
set(value) {
....,value
}
}
按照约定,Delegate类必须具有getValue和setValue(仅适用于可变变量)方法,它既可以是成员函数,也可以是扩展函数。
惰性初始化和"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<Email>? = null
get() {
if (_emails==null){
//访问去加载
_emails= loadEmails(this)
}
//如果加载了就直接返回
return field
}
}
val p = Person("Alc")
p.emails //load emails for Alc
p.emails //只执行一次,所以无值
支持属性技术值得熟练掌握。_emails(可空)用来存储这个值,另一个emails(非空则用来提供对属性的读取访问。值得注意的是,它的线程并不是安全的。
标准库函数lazy委托则正好能完美解决问题。它反回一个对象具有一个名为getValue且签名正确的方法,因此可以与by(如前所说,委托必须要有getVaule())一起创建委托属性。lazy的参数是一个lambda,可以用它初始值,默认情况下,lazy函数是线程安全。
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
实现委托属性
直接举个例子吧,当对象属性更改时通知监听器。
//使用propertyChangeSupport
class PropertyChangeAware {
protected val changeSupport =PropertyChangeSupport(this)
fun addPropertyChangeListener(listener:PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener){
changeSupport.removePropertyChangeListener(listener)
}
}
再写个Person,定义一个只读属性(名字)和两个可写属性(年龄和工资),当年龄和工资变化,通知监听器。
class Person(val name: String,age:Int,salary:Int):PropertyChangeAware() {
var age:Int=age
set(newAge){
//field允许访问属性背后支持字段
val oldAge=field
field=newAge
//属性变化通知监听
changeSupport.firePropertyChange("old",oldAge,newAge)
}
var salary:Int =salary
set(newSalary){
val oldSalary = field
field=newSalary
changeSupport.firePropertyChange("salary",oldSalary,newSalary)
}
}
fun main(args: Array<String>) {
val p = Person("Jojo", 24, 2000)
p.addPropertyChangeListener(
PropertyChangeListener { evt ->
println("Property ${evt.propertyName} change" + "from ${evt.oldValue} to ${evt.newValue}")
}
)
//改变年龄
p.age = 25 //Property old changefrom 24 to 25
//改变工资
p.salary = 6000 //Property salary changefrom 2000 to 6000
}
setter里有很多重复代码,可抽取类优化。
class ObservsbleaProperty(val proName: String,
var propValue: Int,
val changeSupport: PropertyChangeSupport) {
fun getValue()=propValue
fun setValue(newValue:Int){
val oldValue=propValue
propValue=newValue
changeSupport.firePropertyChange(proName,oldValue,newValue)
}
}
改造Person
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
var _age = ObservsbleaProperty("age", age, changeSupport)
var age: Int
get() = _age.getValue()
set(newAge) {
_age.setValue(newAge)
}
var _salary=ObservsbleaProperty("salary",salary,changeSupport)
var salary: Int
get() = _salary.getValue()
set(value) {_salary.setValue(value)}
}
这仍然有很大的不足,比如需要更多的样板代码。kotlin的委托属性则可以避免这些样板代码。
进一步优化,改造ObservsbleaProperty匹配kotlin约定,从而作为属性委托。
class ObservsbleaProperty(val proName: String,
var propValue: Int,
val changeSupport: PropertyChangeSupport) {
operator fun getValue(p: Person, prop: KProperty<*>) = propValue
operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(proName, oldValue, newValue)
}
}
改造后增加两个参数,一个是接收实例,一个是设置或读取属性,另一个表示本身。这个类型为 KProperty,值得一提的是 KProperty是kotlin中的反射。
使用改造后的委托属性绑定更改。
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
var age:Int by ObservsbleaProperty(age,changeSupport)
var salary:Int by ObservsbleaProperty(salary,changeSupport)
}
Kotlin会自动将委托存储在隐藏属性中,并在访问或修改是调用委托的getValue,setValue。实际上Kotlin标准库已经包含了ObservableProperty的类,不过需要传递lambda。
//使用标准库的函数来实现属性修改通知
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
var age:Int by ObservsbleaProperty(age,changeSupport)
var salary:Int by ObservsbleaProperty(salary,changeSupport)
}
by右边的表达式不一定是新创建的实例,也可以是函数调用,另一个属性或任何其它表达式,只要它的值能被编译器用正确参数来调用getValue,setValue就行。
属性变换规则
委托属性的变换规则到这里相信已经清楚明了。即在每个属性访问器中,编译器都会生成对应的getVaule和setVaule。
//get
val x =c.prop
//等价于
val x =<delegate>.getValue(c,<property>)
//set
c.prop =x
//等价于
val x =<delegate>.setValue(c,<property>,x)
这个机制能适应很多场景。比如在map中保存属性值。
class Person {
private val map = hashMapOf<String,String>()
fun setMap(key:String,value:String){
map[key]=value
}
val keyName:String by map
}
fun main(args: Array<String>) {
val p = Person()
val map = mapOf("keyName" to "JoJo","jb" to "kotlin")
for ((key,value) in map){
p.setMap(key,value)
}
//如果没有会抛异常
println(p.keyName) //JoJo
}
因为标准库已经在Map和MutableMap接口上定义了getValue,setValue的扩展函数。所以这里可以直接使用。
总结,略。