Kotlin运算符重载及其他约定

一、重载算术运算符

1.1 重载二元算术运算

kotlin 允许我们重载常用的二元算术运算:+ - * / ,这样我们这些基本运算就不只是能运用于基本数据结构(int,string 等)了,我们还可以用这些符号操作对象,集合等。比如对象A+对象B,往集合C添加元素等。

重载对象相加运算符写法如下:

data class Point(val x: Int, val y: Int) {
    //operator关键字就是重载运算符标识 plus 对应+ 不可随意更名
    operator fun plus(p: Point): Point {
        return Point(x + p.x, y + p.y)
    }
}

我们建立一个Point类重载plus 方法,这样我们可以在外部调用类的方法让两个对象相加:

@Test
    fun testSecondPoint() {
        val p1 = Point(10, 20)
        val p2 = Point(30, 40)

        print(p1 + p2)
        print("\n" + p1.plus(p2))
    }

打印结果如下:

Point(x=40, y=60)
Point(x=40, y=60)


Process finished with exit code 0

除了把Point类声明为成员函数外,还可以声明为扩展函数,对上面的类进行如下更改即可:

data class Point(val x: Int, val y: Int) {
    //operator关键字就是重载运算符标识 plus 对应+ 不可随意更名
//    operator fun plus(p: Point): Point {
//        return Point(x + p.x, y + p.y)
//    }
}

//把方法写到类的外面去,等同于java的静态函数
operator fun Point.plus(p: Point): Point {
    return Point(x + p.x, y + p.y)
}

这种写法和第一种写法是等价的,这种写法用的更多一些。在Kotlin 中你不能定义自己的运算符,只能重载系统已经有的运算符,可重载的二元运算符如下:

 a*b  times //对应的方法
 a/b  div
 a%b  rem
 a+b  plus
 a-b  minus

移位运算:
kotlin没有为标准数字类型定义任何位运算符,但提供了一些函数供我们使用:

shl -------带符号左移
shr -------带符号右移
ushr -------无符号右移
and --------按位与
or  ----------按位或
xor  ----------按位异或
inv  ----------按位取反

下面举个例子:

@Test
    fun testWei() {
        print(0x01 and 0x10)
        print("\n")
        print(0x02 or 0x10)
        print("\n")
        print(0x12)
        print("\n")
        print(0x02 or 0x10)
        print("\n")
        print(4 shl 1)
        print("\n")
        print(4 shr 2)
        print("\n")
        print(0x01 ushr 0x10)
    }
0
18
18
18
8
1
0


Process finished with exit code 0


print(4 shl 1)  代表 100左移1位---1000 = 8
print(4 shr 2)  代表 100右移2位---001 = 1

1.2 重载复合赋值运算符

上面的二元运算符对于合并步骤的操作也是有效的,比如 += -=,看如下例子:

var point = Point(10, 20)  //这里point 需要改为变量,常量就不能相加了
        point += Point(2, 3)

        print(point)
打印如下:
Point(x=12, y=23)


Process finished with exit code 0

kotlin系统为这种复合运算也定义了运算符,就是plusAssign,minusAssign等。
我们重载一个试试:

operator fun Point.plusAssign(p: Point) {
    this.x += p.x
    this.y += p.y
}

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

@Test
    fun testValue() {
        val point = Point(10, 20)
        val pointB = Point(5, 5)
        point.plusAssign(pointB)

        print(point)
    }
    
    
    结果:
    Point(x=15, y=25)


Process finished with exit code 0
    

注意这里的Point类的x,y必须声明为变量,否则会报错。

1.3 重载一元运算符

常见的一运算符如下:

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

现在让我们拿inc来试一下:

operator fun Point.inc(): Point {
    return Point(++x, ++y)
}
fun testInc(){
        var point = Point(10, 20)
        point.inc()

        print(point)
    }
    
    输出:
    Point(x=11, y=21)


Process finished with exit code 0

二、重载比较运算符

与算术运算符一样,我们也可以对任何对象使用比较运算符,而不仅仅限于基本数据类型。

2.1 等号运算符 'equals'

在kotlin中,所有== , !=运算符,都会转换成equals方法的调用。如下:

a==b  ------------------>a?.equals(b)?:(b==null)

比较 a 和 b 会检查 a是否非空,如果非空,调用a.equals(b) ,如果两个都为空,则返回true,equal函数是系统帮我们写好的,我们直接调用就行。如果我们需要手动去更改里面的比较逻辑,我们重写一下equals这个函数看一下:

data class Point(var x: Int, var y: Int) {
    override fun equals(obj: Any?): Boolean {
        if (obj === this) return true
        if (obj !is Point) return false
        return obj.x == x && obj.y == y
    }
}
@Test
    fun testequals() {
        print(Point(10, 15) == Point(10, 15))
        print("\n")
        print(Point(10, 15) != Point(20, 30))
        print("\n")
        print(null == Point(10, 15))
    }
输出:
true
true
false


Process finished with exit code 0

注意:Kotlin 中所有对象都支持等式比较,在Any类中已经标记了operator,我们不需要再次标记,我们需要重写,添加override关键字;还需要注意这里的equals方法不能实现为扩展函数,因为继承自Any类的实现始终优先于扩展函数。

2.2 排序运算符 'compareTo'

Kotlin中基础数据比较大小可以直接进行比较,比如:int、string ;但是对象的比较的话和java类似,都需要实现 Comparable接口,重写compareTo 方法,如下:

data class Point(var x: Int, var y: Int) : Comparable<Point> {
    override fun compareTo(obj: Point): Int {
        return compareValuesBy(this, obj, Point::x, Point::y)
    }
}
tip:a>=b-----------> a.compareTo(b)>=0 (两个对象的比较转换为compareTo的函数调用,然后结果与零进行比较)

我们调用一下看下效果:

@Test
    fun testcompareTo() {
        print(Point(10, 15) < Point(1, 2))
        print("\n")
        print(Point(10, 15) <= Point(20, 3))
        print("\n")
        print(Point(10, 15) <= Point(10, 3))
        print("\n")
    }
输出:
false
true
false

Process finished with exit code 0

可以看到我们先比较x,如果x值相等的情况,才会去比较y,有一个先后顺序。

三、集合与区间的约定

处理集合常见的操作是通过下标来获取和设置元素,以及检查元素是否属于当前集合,所有这些操作都支持运算符语法。比如下标运算符(a[b]),是否在集合中 in 运算符。

3.1 通过下标来访问元素 'get' 'set'

对于基础类型,我们可以直接访问,如下
1、访问map中的元素:

@Test
    fun testGetSet() {
        val map = mutableMapOf(0 to "a", 1 to "b", 2 to "c", 3 to "d")
        val value=map[2]
        print("\nvalue:"+value)

        map[1]="bbb"
        print("\nmap:"+map)
    }
输出:
value:c
map:{0=a, 1=bbb, 2=c, 3=d}


Process finished with exit code 0

这种下标获取在kotlin中会被转换成对get,set运算符方法的调用;map和MutableMap已经实现了这些方法,我们可以直接用,现在让我们来实现一个对象的get set运算符吧:

operator fun Point.get(index: Int): Int {
    return when (index) {
        0 -> x
        1 -> y
        else -> throw IndexOutOfBoundsException("IndexOutOfBoundsException")
    }
}

operator fun Point.set(index: Int, value: Int) {
    return when (index) {
        0 -> x = value
        1 -> y = value
        else -> throw IndexOutOfBoundsException("IndexOutOfBoundsException")
    }
}

然后在进行调用:

@Test
    fun testGetSet() {
        val map = mutableMapOf(0 to "a", 1 to "b", 2 to "c", 3 to "d")
        val value = map[2]
        print("\nvalue:" + value)

        map[1] = "bbb"
        print("\nmap:" + map)

        val p = Point(10, 20)
        print("\nmap:" + p[0])

        p[1] = 23
        print("\nmap:${p}")

        p.set(0, 5)
        print("\nmap:${p}")
    }
输出:
value:c
map:{0=a, 1=bbb, 2=c, 3=d}
map:10
map:Point(x=10, y=23)
map:Point(x=5, y=23)


Process finished with exit code 0

3.2 'in' 的约定

in 运算符用来检测某个对象是否属于集合内,对应的函数是contains,我们用某个点是否属于某个矩形来做测试:

data class Rectangle(val upLeft: Point, val lowRight: Point)

operator fun Rectangle.contains(p: Point): Boolean {
    return p.x in upLeft.x until lowRight.x &&
            p.y in upLeft.y until lowRight.y
}
@Test
    fun testIn() {
        val rect = Rectangle(Point(10, 20), Point(50, 50))
        val p = Point(20, 30)
        val p1 = Point(5, 5)

        print("\nmap:${p in rect}")
        print("\nmap:${p1 in rect}")
        print("\nmap:${rect.contains(p)}")

    }
输出:
map:true
map:false
map:true

Process finished with exit code 0

in 运算符会转化为集合类对contains的调用,所以两者等价

3.3 rangeTo 的约定

start .. end ------->start.rangeTo(end)

