第11章 运算符重载与约定

我们在《第2章 Kotlin 语法基础》中已经学习过关于运算符的相关内容,本章将继续深入探讨Kotlin中的运算符的重载与约定。

通常一门编程语言中都会内置预定义的运算符(例如: + , - , * , / , == , != 等),这些运算符的操作对象只能是基本数据类型。而在实际的编程场景中有很多自定义类型,其实也有类似的运算操作。这就是我们通常说的运算符重载(overload)。

Java中是不支持运算符重载的。而 Kotlin 允许我们为自己的类型实现一套自己的操作符运算逻辑的实现(重载函数)。这些操作符在Kotlin中是约定好的固定的符号 (如:加法 + 、乘法 *)和固定的优先级。而实现这样的操作符,我们也必须使用映射的固定名字的成员函数或扩展函数(加法 plus 、 乘法times)。 重载操作符的函数需要用 operator 修饰符来标记。

11.1 什么是运算符重载

运算符重载是对已有的运算符赋予新的含义,使同一个运算符作用于不同类型的数据,会有对应这个数据类型的行为。

运算符重载的实质是函数重载,本质上是对运算符函数的调用,从运算符到对应函数的映射的这个过程由编译器完成。由于一般数据类型间的运算符没有重载的必要,所以运算符重载主要是面向对象之间的。

Kotlin中的运算符重载约定定义在 org.jetbrains.kotlin.util.OperatorNameConventions中

package org.jetbrains.kotlin.utilimport org.jetbrains.kotlin.name.Nameobject OperatorNameConventions {@JvmField val GET_VALUE = Name.identifier("getValue")@JvmField val SET_VALUE = Name.identifier("setValue")@JvmField val PROVIDE_DELEGATE = Name.identifier("provideDelegate")@JvmField val EQUALS = Name.identifier("equals")@JvmField val COMPARE_TO = Name.identifier("compareTo")@JvmField val CONTAINS = Name.identifier("contains")@JvmField val INVOKE = Name.identifier("invoke")@JvmField val ITERATOR = Name.identifier("iterator")@JvmField val GET = Name.identifier("get")@JvmField val SET = Name.identifier("set")@JvmField val NEXT = Name.identifier("next")@JvmField val HAS_NEXT = Name.identifier("hasNext")@JvmField val COMPONENT_REGEX = Regex("component\\d+")@JvmField val AND = Name.identifier("and")@JvmField val OR = Name.identifier("or")@JvmField val INC = Name.identifier("inc")@JvmField val DEC = Name.identifier("dec")@JvmField val PLUS = Name.identifier("plus")@JvmField val MINUS = Name.identifier("minus")@JvmField val NOT = Name.identifier("not")@JvmField val UNARY_MINUS = Name.identifier("unaryMinus")@JvmField val UNARY_PLUS = Name.identifier("unaryPlus")@JvmField val TIMES = Name.identifier("times")@JvmField val DIV = Name.identifier("div")@JvmField val MOD = Name.identifier("mod")@JvmField val REM = Name.identifier("rem")@JvmField val RANGE_TO = Name.identifier("rangeTo")@JvmField val TIMES_ASSIGN = Name.identifier("timesAssign")@JvmField val DIV_ASSIGN = Name.identifier("divAssign")@JvmField val MOD_ASSIGN = Name.identifier("modAssign")@JvmField val REM_ASSIGN = Name.identifier("remAssign")@JvmField val PLUS_ASSIGN = Name.identifier("plusAssign")@JvmField val MINUS_ASSIGN = Name.identifier("minusAssign")// If you add new unary, binary or assignment operators, add it to OperatorConventions as well@JvmFieldinternal val UNARY_OPERATION_NAMES = setOf(INC, DEC, UNARY_PLUS, UNARY_MINUS, NOT)@JvmFieldinternal val SIMPLE_UNARY_OPERATION_NAMES = setOf(UNARY_PLUS, UNARY_MINUS, NOT)@JvmFieldval BINARY_OPERATION_NAMES = setOf(TIMES, PLUS, MINUS, DIV, MOD, REM, RANGE_TO)@JvmFieldinternal val ASSIGNMENT_OPERATIONS = setOf(TIMES_ASSIGN, DIV_ASSIGN, MOD_ASSIGN, REM_ASSIGN, PLUS_ASSIGN, MINUS_ASSIGN)
}

