博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Kotlin运算符重载及其他约定
阅读量:6984 次
发布时间:2019-06-27

本文共 14406 字,大约阅读时间需要 48 分钟。

重载算数运算符

下面以一个栗子开始,我们先定义一个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 timesa / b diva % b moda + b plusa - 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
()>>> numbers += 42>>> println(numbers[0])复制代码

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

operator fun
MutableCollection
.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
{ 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中使用简洁的运算符语法,不用再增加扩展函数.

集合与区间的约定

集合操作中最常见的就是通过下标获取和设置元素,以及检查元素是否属于当前集合。 所有的这些操作都支持运算符语法a。可以使用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.rangeTo(that : T) : CloseRange
复制代码

这个函数返回一个区间用来检查其他一些元素是否属于它, 下面用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
){ for((key,value) in map){ println(“$key -> $value") }}>>> val map = mapOf(“Oracle” to “Java” , “JetBrains” to “Kotlin”)>>> printEntries(map)Oracle -> JavaJetBrains -> 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
{ println(“Load emails for ${person.name}")}复制代码

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

class Person(val name : String){    private var_emails : List
? = null val emails : List
get(){ if(_emails == null){ _emails = loadEmails(this) } return _emails!! }}>>> val p = Person(“Alice”)>>> p.emailsLoad 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
) { 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 35Property 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 
= MyDelegate() var prop : Type get() =
.getValue(this,
) set(value : Type) =
.setValue(this,
, value)}复制代码

在map中保存属性值

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

class Person{    private val _attributes = hashMapOf
() 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
() 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来作为委托属性委托,来灵活处理具有可变属性集的对象

转载于:https://juejin.im/post/5cbecc296fb9a0324c20bceb

你可能感兴趣的文章
COMPUTER HARDWARE OPENCART 主题模板 ABC-0059
查看>>
android listview item点击时更改textview的颜色 代码中实现
查看>>
How to install Docker on Ubuntu
查看>>
EXTjs
查看>>
开启win7 FTP 服务 无法登陆的原因
查看>>
SSO之CAS单点登录详细搭建
查看>>
开发自定义JSF组件(4) 保存状态与恢复状态
查看>>
ZBarSDK扫描二维码
查看>>
Windows的Win键被自动按下解决方案
查看>>
lucene4.7 分页(五)
查看>>
MyEclipse_15字体设置
查看>>
PHP中处理函数的函数(Function Handling Functions)
查看>>
href="#"与javascript:void(0)的区别
查看>>
web端即时通讯
查看>>
Python xlrd 读取xls文件
查看>>
Netflix Zuul与Nginx的性能对比
查看>>
【算法学习】枚举与剪枝(一)
查看>>
python 监控jvm脚本
查看>>
1.C#简介
查看>>
一致性哈希算法
查看>>