这个约定主要用来表示一个区间,优先级要低于算术运算符
这个约定一般用在基础类型中,如下:

@Test
    fun testRangeTo() {
        val a = 5
        print("\nmap:${0 .. (a+1)}")

        print("\nmap:${0.rangeTo(a+1)}")
    }
输出:
map:0..6
map:0..6


Process finished with exit code 0

四、结构声明和组件函数

4.1 结构声明和循环

结构声明:允许你展开单个复合值,并使用它来初始化多个单独的变量

@Test
    fun testXIgou() {
        val p=Point(10,20)
        //同时声明两个变量,然后用对象p给它赋值
        val (x,y)=p
        print("\n ${x}")
        print("\n ${y}")
    }
输出:
10
 20


Process finished with exit code 0

我们可以把上诉赋值过程看成:

val (a,b)=p ------------>val  a=p.component1()  val b=p.component2()

这个流程有点类似于java中split 方法将字符串分割,我们再来看看如何分割:

data class Name(
    val name: String,
    val ext: String
)

fun aplit(fullName: String): Name {
    val result = fullName.split(".", limit = 2)
    return Name(result[0], result[1])
}
fun testXI() {
        val (name,ext) = aplit("hello.txt")
        print("\n ${name}")
        print("\n ${ext}")
    }
    
    输出:
    hello
    txt


Process finished with exit code 0

相当于我们先分割字符串,在存入对象,最后在数据类中取值,kotlin只支持取前5元素
再来看下循环中如何使用:

@Test
    fun testXunhuan() {
        val map = mapOf("苹果" to "水果", "土豆" to "蔬菜")
        for ((key, value) in map) {
            print("\n$key -> $value")
        }
        val str="a,b,c,d,e,f,g"
        val list=str.split(",")
        for (p in list) {
            print("\n----$p")
        }
    }
苹果 -> 水果
土豆 -> 蔬菜
----a
----b
----c
----d
----e
----f
----g


Process finished with exit code 0

五、委托属性

5.1 委托属性基本操作

基本语法:

class Foo {
    var p:Type by Delegate()
}

属性p将它的访问器逻辑委托给了另一个对象:delegate类的一个新对象实例。通过关键字 by 对其后的表达式求值来获取这个对象,关键字 by 可以用于获取任何符合属性委托约定规则的对象。

5.2 使用委托属性:惰性初始化 和 by lazy()

惰性初始化是在第一次访问的时候,根据需要创建对象的一部分。在初始化过程需要消耗大量资源的时候非常有用。如下:

class Person(val name:String) {
    val emails by lazy{
        //懒加载数据内容,对于耗费资源的操作很有用
        // 比如查询数据库,这个是线程安全的
    }
}

5.3 实现委托属性

举个例子是对某人的年龄和工资变化进行跟踪:
首先创建一个类来当作被委托类,重载运算符 getvalue ,setvalue:

class ObservablePro(
    var oldVaule: Int, val changeVaule: PropertyChangeSupport
) {
    operator fun getValue(p: Person, prop: KProperty<*>): Int = oldVaule

    operator fun setValue(p: Person, prop: KProperty<*>, newV: Int) {
        val oldV = oldVaule
        oldVaule = newV
        changeVaule.firePropertyChange(prop.name, oldV, newV)
    }
}

然后,创建一个类,内部实现构造监听器以及委托给上面写的那个类

class Person(
    val name: String, age: Int, salary: Int
) {
    protected val change = PropertyChangeSupport(this)
    fun addListener(listener: PropertyChangeListener) {
        change.addPropertyChangeListener(listener)
    }

    var age: Int by ObservablePro(age, change)
    var salary: Int by ObservablePro(salary, change)
}

最后,外部调用初始化Person类并添加监听:

@Test
    fun testdelegate() {
        val person = Person("yang", 26, 1000)
        person.addListener(PropertyChangeListener {
            print("\nperson 数据有变更salary:${person.salary}\tage:${person.age}\tname:${person.name}")
        })
        person.age = 27
        person.salary = 1200
        person.age = 28
        person.salary = 1400
        person.age = 29
        person.salary = 1500
    }
输出:
person 数据有变更salary:1000 age:27  name:yang
person 数据有变更salary:1200 age:27  name:yang
person 数据有变更salary:1200 age:28  name:yang
person 数据有变更salary:1400 age:28  name:yang
person 数据有变更salary:1400 age:29  name:yang
person 数据有变更salary:1500 age:29  name:yang


Process finished with exit code 0

可以看到随着属性更改后,监听器在不断回调

推荐阅读更多精彩内容