运算符跟操作符函数的映射关系的定义在
org.jetbrains.kotlin.types.expressions.OperatorConventions.java中

public static final ImmutableBiMap<KtSingleValueToken, Name> UNARY_OPERATION_NAMES = ImmutableBiMap.<KtSingleValueToken, Name>builder().put(KtTokens.PLUSPLUS, INC).put(KtTokens.MINUSMINUS, DEC).put(KtTokens.PLUS, UNARY_PLUS).put(KtTokens.MINUS, UNARY_MINUS).put(KtTokens.EXCL, NOT).build();public static final ImmutableBiMap<KtSingleValueToken, Name> BINARY_OPERATION_NAMES = ImmutableBiMap.<KtSingleValueToken, Name>builder().put(KtTokens.MUL, TIMES).put(KtTokens.PLUS, PLUS).put(KtTokens.MINUS, MINUS).put(KtTokens.DIV, DIV).put(KtTokens.PERC, REM).put(KtTokens.RANGE, RANGE_TO).build();public static final ImmutableBiMap<Name, Name> REM_TO_MOD_OPERATION_NAMES = ImmutableBiMap.<Name, Name>builder().put(REM, MOD).put(REM_ASSIGN, MOD_ASSIGN).build();public static final ImmutableBiMap<KtSingleValueToken, Name> ASSIGNMENT_OPERATIONS = ImmutableBiMap.<KtSingleValueToken, Name>builder().put(KtTokens.MULTEQ, TIMES_ASSIGN).put(KtTokens.DIVEQ, DIV_ASSIGN).put(KtTokens.PERCEQ, REM_ASSIGN).put(KtTokens.PLUSEQ, PLUS_ASSIGN).put(KtTokens.MINUSEQ, MINUS_ASSIGN).build();

其中,KtTokens.kt中定义了+, -, *, / , == , ! , ++, -- , *= , /= 等等运算符的符号。从源码中的这一句

    public static final ImmutableSet<KtSingleValueToken> NOT_OVERLOADABLE =ImmutableSet.of(KtTokens.ANDAND, KtTokens.OROR, KtTokens.ELVIS, KtTokens.EQEQEQ, KtTokens.EXCLEQEQEQ);

我们可以知道,Kotlin中的 && 、 || 、 ?: 、 === 、 !== 是不能被重载的。

有了操作符重载我们可以将两个对象加起来变成另外一个对象。例如,我们自定义一个BoxInt类型,然后重载 times (乘法 * )函数, plus ( 加法 + )函数。

class BoxInt(var i: Int) {operator fun times(x: BoxInt) = BoxInt(i * x.i) // 使用类成员函数重载override fun toString(): String {return i.toString()}
}operator fun BoxInt.plus(x: BoxInt) = BoxInt(this.i + x.i) // 使用扩展函数的方式重载

然后,我们的测试代码如下

fun main(arg: Array<String>) {val a = BoxInt(3)val b = BoxInt(7)println(a + b) //10println(a * b) //21
}

运算符重载其实是Kotlin的一个语法糖。我们可以把上述代码反编译成Java 字节码,可以看到 a+b 其实是等价于Java中的:

   public static final BoxInt plus(@NotNull BoxInt $receiver, @NotNull BoxInt x) {...return new BoxInt($receiver.getI() + x.getI());}

下面是a+b被编译成class代码之后的样子。第三句的 INVOKESTATIC 验证了上面的说明:

    ALOAD 1ALOAD 2INVOKESTATIC com/easy/kotlin/OperatorOverloadDemoKt.plus (Lcom/easy/kotlin/BoxInt;Lcom/easy/kotlin/BoxInt;)Lcom/easy/kotlin/BoxInt;POP

而 a * b 等价于 Java 中的:

public final class BoxInt {public final BoxInt times(@NotNull BoxInt x) {...return new BoxInt(this.i * x.i);}...
}

