Kotlin学习系列之:运算符重载(operator overloading)

x33g5p2x  于2022-03-08 转载在 其他  
字(3.1k)|赞(0)|评价(0)|浏览(286)

1.引入:运算符重载,最初接触到这个概念是在C++里,Java中是没有的,取而代之的是通过特定接口来实现,比如进行排序比较大小时,我们可以实现Comparable接口。而Kotlin中,又重新支持该特性,因为这样会显得更加直观。

2.如何实现运算符重载:

  • 方式一:在类内定义,以成员方法(member)的形式

比如,我们定义一个Point类:

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

为了方便,这里将其声明为data class(data class的介绍请参看系列中此篇)。现在我们要支持对两个Point对象的使用运算符+实现相加功能,我们可以如何实现呢?

package com.xlh.kotlin.convention

data class Point(val x: Int, val y: Int) {
    /**
     * 方式一:通过member的形式
     * 1. operator 关键字 :运算符重载中必须使用该关键字
     * 2. 函数名约定
     */
    operator fun plus(o: Point): Point {
        return Point(x + o.x, y + o.y)
    }
}

fun main(){
    val p1 = Point(1, 2)
    val p2 = Point(3, 4)
    println(p1 + p2)    //打印 Point(x=4, y=6)
}

正如我在注释里标注的一样,这里我们只需要注意两点:

  • operator关键字: 表明该方法用于运算符重载
  • 方法名:方法名是固定的,如果想要重载运算符+,则方法名必须是plus;如果想要重载运算符-,则方法名必须是minus。详情参看下表:
operatorfunction name
+plus
-minus
*times
/div
%mod

然后我们就可以使用+运算符实现两个Point对象的相加。那么运算符重载本质上是怎样的呢,下面我们通过反编译来一探究竟。

来到out/production/JavaSETest(源码在idea下编写)目录下执行:javap -c 全类名(com.xlh.kotlin.convention.PointKt)

可以看得出来,对于此处的p1 + p2,相当于p1.plus(p2),然后会生成一个新的Point对象返回。如果你对于kotlin中的另一个特性——扩展的原理熟悉的话,你会发现这块反编译的结果和它极其相似。所以,我们还可以通过扩展的方式来实现运算符重载。

  • 方式二:通过扩展(extension)的方式实现运算符重载
operator fun Point.minus(other: Point): Point{
    return Point(x - other.x, y - other.y)
}

fun main(){
    val p1 = Point(1, 2)
    val p2 = Point(3, 4)
    println(p2 - p1)    // 打印Point(x=2, y=2)
}

3.在Java中如何调用:

public static void main(String[] args) {

    Point p1 = new Point(10, 20);
    Point p2 = new Point(20, 30);

    // member
    Point plusResult = p1.plus(p2);
    // extension        
    Point minusResult = PointKt.minus(p2, p1);

    System.out.println("p1 + p2 = " + plusResult);
    System.out.println("p2 - p1 = " + minusResult);

}

4.单向运算符重载不支持交换律.

operator fun Point.times(scale: Double): Point{
    return Point((x * scale).toInt(), (y * scale).toInt())
}

对于Point,我们这里重载了*,于是我们可以这样去使用:

val p1 = Point(1, 2)
println(p1 * 2.0)   //输出Point(x=2, y=4)

但是,我们不可以写成2.0 * p1,这样编译不会通过。如果要想使用2.0 * p1这种形式,那么我们必须相对地去实现Double.times()

operator fun Double.times(p: Point): Point {
    return Point((this * p.x).toInt(), (this * p.y).toInt())
}

5.返回类型可以和参与运算的左右两边类型都不相同。例如:

operator fun Char.times(count: Int): String{
    return toString().repeat(count)
}

于是我们可以有:

println('a' * 3)    //输出aaa

这里,Char * Int 最终可以得倒一个String类型

6.前面我们所列举的例子都是二元运算符的重载,下面我们来看看一元运算符的重载。

operatorfunction name
+aunaryPlus
-aunaryMinus
!anot
a, ainc
--a, a--dec

我们就以自增运算符为例:

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

var n1 = BigDecimal.ONE
println(n1++)   //打印1
println(++n1)   //打印3

注意:对于需要进行自增或自减的变量必须是var声明的,也很好理解,自增或自减的过程是会修改变量的值的,声明为val自然不合适

7.比较运算符重载

  • ==:equals()。这个大家应该很熟悉了(结构性相等的比较),我们这里只补充一点,大家有没有想过这样的一个问题,如果左边的值为null,会不会空指针呢?答案是不会的,因为a == b,相当于:a?.equals(b) ?: (b == null)
  • === : 这个不允许被重载(引用性相等的比较)
  • 、<、>=、<= : 这类常用用在集合或数组的排序当中,或者是求最值。我们可以定义一个Student类:

class Student(val firstName: String, val lastName: String) : Comparable<Student> {

    override fun compareTo(other: Student): Int {
        return compareValuesBy(this, other, Student::lastName, Student::firstName)
    }
}

于是可以有:

val p1 = Student("san", "zhang")
val p2 = Student("si", "li")
println(p1 > p2) //true

如果你足够细心,你会发现这里的compareTo并没有使用operator关键字修饰,为什么呢?因为在父接口Comparable中,已经使用了operator:

public interface Comparable<in T> {
    /**
     * Compares this object with the specified object for order. Returns zero if this object is equal
     * to the specified [other] object, a negative number if it's less than [other], or a positive number
     * if it's greater than [other].
     */
    public operator fun compareTo(other: T): Int
}

参考文献:《Kotlin in action》

相关文章