Kotlin运算符重载及其他约定

重载算数运算符

下面以一个栗子开始,我们先定义一个Point
data class Point(val x : Int, val y : Int) 下面给Point定义一些算术运算符.(在java中算术运算符只能用于基本数据类型,但是在kotlin中可以在任何类型下面使用)
定义一个plus运算符

data class Point(val x: Int, val y : Int){
    operator fun plus(other : Point) : Point{
        return Point(x+other.x, y+other.y)
    }
}
>>> val p1 = Point(10,20)
>>> val p2 = Point(30,40)
>>> println(p1 + p2)
Point(x = 40, y = 60)

事实上它调用的是a.plus(b).
还可以定义成扩展函数

operator fun Point.plus(other : Point) : Point{
    return Point(x + other.x , y + other.y)
}

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())
}

定义一个返回结果不同的运算符

operator fun Char.times(count : Int) : String{
    return toString().repeat(count)
}
>>> println(‘a’ * 3)
aaa

这个运算符,接收一个Char作为左值,Int作为右值,然后返回一个String类型.

重载复合赋值运算符

通常情况下,当定义像plus这样运算符函数时,kotlin不止支持+号运算,也支持+=. 像+=,-=等这些运算符被称为复合赋值运算符.

>>> var point = Point(1,2)
>>> point += Point(3,4)
>>> println(point)
Point(x=4,y=6)

这等同于point = point + Point(3,4)的写法。
在一些情况下,定义+=运算可以修改使用它变量所引用的对象,但是不会重新分配引用。将一个元素添加到可变集合,就是一个很好的例子:

>>> val numbers = ArrayList<Int>()
>>> numbers += 42
>>> println(numbers[0])

如何定义这种复合赋值运算符呢,拿+=来举例,Kotlin标准库为可变集合定义了plusAssign函数,在前面的例子中可以这样使用:

operator fun<T> MutableCollection<T>.plusAssign(element : T){
    this.add(element)
}

不过在代码中用到+=的时候,理论上plus和plusAssign都可能被调用。如果在这种情况下啊,两个函数有定义且适用,编译器会报错。一种可行的解决方法是, 不要使用运算符,使用普通函数调用. 另外一个办法是,用val替换var, 这样plusAssign运算就不再适用。
但是一般来说,最好一致地设计出新的类:尽量不要同时给一个类添加plus和plusAssign运算. 如果像前面一个示例中的Point, 这个类是不可变的,那么只需要提供plusAssign和类似的运算就够了.
kotlin标准库支持集合的这两种方法。+和-运算符总是返回一个新的集合。+=和-=运算符用于可变集合时,始终在一个地方修改它们。
下面来看一个栗子

>>> val list = arrayListOf(1,2)
>>> list += 3               //+=修改”list"
>>> val newList = list + listof(4,5)    //+返回一个包含所有元素的新列表
>>> println(list)
[1,2,3]     
>>> println(newList)
[1,2,3,4,5]

重载一元运算符

重载一元运算符的过程与你在前面看到的方式相同:用预先定义的一个名称来声明函数(成员函数或扩展函数),并用operator标记。下面举个栗子

operator fun Point.unaryMinus():Point{
    return Point(-x,-y)
}
>>>val p = Point(10,20)
>>>println(-p)
Point(x=-10,y=-20)

可以用于重载的一元算法的运算符
+a unaryPlus
-a unaryMinus
!a not
++a,a++ inc
–a,a— dec

重载比较运算符

与算术运算符一样,在Kotlin中,可以对任何对象使用比较运算符(,!=,>,<等),而不仅仅限于基本数据类型。不用像Java那样调用equals或compareTo函数。
等号运算符:”equals”
在kotlin中使用
运算符,它将被转换成equals方法调用. 这只是我们要讨论的约定原则中的一个。使用!=运算符也会被转换成equals函数调用,明显的差异在于,它们的结果是相反的。注意,和所有其他运算符不同的是,==和!=可以用于可空运算数,因为这些运算符事实上会检查运算数是否为null.比较 a == b会检查a是否为非空,如果不是,就调用a.equals(b) : 否则,只有两个参数都是空引用,结果才是true
a == b -> a?.equals(b) ?: (b == null)
下面我们来重写equals函数