对应的字节码如下。同样的,第3行 INVOKEVIRTUAL 表明运算符重载确实是Kotlin的在编译器层面实现的一个语法糖。

    ALOAD 1ALOAD 2INVOKEVIRTUAL com/easy/kotlin/BoxInt.times (Lcom/easy/kotlin/BoxInt;)Lcom/easy/kotlin/BoxInt;POP

从上面的例子的分析,我们可以看出 Kotlin 通过在编译器层面做了大量工作,就是为了让 Kotlin 程序员们的代码尽可能的简洁,而让编译器处理更多的事情。毋庸置疑的是Kotlin的简洁优雅而且强大实用的语法和各种各样的语法糖可以大大地提升程序员们的生产力。这是都是直接使用 Java 享受不到的特性。

11.2 重载二元算术运算符

通过阅读上面的源码,我们可以总结出Kotlin中的二元运算符以及对于的运算符重载函数名称之间的映射关系如下表

二元运算符 重载函数名称 备注
a + b a.plus(b) 加法操作
a - b a.minus(b) 减法操作
a * b a.times(b) 乘法操作
a / b a.div(b) 除法操作
a % b a.rem(b) 取余操作,早期版本中叫mod
a..b a.rangeTo(b) 范围操作符

例如,一个简单的 1+1 = 2 的运算的实例代码

>>> 1+1
2

其实,本质上执行的是

>>> 1.plus(1)
2

Kotlin中使用 operator fun 声明重载运算符函数。例如上面的Int类型的加法运算符函数的声明如下

operator fun plus(other: Byte): Int

自定义类型的运算符重载函数的作用与内置赋值运算符的作用是同样的声明方式,但是具体的运算逻辑的实现则是“自定义”的。

编程实例题:

设计一个类Complex,实现复数的基本操作:
成员变量:实部 real,虚部 image,均为整数变量;
构造方法:无参构造函数、有参构造函数(参数2个)
成员方法:两个复数的加、减、乘操作。例如:
相加: (1+2i) + (3+4i) = 4 + 6i
相减: (1+2i) - (3+4i) = -2 - 2i
相乘: (1+2i) * (3+4i) = -5 + 10i

1.首先,我们来声明一个类Complex,里面声明两个Int成员变量 real, image。代码如下

package com.easy.kotlinclass Complex {var real: Int = 0var image: Int = 0
}

使用IDEA进行Kotlin编程的过程是非常享受的过程。直接在当前源码文件Complex类内右击鼠标,我们会看到 Generate (Mac 上的快捷键是 Command N) 如下图

在当前源码文件Complex类内右击鼠标

点击 Generate ,进入 Generate 生成对话框

Generate 生成对话框

点击 Secondary Constructor, 进入初始化构造函数的属性选择对话框

初始化构造函数的属性选择对话框

在这个Choose Properties to Initialize by Constructor对话框中不选参数将会生成无参构造函数,如下图

不选参数将会生成无参构造函数

选中2个参数将会生成这2个参数的构造函数,如下图

选中2个参数将会生成这2个参数的构造函数

最终自动生成的无参构造函数、2个参数的构造函数代码如下

package com.easy.kotlinclass Complex {var real: Int = 0var image: Int = 0constructor()constructor(real: Int, image: Int) {this.real = realthis.image = image}
}

我们看到,Generate对话框中还可以自动生成equals() 和 hashCode()函数,toString()函数,可以选择 Override Methods,Implement Methods,自动生成 Copyright等功能。

2.实现加法、减法、乘法运算符重载函数

复数加法的运算规则是:实部加上实部,虚部加上虚部

(a+bi) + (c+di) = ( a + c )+ ( b + d )i

对应的算法的函数实现是

    operator fun plus(c: Complex): Complex {return Complex(this.real + c.real, this.image + c.image)}

复数减法的运算规则是:实部减去实部,虚部减去虚部

(a+bi) - (c+di) = (a - c)+ (b - d)i

对应的算法的函数实现是

    operator fun minus(c: Complex): Complex {return Complex(this.real - c.real, this.image - c.image)}

复数乘法的运算规则是按照乘法分配律展开:

(a+bi)(c+di) = ac-db + (bc+ad)i

对应的算法的函数实现是

    operator fun times(c: Complex): Complex {return Complex(this.real * c.real - this.image * c.image, this.real * c.image + this.image * c.real)}

