Kotlin学习(十七): 运算符重载

日本编程丛书-图片源于网络

在前面写了关于集合和范围的内容,里面包括了一点运算符重载的内容,在这里我们来详细了解运算符重载的知识,内容参考《Kotlin实战》(Kotlin in Action)。

什么是运算符重载?

简单来说,就是Kotlin通过调用自己代码中定义特定的函数名的函数(成员函数或者扩展函数),并且用operator修饰符标记,来实现特定的语言结构,例如如果你在一个类上面定义了一个特定函数命名plus的函数,那么按照Kotlin的约定,可用在这个类的实例上使用+运算符,下面是代码。

用于重载运算符的所有函数都必须使用operator关键字标记。

// 一个简单的数据类
data class Foo(val x: Int, val y: Int) {
    operator fun plus(other: Foo) : Foo = Foo(x + other.x, y + other.y)
}

fun main(args: Array<String>) {
    // 使用的时候
    val f1 = Foo(10, 20)
    val f2 = Foo(30, 40)
    // 直接用+运算符代替plus函数,事实上会调用plus函数
    println(f1 + f2) // 打印内容为Foo(x=40, y=60)
}

那么Java如何调用运算符函数呢?

重载的运算符实际上是被定义成一个函数,Java调用Kotlin运算符就跟调用普通函数一样调用就行。

重载算术运算符

算术运算符包括二元运算符、复合赋值运算符、一元运算符,当Kotlin在给一个集合添加元素的时候,是调用add方法,用到重载的话,我们就可以直接用+=来进行这个操作,就会显得更加的优雅。。。


fun Any.println() = println(this)

fun main(args: Array<String>) {
    val list = arrayListOf(1, 2 ,3)
    list.println() // 打印[1, 2, 3]
    list.add(4)
    list.println() // 打印[1, 2, 3, 4]
    list += 5
    list.println() // 打印[1, 2, 3, 4, 5]
}

重载二元算术运算符

二元算术运算符就是常见的+-*/和取余%,优先级与数学的是一样的,*/%要高于+-的优先级。

下面我们列举对应的函数名:

表达式 函数名
a * b times
a / b div
a % b rem,mod(弃用)
a + b plus
a - b minus

下面我们来写个类,里面包含这几种函数,同时还有扩展函数的定义。

fun Any.println() = println(this)

// Extension
operator fun Foo.minus(other: Foo): Foo = Foo(x - other.x, y - other.y)
operator fun Foo.div(other: Foo): Foo = Foo(x / other.x, y / other.y)

data class Foo(val x: Int, val y: Int) {
    operator fun plus(other: Foo): Foo = Foo(x + other.x, y + other.y)
    operator fun times(other: Foo): Foo = Foo(x * other.x, y * other.y)
    operator fun rem(other: Foo): Foo = Foo(x % other.x, y % other.y)
}

fun main(args: Array<String>) {
    val f1 = Foo(30, 40)
    val f2 = Foo(10, 20)
    (f1 - f2).println() // 打印Foo(x=20, y=20)
    (f1 + f2).println() // 打印Foo(x=40, y=60)
    (f1 * f2).println() // 打印Foo(x=300, y=800)
    (f1 / f2).println() // 打印Foo(x=3, y=2)
    (f1 % f2).println() // 打印Foo(x=0, y=0)
}

除了定义相同类型的运算数之外,还能定义运算数类型不同的运算符:

data class Foo(val x: Int, val y: Int) {
    operator fun times(other: Double): Foo = Foo((x * other).toInt(), (y * other).toInt())
}

fun main(args: Array<String>) {
    val f1 = Foo(30, 40)
    (f1 * 1.5).println() // 打印Foo(x=45, y=60)
}

当你通过这样子去调用这个运算符的时候

(1.5 * f1).println()

这时候,编译器会提示你出错了

image

为什么会这样呢?

image

因为Kotlin的运算符不会自动至此交换性(交换运算符的左右两边)。

那要怎么样才能那样写呢?

image

需要定义一个单独的运算符

operator fun Double.times(other: Foo): Foo = Foo((this * other.x).toInt(), (this * other.y).toInt())

这样子就能直接支持运算符两边互换使用了。。。

(f1 * 1.5).println()
(1.5 * f1).println()

运算符函数不是单一返回类型的,也是可以定义不同的返回类型,下面举个栗子:

operator fun Char.times(count: Int): String = toString().repeat(count)

fun main(args: Array<String>) {
    ('a' * 3).println() // 打印aaa
}

在上面的代码中,这个运算符是Char类型的扩展函数,参数类型是Int类型,所以是Char * Int这样的操作,返回类型是String

注意:运算符和普通函数一样,可以重载operator函数,可以定义多个同名,但是参数不一样的方法。

重载复合赋值运算符

什么是复合赋值运算符?
类似于+=这样的,合并了两部操作的运算符,同时赋值,称为符合运算符。

下面我们列举对应的函数名:

表达式 函数名
a += b timesAssign
a /= b divAssign
a %= b remAssign
a += b plusAssign
a -= b minusAssign
fun main(args: Array<String>) {
    var f1 = Foo(1, 2)
    f1 += Foo(3, 4)
    f1.println() // 打印Foo(x=4, y=6)
}

上面的+=等同于f1 = f1 + Foo(3, 4),这些操作当然是只对可变变量有效的。

默认情况下,复合赋值运算符是可以修改变量所引用的对象,同时重新分配引用,但是在将一个元素添加到一个可变集合的时候,+=是不会重新分配引用的:

fun main(args: Array<String>) {
    val list = mutableListOf<Int>()
    list += 42
    list.println() // 打印[42]
}

同样我们可以对复合赋值运算符进行重载,同样可以定义多个同名,但是参数不一样的方法:

operator fun MutableCollection<Int>.plusAssign(element: Int) {
    this.add(element - 1)
}

fun main(args: Array<String>) {
    val list = mutableListOf<Int>()
    list += 42
    list.println() // 打印[41]
}

如果在plusplusAssign两个函数同时被定义且适用,那么编译器就会报错,最好在设计新类的时候保持(可变性)一致,尽量不同时定义plusplusAssign运算。如Foo类是不可变的,那么只提供plus运算,如果一个类是可变的,如构造器,那么只需提供plusAssign和类似的运算就够了。

image

实际上+=可以被转换为plus或者plusAssign函数调用,而Kotlin的标准库中为集合支持这两种方法。

  • +-运算符会返回一个新的集合。
  • +=-=用于可变集合,会修改集合,如果是只读,那么就会返回一个修改过的副本,也就是说只有在只读集合被定义为var类型的时候,才能使用+=-=
fun main(args: Array<String>) {
    // 可变类型
    val list = mutableListOf<Int>(1, 2)
    // += 修改list
    list += 3
    // + 返回一个新的List
    val newList = list + listOf<Int>(4, 5) // 除了使用单个元素参数,也可使用元素类型相同的集合
    list.println() // 打印[1, 2, 3]
    newList.println() // 打印[1, 2, 3, 4, 5]
    var varList = listOf<Int>(1, 2)
    // 只读集合类型为var
    varList.println() // 打印[1, 2]
    varList += 3
    varList.println() // 打印[1, 2, 3]
}

重载一元运算符

Kotlin中允许重载一元运算符,如-a,+a等等,同样我们列举支持的一元运算符和对应的函数名:

表达式 函数名
+a unaryPlus
-a unaryMinus
!a not
++a, a++ inc
--a, a-- dec

重载一元运算符过程与前面一样,通过预先定义的一个名称来声明函数(成员函数或者扩展函数),并且用operator修饰符标记。

注意:一元运算符是没有参数的。

data class Foo(val x: Int, val y: Int)

operator fun Foo.unaryMinus() = Foo(-x, -y)

fun main(args: Array<String>) {
    val f1 = Foo(1, 2)
    (-f1).println() // 打印Foo(x=-1, y=-2)
}

当重载自增自减运算符符是,编译器自动支持前缀--a和后缀a--语义。

operator fun BigDecimal.inc() = this + BigDecimal.ONE

fun main(args: Array<String>) {
    var bd = 0
    (bd++).println() // 打印0
    (++bd).println() // 打印2
}

重载比较运算符

比较运算符,可以在除了基本数据类型外的任意对象上使用,当Java中使用equalscompareTo时,在Kotlin中,直接用运算符重载。
比较运算符分为等号运算符和排序运算符。

表达式 函数名
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

等号运算符equals

在我们平时使用判断字符串是否与某个字符串相等的时候,会使用equals函数来判断,然而在Kotlin中,我们可以是用==来代替equals函数,~=来代替!qeuals
在Java中如果使用null对象来equals的话,会爆空指针异常,而Kotlin中的==是支持可空类型的,因为会先判断是否为空,如a == b会先检查a是否为空,如果不是,就会调用a.equals(b),否则只有两个参数都是空值,结果才为真。

image

下面我们来重载equals运算符

data class Foo(val x: Int, val y: Int) {
    override operator fun equals(other: Any?): Boolean = when {
        // 使用恒等运算符来判断两个参数是否同一个对象的引用
        other === this -> true
        other !is Foo -> false
        else -> other.x == x && other.y == y
    }
}

fun main(args: Array<String>) {
    val f1 = Foo(1, 2)
    val f2 = Foo(1, 2)
    val f3 = Foo(10, 20)
    (f1 == f2).println() // true
    (f1 == f2).println() // true
    (f1 != f2).println() // false
    (null == f1).println() // false
}

注意:===与Java一样,检查两个参数是否是同一个对象的引用,如果是基本数据类型,检查值是否相同,===!==不能被重载。

排序运算符compareTo

在Java中,基本数据类型集合排序通常都是使用<>来比较,而其他类型需要使用element1.compareTo(element2)来比较的。而在Kotlin中,通过使用比较运算符(>``<``>=``<=)来进行比较。

image

比较运算符会被转换成compareTo函数,compareTo的返回类型必须为Int

class Person(private val firstName: String, private val lastName: String) : Comparable<Person> {
    override fun compareTo(other: Person): Int = compareValuesBy(this, other, Person::lastName, Person::firstName)
}

fun main(args: Array<String>) {
    (Person("Alice", "Smith") < Person("Bob", "Johnson")).println() // 打印false
}

compareValuesBy函数是按顺序依次调用回调方法,两两一组分别做比较,然后返回结果,如果则返回比较结果,如果相同,则继续调用下一个,如果没有更多回调来调用,则返回0。

image

override标记

Any
Comparable

从上面可以看到,equalscompareTo都是被override标记的,之所以会被标记,是因为在Any类中已经定义了equals函数,而所有的对象都默认继承Any类,所有才重载的时候需要使用override标记,而且equals不能定义为扩展函数,因为Any类的实现是重要优先于扩展函数。
同样,compareToComparable接口中已经定义了,所有在重载的时候,需要使用override标记。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容