class Point(val x : Int, val 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
    }
}
>>> println(Point(10,20) == Point(10,20))
true
>>> println(Point(10,20) != Point(5,5))
true
>>> println(null == Point(1,2))
false

kotlin中的=为恒等运算符(=)来检查参数与调用equals的对象是否相同。恒等运算符与Java中的==运算符是完全相同的: 检查两个参数是否是同一个对象的引用(如果是基本数据类型,检查他们是否是相同的值). 注意 === 运算符不能被重载.
排序运算符:compareTo
通常在Java中实现Comparable接口,来自定义两个对象之前通过调用compareTo的比较逻辑,注意必须明确写为element1.compareTo(element2).
在Kotlin中实现comparable接口后,比较运算符(<,>,<=和>=)的使用讲转换成compareTo. compareTo的返回类型必须为Int. p1 < p2表达式等价于p1.compareTo(p2) < 0.
下面举个栗子

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(“Alice”,”Smith”)
>>> val p2 = Person(“Bob”,”Johnson”)
>>> println(p1 < p2)
false 

另外所有Java实现了Comparable接口的类,都可以在Kotlin中使用简洁的运算符语法,不用再增加扩展函数.

集合与区间的约定

集合操作中最常见的就是通过下标获取和设置元素,以及检查元素是否属于当前集合。 所有的这些操作都支持运算符语法ab。可以使用in运算符来检查元素是否在集合或区间内,也可以迭代集合.
通过下标访问元素 : “get” 和 “set"
在kotlin中,可以用Java中数组的方式来访问map中的元素-使用方括号:

val value = map[key]

也可以用同样的运算符来改变一个可变map的元素:

mutableMap[key] = newValue

在Kotlin中下标运算符是一个约定。使用下标运算符读取元素被转换成get运算符方法的调用,并且写入元素将调用set. Map和MutableMap的接口已经定义了这些方法。
给自己的类添加类似的方法.