然后,为了可读性,我们重写 toString() 函数如下

    override fun toString(): String {val img = if (image >= 0) "+ ${image}i" else "${image}i"return "$real ${img} "}

测试代码:

fun main(args: Array<String>) {val c1 = Complex(1, 1)val c2 = Complex(2, 2)val p = c1 + c2val m = c1 - c2val t = c1 * c2println(p) println(m)println(t)
}

输出:

3 + 3i
-1 -1i
0 + 4i

11.3 重载自增自减一元运算符

我们已经知道Kotlin中可以重载的一元运算符有

运算符函数 运算符
a.unaryPlus() +a
a.unaryMinus() -a
a.not() !a
a.inc() a++,++a
a.dec() a--,--a

现在我们就用实例来深入说明怎样重载这些运算符。
我们定义一个Point类,然后实现unaryMinus运算符函数的重载。

class Point(val x: Int, val y: Int) {operator fun unaryMinus() = Point(-x, -y)override fun toString(): String {return "Point(x=$x, y=$y)"}
}

测试代码

val p1 = Point(1, 1)
println(-p1) // Point(x=-1, y=-1)

我们现在给Java中的BigDecimal 类型添加一个自增运算符 inc() 函数,给已有的类添加运算符重载函数,我们采用扩展函数来实现。定义 operator fun BigDecimal.inc() ,实现代码如下

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

然后,我们就可以直接对一个 BigDecimal 类型进行自增的操作了。测试代码如下

    var bigDecimal1 = BigDecimal(100)var bigDecimal2 = BigDecimal(100)val tmp1 = bigDecimal1++val tmp2 = ++bigDecimal2println(tmp1)// 100println(tmp2)// 101println(bigDecimal1) // 101println(bigDecimal2) // 101

因为自增后缀表达式是先返回表达式的值,然后进行加1操作,所以tmp1的值是100;而自增后缀表达式是加1的后值作为表达式的值,所以tmp2的值是101 。而在下一行打印变量bigDecimal1,bigDecimal2的值都是101 。

类似的自减运算符重载函数实现如下

operator fun BigDecimal.dec() = this - BigDecimal.ONE

测试代码:

    var bigDecimal3 = BigDecimal(100)var bigDecimal4 = BigDecimal(100)val tmp3 = bigDecimal3--val tmp4 = --bigDecimal4println(tmp3)// 100println(tmp4)// 99println(bigDecimal3) // 99println(bigDecimal4) // 99

而我们之所以能在实现函数中直接调用 this + BigDecimal.ONE 和 this - BigDecimal.ONE 是因为Kotlin 语言本身已经对 BigDecimal 进行了加法、减法、乘法、取余、取负等运算符的重载。这些重载运算符函数定义在BigNumbers.kt中

public inline operator fun BigDecimal.plus(other: BigDecimal) : BigDecimal = this.add(other)public inline operator fun BigDecimal.minus(other: BigDecimal) : BigDecimal = this.subtract(other)public inline operator fun BigDecimal.times(other: BigDecimal) : BigDecimal = this.multiply(other)public inline operator fun BigDecimal.div(other: BigDecimal) : BigDecimal = this.divide(other, RoundingMode.HALF_EVEN)public inline operator fun BigDecimal.mod(other: BigDecimal) : BigDecimal = this.remainder(other)public inline operator fun BigDecimal.rem(other: BigDecimal) : BigDecimal = this.remainder(other)public inline operator fun BigDecimal.unaryMinus() : BigDecimal = this.negate()

而这些运算符重载函数实现的背后其实就是调用的BigDecimal 的add 、 subtract、 multiply 、divide、remainder 、 negate 等方法。

我们可以看出,Kotlin 通过更高层次的封装,大大简化了BigDecimal 数据类型的算术运算的代码,使得BigDecimal 算术运算的代码更加简单易读。而在Java中,我们不得不实用冗长的方法名进行调用。虽然Kotlin背后的调用的仍然是Java的方法,但是对于Kotlin程序员来说,无疑是更加简洁明了了。

11.4 重载比较运算符

我们知道,在Java中,< 、> 、 >= 、 <= 、 ==、 != 运算符,只能作用于基本数据类型的比较

    public static void main(String[] args) {int x = 1;int y = 1;boolean b1 = x > y;boolean b2 = x < y;boolean b3 = x >= y;boolean b4 = x <= y;boolean b5 = x == y;boolean b6 = x != y;System.out.println(b1);System.out.println(b2);System.out.println(b3);System.out.println(b4);System.out.println(b5);System.out.println(b6);}

而在对象类型上是不允许使用这些比较运算符进行比较的

Java对象类型上不允许使用比较运算符进行比较

而实际上,只要给定一个比较标准,原则上对象之间也是可以比较大小的,而不是仅仅限于基本数据类型。因为Kotlin中一切类型都是引用类型。所以,对象之间的比较将是“自然而然”的。本节我们介绍比较运算符的重载。

上面的BigDecimal 比较的Java代码,在Kotlin中是允许的

    val bd1 = BigDecimal.ONEval bd2 = BigDecimal.ONEval bdbd = bd1 > bd2val bdeq = bd1 == bd2val bdeqeq = bd1 === bd2println(bdbd) // falseprintln(bdeq) // trueprintln(bdeqeq) // true

其中的大于号 > 会映射成调用 compareTo(BigDecimal val) > 0 的值。

Kotlin中的比较运算符与重载函数名之间的映射关系如下表所示

表达式 翻译成函数调用
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(Object x) 方法。需要注意的是,a == b表达式中就算 a、 b 是null,也可以安全调用。因为 a==b 会被Kotlin编译器翻译成带可空性判断的 equals() 方法的调用: a?.equals(b) ?: (b === null) 。

而3个等于号 === 是Kotlin中自己实现的运算符,这个运算符不能被重载,它不仅比较值是否相等,还会去比较对象的引用是否相等。因为BigDecimal.ONE 是常量,在JVM内存模型中是存在常量区的,所以 bd1 === bd2 返回的也是 true 。

例如,我们对下面的Point类

class Point(val x:Int, val y:Int)

如果我们不去重载实现其equals() 函数,编译器会去自动生成一个equals()函数跟hashCode()函数


class Point(val x:Int, val y:Int){override fun equals(other: Any?): Boolean {if (this === other) return trueif (javaClass != other?.javaClass) return falseother as Pointif (x != other.x) return falseif (y != other.y) return falsereturn true}override fun hashCode(): Int {var result = xresult = 31 * result + yreturn result}
}

而如果我们想自定义相等的判断方法,可以进行重写实现其 equals() 函数。

现在,我们来比较两个Point对象的大小。如果我们定义Point对象之间大小的比较标准是其范数(用来度量某个向量空间(或矩阵)中的每个向量的长度)的大小,那么比较运算符的重载函数实现如下

    operator fun compareTo(other: Point): Int {val thisNorm = Math.sqrt((this.x * this.x + this.y * this.y).toDouble())val otherNorm = Math.sqrt((other.x * other.x + other.y * other.y).toDouble())return thisNorm.compareTo(otherNorm)}

测试代码

    val p1 = Point(1, 1)val p2 = Point(1, 1)val p3 = Point(1, 3)println(p1 >= p2) // trueprintln(p3 > p1) // true

11.5 重载计算赋值运算符

同样的计算赋值运算符

表达式 翻译成运算符重载函数的调用
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b)

如果我们想要重载某个类型的这些赋值运算符,只需要实现其对应的运算符重载函数即可。

本章小结

在进行对象之间的运算时,编译器解析的时候会去调用对应运算符重载函数。为了代码简单易懂,在实现运算符重载函数的时候一定要考虑其实际问题场景的意义,并且在运算符重载函数上写清楚对象之间的比较规则,注释写清楚。否则,如果滥用运算符重载,会导致代码易读性大大下降。