operator fun Point.get(index : Int) : Int{
    return when(index){
        0 -> x
        1 -> y
        else ->
            throw IndexOutOfBoundsException(“Invalid coordinate $index")
    }
}
>>> val p = Point(10,20)
>>> println(p[1])

只需要定义一个名为get的函数,并标记operator. 向p[1]这样将被转换为get方法的调用.

x[a,b] -> x.get(a,b)

注意,get参数可以是任意的类型,而不只是Int. 例如,当你对map使用下标运算符时,参数类型是键的类型,它可以是任意类型。还可以定义多个参数的get方法. 例如,如果要实现一个类来表示二维数组或矩阵,可以定义一个方法

operator fun get(rowIndex : Int, colIndex : Int),

然后matrix[row , col] 来调用. 另外get方法也支持重载使用不同的键类型访问集合.
我们可以重写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[1] = 42
>>> println(p)
MutablePoint(x = 10 , y = 42)

”in”的约定
集合支持另外一个运算符就是in运算符,用于检查某个对象是否属于集合。相应的函数叫做contains.

data class Rectangle(val upperLeft : Point , val lowerRight : Point)
operator fun Rectangle.contains(p : Point) : Boolean{
    return p.x in upperLeft.x until lowerRight.x &&
         p.y in upperLeft.y until lowerRight.y 
}
>>> val rect = Rectangle(Point(10,20),Point(50,50))
>>> println(Point(20,30) in rect)
true
>>> println(Point(5,5) in rect)
false

这里需要值得注意的是until是表示一个开区间,10 until 20 包含从10到19的数字,但不含20,闭区间用10…20表示.
rangeTo的约定
要创建一个区间,请使用…语法: 举个例子, 1…10 代表有从1到10的数字,现在来说说创建它的约定.
…运算符是调用rangeTo函数的一个简洁方法
start … end -> start.rangeTo(end)
rangeTo函数返回一个区间。你可以为自己的类定义这个运算符。但是如果该类实现了Comparable接口,那么不需要了: 因为这个库定义了可以用于任何比较元素的rangeTo函数

operator fun <T: Comparable<T>> T.rangeTo(that : T) : CloseRange<T>

这个函数返回一个区间用来检查其他一些元素是否属于它, 下面用LocalData举个例子

>>> val now = LocalDate.now()
>>> val vacation = now..now.plusDays(10)
>>> println(now.plusWWeeks(1) in vacation)

now…now.plusDays(10) 表达式将会被编译器转换为now.rangeTo(now.plusDays(10)). rangeTo并不是LocalDate的成员函数,而是Comparable的一个扩展函数.
rangeTo运算符优先级低于算术运算符,不过作为一个良好的编码习惯,通常也用括号括起来以免混淆

>>> val n = 9
>>> println(0..(n+1))
0..10

在”for”循环中使用”iterator”的约定
kotlin中for循环使用in运算符来执行迭代。这意味着一个诸如for(x in list){…}将被转换成list.iterator()的调用,然后就想在Java中一样,重复调用hasNext和next方法.

解构声明和组件函数

这个功能允许展开单个复合值,并使用它来初始化多个单独变量.

>>> val p = Point(10,20)
>>> val (x,y) = p
>>> println(x)
10
>>> println(y)
20

要在解构声明中初始化每个变量,将调用名为componentN的函数,其中N是声明变量的位置。

val (a,b) = p 
-> val a = p.component1()
-> val b = p.component2()
class Point(val x : Int, val y : Int){
    operator fun component1() = x
    operator fun component2() = y
}

解构声明主要使用场景之一,是从一个函数返回多个值,这个非常有用。 举个例子,编写一个简单函数,来将一个文件名分割成名字和扩展名.

data class NameComponents(val name : String, val extension : String)
fun splitFilename(fullName : String) : NameComponents{
    val result = fullName.split(.,limit = 2)
    return NameComponents(result[0],result[1])
}
>>> val (name,ext) = splitFilename(“example.kt”)
>>> println(name)
example
>>> println(ext)
kt

componentN函数在数组和集合上也有定义,可以进一步改进这个代码。下面使用解构声明来处理集合

data class NameComponents(val name : String, val extension : String)
fun splitFilename(fullName : String) : NameComponents{
    val(name,extension) = fullName.split(.,limit = 2)
    return NameComponents(name, extension)
}

解构声明和循环
解构声明还可以用于in循环,一个例子,是枚举map中的条目. 下面是一个小例子,使用这个语法打印给定map中的所有条目

fun printEntries(map : Map<String,String>){
    for((key,value) in map){
        println(“$key -> $value")
    }
}
>>> val map = mapOf(“Oracle” to “Java” , “JetBrains” to “Kotlin”)
>>> printEntries(map)
Oracle -> Java
JetBrains -> Kotlin

重用属性访问的逻辑:委托属性

委托属性的基本语法:

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

属性p将它的访问器逻辑委托给了另一个对象:这里是Delegate类的一个新的实例。
编译器创建一个隐藏的辅助属性,并使用委托对象的实例进行初始化,初始属性p会委托给该实例。为了简单起见,我们把它称为delegate:

class Delegate{
    operator fun getValue() {}
    operator fun setValue(,value : Type){...}
}
class Foo{
    var p : Type by Delegate()
}
>>> val foo = Foo()
>>> val oldValue = foo.p
>>> foo.p = newValue

使用委托属性:惰性初始化和”by lazy()”
惰性初始化是一种常见的模式,直到第一次访问该属性的时候,才根据需要创建对象的一部分。
举个栗子,一个Person类,可以用来访问一个人写的邮件列表。邮件存储在数据库中,访问比较耗时。你希望只有在首次访问时才加载邮件,并只执行一次。假设你已经有函数loadEmails,用来从数据库中检查电子邮件:

class Email{ /* … */}
fun loadEmails(person : Person) : List<Email>{
    println(“Load emails for ${person.name}")
}

下面展示如何使用额外_emailds属性来实现惰性加载,在没有加载之前为null, 然后加载为邮件列表.

class Person(val name : String){
    private var_emails : List<Email>? = null

    val emails : List<Email>
        get(){
            if(_emails == null){
                _emails = loadEmails(this)
            }
            return _emails!!
        }
}
>>> val p = Person(“Alice”)
>>> p.emails
Load emails for Alice
>>> p.emails

这里使用了所谓的支持属性技术。你有一个属性,_emails, 用来存储这个值,而另一个emails, 用来提供属性的读取访问.
但是上面这个代码有点啰嗦:要是有几个惰性属性那得有多长。而且,它并不总是正常运行:这个实现不是线程安全。使用委托属性会让代码变得简单很多,可以用于封装存储值的支持和确保该值只被初始化一次的逻辑。在这里可以使用标准函数lazy返回的委托.

class Person(val name : String){
    val emails by lazy{ loadEmails(this) }
}

lazy的参数是一个lambda,可以调用它来初始化这个值。

委托属性的原理

在java中存在一个PropertyChangeSupport类用来监听属性的变化. 这意味着当属性发生变化的时候会收到相应的通知,来看看下面示例:

public class SomeBean {
    private String property;
    private PropertyChangeSupport changeSupport;
    public void setProperty(String newValue) {
        String oldValue = property;
        property = newValue;
        changeSupport.firePropertyChange("property", oldValue, newValue);
    }

    public void addPropertyChangeListener(PropertyChangeListener l) {
        changeSupport.addPropertyChangeListener(l);
    }

    public void removePropertyChangeListener(PropertyChangeListener l) {
        changeSupport.removePropertyChangeListener(l);
    }
}

这意味着当调用setProperty后会通知addPropertyChangeListener中的PropertyChangeListener. 而在kotlion我们也可以利用该特性来实现属性修改的通知

open class PropertyChangeAware{
    protected val changeSupport = PropertyChangeSupport(this)

    fun addPropertyChangeListener(listener: PropertyChangeListener){
        changeSupport.addPropertyChangeListener(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener){
        changeSupport.removePropertyChangeListener(listener)
    }
}
class Person_ONE(val name : String, age : Int, salary : Int) : PropertyChangeAware(){
    var age : Int = age
        set(newvalue){
            val oldValue = field
            field = newvalue
            changeSupport.firePropertyChange("age",oldValue,newvalue)
        }
    var salary : Int = salary
        set(newvalue){
            val oldValue = field
            field = newvalue
            changeSupport.firePropertyChange("salary",oldValue,newvalue)
        }
}
fun main(args: Array<String>) {
    val p = Person_ONE("Dmitry",34,2000)
    p.addPropertyChangeListener(
            PropertyChangeListener { evt: PropertyChangeEvent? ->
                println("Property ${evt?.propertyName} changed "+ "from ${evt?.oldValue} to ${evt?.newValue}")
            }
    )
    p.age = 35
    p.salary = 2100
}
>>>
Property age changed from 34 to 35
Property salary changed from 2000 to 2100
>>>

另外也可以利用辅助类实现上面的属性修改的通知

open class ObservableProperty(val propNmae : String, var propValue : Int, val changeSupport: PropertyChangeSupport){
    fun getValue():Int = propValue
    fun setValue(newvalue: Int){
        val oldValue = propValue
        propValue = newvalue
        changeSupport.firePropertyChange(propNmae,oldValue,newvalue)
    }
}
class Person_TWO(val name : String, age : Int, salary : Int) : PropertyChangeAware(){

    val _age = ObservableProperty("age",age,changeSupport)

    var age : Int
        get() = _age.getValue()
        set(value){
            _age.setValue(value)
        }
    val _salary = ObservableProperty("salary",salary,changeSupport)
    var salary : Int
        get() = _salary.getValue()
        set(value) {_salary.setValue(value)}
}

可以看到你需要非常多的样板,但是Kotlin的属性功能可以让你摆脱这些样板代码。但是在此之前你需要更改ObservableProperty方法的签名,来匹配Kotlin约定所需的方法.
下面来看看ObservableProperty来作为属性委托

class ObservableProperty(var propValue : Int, val changeSupport: PropertyChangeSupport){
    operator fun getValue(p : Person, prop : KProperty<*>) : Int = propValue
    operator fun setValue(p : Person, prop : KProperty<*> , newValue : Int){
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(prop.name,oldValue,newValue)
    }
}

下面可以见识kotlin委托属性的神奇了.来看看代码变短多少?

class Person(val name : String, age : Int, salary : Int) : PropertyChangeAware(){
    var age : Int by ObservableProperty(age,changeSupport)
    var salary : Int by ObservableProperty(salary,changeSupport)
}

by后面的对象称为委托。Kotlin会自动将委托存储在隐藏属性中,并在访问或修改属性时调用委托的getValue和setValue.
在kotlin中你并需要手动去实现一个ObservableProperty,你只需要传递一个lambda,来告诉它如何通知属性值的更改.

class Person_Four(val name : String, age: Int, salary: Int) : PropertyChangeAware(){
    private val observer = {
        prop : KProperty<*>, oldValue : Int, newValue : Int ->
        changeSupport.firePropertyChange(prop.name,oldValue,newValue)
    }
    var age : Int by Delegates.observable(age,observer)
    var salary : Int by Delegates.observable(salary,observer)
}

by右边的表达式不一定是新创建的实例,也可以是函数调用,另一个属性或其它表达式。

委托属性的变换规则

class C{
    var prop : Type by MyDelegate()
}
val c = C()

MyDelegate实例会被保存到一个隐藏的属性中,它被称为. 编译器也将用一个KProperty类型的对象来代表这个属性,它被称为.
编译器生成的代码如下:

class C{
    private val <delegate> = MyDelegate()
    var prop : Type
        get() = <delegate>.getValue(this,<property>)
        set(value : Type) = <delegate>.setValue(this, <property>, value)
}

在map中保存属性值

委托属性发挥作用的另一种常见用法,是用在有动态定义的属性集的对象中。

class Person{
    private val _attributes = hashMapOf<String,String>()
    fun setAttribute(attrName : String, value : String){
        _attributes[attrName] = value
    }

    val name : String
        get() = _attributes["name"]!!
}
>>> val p = Person()
>>> val data = mapOf("Oracle" to "Java" , "company" to "JetBrains")
>>> for ((attrName,value) in data){
>>>    p.setAttribute(attrName,value)
>>> }
>>> println(p.name)
Dmitry

使用委托属性把值存到map中

class Person{
    private val _attributes = hashMapOf<String,String>()
    fun setAttribute(attrName : String, value : String){
        _attributes[attrName] = value
    }
    val name : String by _attributes
}

小结:

  1. Kotlin允许使用对应名称的函数来重载一些标准的数学运算,但是不能定义自己的运算符
  2. 比较映射为equals和compareTo方法的调用
  3. 通过定义名为get,set和contains的函数,就可以让你自己的类与Kotlin的集合一样,使用[]和in运算符
  4. 可以通过约定来创建区间,以及迭代集合和数组
  5. 解构声明可以展开单个对象用来初始化多个变量,这可以方便地用来从函数返回多个值。它们可以自动处理数据类, 可以通过给自己的类定义名为componentN的函数
  6. 委托属性可以用来重用逻辑, 这些逻辑控制如何存储,初始化,访问和修改属性值,这是用来构建框架的一个强大的工具
  7. lazy标准库函数提供了一种实现惰性初始化属性的简单方法
  8. Delegates.observable 函数可以用来添加属性更改的观察者
  9. 委托属性可以使用任意map来作为委托属性委托,来灵活处理具有可变属性集的对象

最后

在现在这个金三银四的面试季,我自己在网上也搜集了很多资料做成了文档和架构视频资料免费分享给大家【包括高级UI、性能优化、架构师课程、NDK、Kotlin、混合式开发(ReactNative+Weex)、Flutter等架构技术资料】,希望能帮助到您面试前的复习且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

资料获取方式:加入Android架构交流QQ群聊:513088520 ,进群即领取资料!!!

点击链接加入群聊【Android移动架构总群】:加入群聊

资料大全

猜你喜欢

转载自blog.csdn.net/weixin_43351655/article/details/89475466
今日推荐