第11章 运算符重载与约定相关推荐

  1. c++第八周【任务1-1】实现复数类中的运算符重载

    /* (程序头部注释开始) * 程序的版权和版本声明部分 * Copyright (c) 2011, 烟台大学计算机学院学生 * All rights reserved. * 文件名称: c++第八周 ...

  2. 一篇文章带你了解Python运算符重载

    回复"python"即可获赠从入门到进阶共10本电子书 今 日 鸡 汤 不堪玄鬓影,来对白头吟. 您可以根据所使用的操作数来更改Python中运算符的含义.这种做法称为运算符重载, ...

  3. python 运算符重载_《fluent python》第 13 章 正确重载运算符

    引言 有些事情让我不安,比如运算符重载.我决定不支持运算符重载,这完全是个人选择,因为我见过太多 C++ 程序员滥用它--James Gosling(Java 之父) ps: 运算符重载它不香吗 写在 ...

  4. 新标准C++(郭炜)第四章细节问题小结(1):运算符重载(一)

    一.运算符重载的概念和原理(P65-P66) 运算符重载的目的:使得C++中的运算符也能用来操作对象. ---------------->运算符重载的实质是编写以运算符作为名称的函数 运算符函数 ...

  5. 《Windows游戏编程大师技巧》(第二版)第11章

    第三部分:核心游戏编程   第11章 算法.数据结构.内存管理和多线程   第12章 人工智能   第13章 游戏物理   第14章 文字时代   第15章 综合运用:编写游戏! 第11章 算法.数据 ...

  6. C++——运算符重载operator

    C++--运算符重载operator C++ prime plus第11章,运算符重载是C++的一种多态.运算符重载格式如下: operator运算符(argument-list) 1.做普通函数重载 ...

  7. c++--运算符重载

    第14章 重载运算与类型转换 1 class Sales_data 2 { 3 /*重载<<和+运算符*/ 4 friend ostream& operator<<(o ...

  8. 笔记②:牛客校招冲刺集训营---C++工程师(面向对象(友元、运算符重载、继承、多态) -- 内存管理 -- 名称空间、模板(类模板/函数模板) -- STL)

    0618 C++工程师 第5章 高频考点与真题精讲 5.1 指针 & 5.2 函数 5.3 面向对象(和5.4.5.5共三次直播课) 5.3.1 - 5.3.11 5.3.12-14 友元 友 ...

  9. 高性能Linux服务器 第11章 构建高可用的LVS负载均衡集群

    高性能Linux服务器 第11章 构建高可用的LVS负载均衡集群 libnet软件包<-依赖-heartbeat(包含ldirectord插件(需要perl-MailTools的rpm包)) l ...

最新文章

  1. python selenium 进入新标签页_Python 爬虫 | 用selenium实现批改网的自动翻译
  2. java 代码效率_Java效率
  3. 【一起去大厂系列】针对left join以及limit的两条优化小技巧
  4. React开发(277):ant design time刚进入页面时间重置
  5. MongoDB学习笔记—02 MongoDB入门
  6. 医疗大数据技术与应用
  7. Cannot find class [***] for bean with name '***' defined in file[***]
  8. 【YOLO家族】【论文翻译】YOLO v1 Unified, Real-Time Object Detection
  9. 禁忌搜索算法(Tabu Search)
  10. Computer Networking——transport layer QA
  11. EXCEL查找与引用函数
  12. 碎碎念集萃三零【静心】
  13. 无服务器永久网站,ZeroNet无需域名服务器建立永久不会被拦截的网站
  14. 支藏人元及五行四时旺衰
  15. elemet-ui后台表格自动排序解决办法
  16. pentaho源码分析
  17. 郑州互联网公司和生活成本
  18. 2023最新SSM计算机毕业设计选题大全(附源码+LW)之java高校就业管理系统157v3
  19. Spring boot开发小而美的个人博客
  20. 3650m5服务器内存选择 ibm_重返荣耀 联想System x3650 M5服务器评测

热门文章

  1. python-笔记(四)函数
  2. Jasperserver 添加字体方法
  3. 抖音测试的软件,抖音app测试版
  4. Pytorch函数keepdim=True
  5. 通过PS制作手机图标心得总结
  6. JUC第六讲:ThreadLocal/InheritableThreadLocal详解/TTL-MDC日志上下文实践
  7. python中max什么意思_python中max()语法的说明
  8. 头条搜索下拉词怎么做?高粱seo实战告诉你答案
  9. Hadoop基础操作--查询集群的计算资源信息
  10. 朋友圈被公司“无偿征用”,该怎么办?