Kotlin核心语法(五):运算符重载以及其它的约定

博客主页java

java在标准库中有一些与特定的类相关联的语言特性,如实现了java.lang.Iterable接口的对象能够在for循环中使用,实现了java.lang.AutoCloseable接口的对象能够在try-with-resources语句中使用。算法

但在kotlin中,一些功能是与特定的函数名相关,而不是与特定的类型绑定。kotlin使用约定的原则,不像java依赖类型。kotlin能够经过扩展函数机制来为现有的类增添新的方法,能够把任意约定方法定义为扩展函数。数据库

一. 重载算术运算符

java中,算术运算符只能用于基本数据类型,+运算符能够与String值一块儿使用。若是给集合添加元素时,想要可以用 += 运算符就完美。在kotlin中,是能够这样作的。segmentfault

1. 重载二元算术运算

先来看一个例子:定义Point类(表明一个点),把点的(X, Y)坐标分别加到一块儿。数组

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

    // 定义一个名为 "plus" 的方法
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

val p1 = Point(1, 2)
val p2 = Point(3, 4)
println(p1 + p2) // 经过使用 + 号 来调用 "plus" 方法
//输出结果>>> Point(x=4, y=6)

operator关键字声明plus函数。全部的重载运算符函数都须要使用该关键字标记,表示这个函数做为约定实现。安全

使用operator修饰符声明plus函数后,能够直接使用 + 号来求和。其实就是调用plus函数。app

除了能够把运算符声明为一个成员函数外,还能够把它定义为一个扩展函数ide

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

val p1 = Point(10, 20)
println(p1 * 1.5) // 不会自定支持交换性,不能 1.5 * p1
//输出结果>>> Point(x=15, y=30)

kotlin运算符不会自定支持交换性,不能 1.5 * p1。若是但愿能够,须要单独定义一个运算符

operator fun Double.times(p: Point): Point {...}

运算符函数的返回类型也能够是任意一个运算数类型。
这个运算符,接收一个Char做为左值,Int做为右值,而后返回一个String类型。

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

println('b' * 3)
//输出结果>>> bbb

2. 重载复合赋值运算符

+= 、-=等这些运算符称为复合赋值运算符。

var p = Point(1, 2)
p += Point(3, 4) // 等同于 p = p + Point(3, 4)写法
println(p)

//输出结果>>> Point(x=4, y=6)

+=运算符能够修改变量所引用的对象,但不会从新分配引用,如:将一个元素添加到可变集合中

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

//输出结果>>> 12

若是定义了一个返回值为Unit,名为plusAssign函数,kotlin会在用到 += 运算符的地方调用它。二元运算符对应函数,如:minusAssign、timesAssign

kotlin标准库为可变集合定义了plusAssign函数:

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

在代码中使用 += 时,理论上 plus 和 plusAssign都有可能被调用,因此尽可能不要同时给一个类添加 plus 和 plusAssign 运算。

如:例子中的Point类,是一个不可变的,那么应该只提供返回一个新值plus运算,若是一个类是可变的,那么只须要提供plusAssign和相似的运算。

kotlin标准库支持集合的两种方法,+ 和 - 运算符老是返回一个新的集合,+= 和 -= 运算符用于可变集合时,始终在一个地方修改它们。而用于只读集合时,返回一个修改过的副本,意味着只有当引用只读集合的变量声明为var时,才能使用+=和-=。

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]

3. 重载一元运算符

预先定义一个名称来声明函数(成员函数或者扩展函数),并用修饰符operator标记。

// 一元运算符无参数
operator fun Point.unaryMinus(): Point {
    return Point(-x, -y) // 坐标取反,而后返回
}

可重载的一元算法的运算符:

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

自增运算符案例:

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

var bd = BigDecimal.ZERO
println(bd++) //后缀运算:在执行后增长(先返回bd变量当前值,而后执行++)
//输出结果>>> 0

println(++bd) //前缀运算:在执行前增长(与后缀运算相反)
//输出结果>>> 2

二. 重载比较运算符

在kotlin中,能够对于任何对象使用比较运算符(==、!=、>、< 等),不只仅限于基本数据类型,能够直接使用比较运算符。不像java须要调用equals或者compareTo函数。

1. 等号运算符:"equals"

若是在kotlin中使用 == 运算符,会将被转换成equals方法的调用。

== 和 != 能够用于可空运算符,由于这些运算符事实上会检查运算数是否为null。比较 a == b 会检查a是否为非空,若是不是,就调用a.equals(b),不然,只有两个参数都是空引用,结果才是true

案例中Point类,被标记为数据类(data),equals的实现会由编译器自动生成。若是须要手动实现,以下:

class Point(val x: Int, val y: Int) {
    override fun equals(other: Any?): Boolean {
        // 优化:检查参数是否与this是同一个对象
        if (this === other) return true
        // 检查参数类型
        if (other !is Point) return false
        // other智能转换为Point来访问x,y属性
        return other.x == x && other.y == y
    }
}

println(Point(1, 2) == Point(1, 2)) //输出结果>>> true
println(Point(2, 3) != Point(3, 4)) //输出结果>>> true
println(null == Point(1, 2)) //输出结果>>> false

恒等运算符(===)来检查两个参数是不是同一个对象的引用(若是是基本数据类型,检查是不是相同的值)。在实现了equals函数后,一般使用这个(===)运算符来优化调用代码,可是===运算符不能被重载。

equals方法是在Any类中定义的,因此equals方法不须要标记为operator,由于Any类中基本方法已经标记了。可是equals不能实现为扩展方法,由于继承自Any类的实现始终优先于扩展函数。

public open class Any { 
    // ...
    public open operator fun equals(other: Any?): Boolean
}

!=运算符也会转换为equals方法调用,编译器会自动对返回值取反。

2. 排序运算符:compareTo

在java中,类能够实现Comparable接口,接口中定义的compareTo方法用于肯定一个对象是否大于另外一个对象。可是在java中,只有基本数据类型可使用< 和 > 来比较,其它类型都须要element1.compareTo(element2)。

而在kotlin中,可使用比较运算符(< 、> 、<=、>=),会被转换为compareTo,compareTo的返回类型必须为Int。

定义Person类实现compareTo方法:先比较firstName,若是相同,再比较lastName

class Person(
    val firstName: String, val lastName: String
) : Comparable<Person> {
    override fun compareTo(other: Person): Int {
        return compareValuesBy( // 按顺序调用给定的方法,并比较它们的值
            this, other,
            Person::firstName, Person::lastName
        )
    }
}

val p1 = Person("a", "b");
val p2 = Person("a", "c");
println(p1 < p2)
//输出结果>>> true

可使用kotlin标准库中的compareValuesBy函数来简洁地实现compareTo方法。全部java中实现了Comparable接口的类,均可以在kotlin使用简洁的运算符语法,不用再增长扩展函数。如:

println("abc" < "cba")
//输出结果>>> true

三. 集合与区间的约定

集合的操做一般都是经过下标。kotlin中全部这些操做都支持运算符语法:经过下标获取或者设置元素,可使用语法a[b](称为下标运算符);可使用in运算符来检查元素是否在集合区间内,也能够迭代集合。

1. 经过下标来访问元素:“get”和“set”

kotlin中,访问map中元素,能够经过方括号的方式:

val value = map[key]

也能够用一样的运算符来改变一个可变map的元素

mutable[key] = newValue

如何工做的呢?
在kotlin中,下标运算符是一个约定。使用下标运算符读取元素会被转换为get运算符方法的调用,写入元素调用set。Map和MutableMap的接口都已经定义了这些方法。

如何给自定义的类添加相似的方法呢?

实现get约定:仍是以自定义Point类为例,使用方括号来引用点的坐标,p[0]访问X坐标,p[1]访问Y坐标

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])
//输出结果>>> 20

只须要定义一个get函数,并标记operator后,p[1]就会被转换为get方法的调用。

注意:get的参数能够是任意类型,而不仅是Int。还能够定义具备多个参数的get方法。若是须要使用不一样的健类型访问集合,也可使用不一样的参数类型定义多个重载的get方法。

实现set约定:上例中Point类是不可变的(变量是val修改),因此实现set约定没有意义。
接下来定义一个可变的点MutablePoint

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, 23)
p[1] = 24
println(p)
//输出结果>>> MutablePoint(x=10, y=24)

只须要定义一个set函数,并标记operator后,p[1]=24就会被转换为set方法的调用。

2. "in"的约定

集合支持的另外一个运算符是in运算符:用来检查某个对象是否属于集合,对于的函数是contains。

实现in的约定:检查点是否属于一个矩形

data class Rectangle(val upperLeft: Point, val lowerRight: Point)

operator fun Rectangle.contains(p: Point): Boolean {
    // 使用until函数来构建一个区间
    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

in右边的对象将会调用contains函数,in左边的对象将会做为函数入参。

3. rangeTo的约定

建立一个区间,使用 .. 语法。如:1..10表示从1到10的数字。 ..运算符是调用rangeTo函数的一个简洁方法。

rangeTo函数返回一个区间。能够为自定义的类定义这个运算符,可是若是该类实现了Comparable接口,就不须要了。能够经过kotlin标准库建立一个任意可比较元素的区间:

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

例如:

val now = LocalDate.now();
val vacation = now..now.plusDays(10) // 建立一个从今天开始的10天的区间
println(now.plusWeeks(1) in vacation) // 检测一个特定的日期是否属于这个区间
//输出结果>>> true

now..now.plusDays(10)会被编译器转换为now.rangeTo(now.plusDays(10))。其中rangeTo并非LocalDate的成员函数,而是Comparable的一个扩展函数。

rangeTo运算符的优先级低于算术运算符,最好把参数扩起来以避免混淆:

val n = 9
println(1..(n + 1)) // 能够写成1..n + 1,但括起来更清晰一点
//输出结果>>> 1..10

表达式1..n.forEach { print(it) }不会被编译,必须把区间表达式括起来才能调用forEach方法

val n = 9
(1..n).forEach { print(it) }
//输出结果>>> 123456789

4. 在"for"循环中使用"iterator"的约定

在kotlin中,for循环中也可使用in运算符,和作区间检查同样。可是在这种状况下它的含义是不一样的:它被用来执行迭代。如:for(x in list) {...} 将被转换成list.iterator()的调用。

在kotlin中,iterator方法能够被定义为扩展函数,因此能够遍历一个常规的java字符串,标准库已经为CharSequence定义了一个扩展函数iterator

operator fun CharSequence.iterator(): CharIterator

for(c in "abc"){}

能够为自定义的类定义iterator方法:实现日期区间的迭代器

operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
    // 这个对象实现了遍历LocalDate元素的Iterator
    object : Iterator<LocalDate> {
        var current = start

        // 日期用到了compareTo约定
        override fun hasNext() =
            current <= endInclusive

        // 在修改前返回当前日期做为结果
        override fun next() = current.apply {
            // 把当前日期增长一天
            current = plusDays(1)
        }
    }

val newYear = LocalDate.ofYearDay(2017, 1)
val daysOff = newYear.minusDays(1)..newYear

for (dayOff in daysOff) { println(dayOff) }
//输出结果>>> 2016-12-31
//          2017-01-01

四. 解构声明和组件函数

相信你们对数据类已经很熟悉了。

接下来解构声明,它是怎么工做的?

// 数据类
data class Point(val x: Int, val y: Int) 

val p = Point(10, 20)
val (x, y) = p  // 声明变量x,y,而后用p的组件来初始化
println(x)
//输出结果>>> 10

println(y)
//输出结果>>> 20

解构声明就像普通的变量声明,但它在括号中有多个变量。

解构声明也用到了约定原理。要在解构声明中初始化每一个变量,将调用名为componentN的函数,其中N是声明中变量的位置。

对于数据类,编译器为每一个在主构造方法中声明的属性生成一个componentN函数。

咱们也能够手动为非数据类型声明这些功能:

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函数在数组和集合中也有定义。当已知大小的集合时,可使用解构声明来处理集合。
改造一下splitFilename函数:

fun splitFilename(fullName: String): NameComponents {
    val (name, ext) = fullName.split(".", limit = 2)
    return NameComponents(name, ext)
}

componentN在标准库只容许使用此语法来访问一个对象的前五个元素。

接收一个函数返回多个值,可使用标准库中的 PairTriple 类。

1. 解构声明和循环

解构声明不只能够用做函数中的顶层语句,还能够在其它能够声明变量的地方,如:in 循环

fun printEntries(map: Map<String, String>) {
    // 在in 循环中用解构声明
    for ((key, value) in map) {
        println("$key -> $value")
    }
}

val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
printEntries(map)
//输出结果>>> Oracle -> Java
//          JetBrains -> Kotlin

其中Map.Entry上扩展函数component1和component2,分别返回它们的健和值

for (entry in map.entries) {
    val key = entry.component1()
    val value = entry.component2()
    // ...
}

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

1. 委托属性的基本操做

委托属性的基本语法:

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

属性p将它的访问器逻辑委托给了另外一个对象,这里是Delegate类的一个新的实例。经过关键字by对其后的表达式求值来获取这个对象。

编译器建立一个隐藏的辅助属性,并使用委托对象的实例进行初始化,初始化属性p会委托给实例

class Foo {
   // 编译器会自动生成一个辅助属性
   private val delegate = Detegate()
   // p的访问都会调用对应的delegate的getValue和setValue
   var p: Type
      set(value: Type) = delegate.setValue(...,value)
      get() = delegate.getValue(...)
}

Detegate类必须具备setValue和getValue方法,能够是成员函数,也能够是扩展函数。

class Detegate {
   // getValue包含了实现getter的逻辑
   operator fun getValue(...) {...}
   // setValue包含了实现setter的逻辑
   operator fun setValue(..., value: Type) {...}
}

class Foo {
  // 关键字by把属性关联上委托对象
  var p: Type by Delegate()
}

val foo = Foo()
val oldValue = foo.p // 经过调用delegate.getValue(...)来实现属性的修改
foo.p = newValue // 经过调用delegate.setValue(..., newValue)来实现属性的修改

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

惰性初始化:当第一次访问该属性的时候,才根据须要建立对象的一部分。

例如:一个Person类,用来访问一我的写的邮件列表。邮件存储在数据库中,访问耗时。可是只但愿在首次访问时才加载邮件,并只执行一次

class Person {
    // _emails属性用来保存数据,关联委托
    private var _emails: List<String>? = null

    val emails: List<String>
        get() {
            if (_emails == null) {
                // 访问时加载邮件
                _emails = loadEmails();
            }
            // 若是已经加载,直接返回
            return _emails!!
        }

    private fun loadEmails(): List<String>? {
        // 耗时
        return listOf("1", "2");
    }
}

val p = Person()
println(p.emails)
//输出结果>>> [1, 2]

若是有几个属性怎么办呢?且这个实现也不是线程安全的。kotlin提供了更好的解决方案:
使用委托属性会让代码变得简单,能够封装用于存储值的支持属性和确保该值只被初始化一次的逻辑。
可使用标准库函数lazy返回委托

使用委托属性来实现惰性初始化:

class Person {
    val emails by lazy { loadEmails() }
}

lazy函数返回一个对象,该对象具备一个名为getValue且签名正确的方法,所以能够把它与by关键字一块儿使用来建立一个委托属性。默认状况下,lazy函数是线程安全的。

3. 实现委托属性

在java中当一个对象的属性发生更改时通知监听器,具备用于此类通知的标准机制:PropertyChangeSupport和PropertyChangeEvent类。可是在kotlin不使用属性委托,怎么实现的呢?

PropertyChangeSupport类维护了一个监听器列表,并向它们发送PropertyChangeEvent事件。要使用它,一般须要把这个类的一个实例存储为bean类的一个字段,并将属性更改的处理委托给它。

为了不在每一个类中都建立这个字段,建立一个工具类,而后bean类继承这个工具类。

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

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

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

写一个Person类,定一个只读属性name和一个可写属性age,当age发生改变时,通知它的监听器

class Person(
    val name: String,
    age: Int
) : PropertyChangeAware() {

    var age: Int = age
        set(newValue) {
            // field标识符容许访问属性背后支持字段
            val oldValue = field
            field = newValue
            // 当属性变化时,通知监听器
            changeSupport.firePropertyChange("age", oldValue, newValue)
        }
}

val p = Person("kerwin", 30)
p.addPropertyChangeListener(PropertyChangeListener { event ->
    println("Property ${event.propertyName} change from ${event.oldValue} to ${event.newValue}")
})

p.age = 31;
//输出结果>>> Property age change from 30 to 31

接下来经过辅助类实现属性变化的通知

class ObservableProperty(
    val propertyName: String,
    var propertyValue: Int,
    val changeSupport: PropertyChangeSupport
) {
    fun getValue() = propertyValue
    fun setValue(newValue: Int) {
        val oldValue = propertyValue
        propertyValue = newValue
        changeSupport.firePropertyChange(propertyName, oldValue, newValue)
    }
}

class Person(
    val name: String,
    age: Int
) : PropertyChangeAware() {

    val _age = ObservableProperty("age", age, changeSupport)
    var age: Int
        get() = _age.getValue()
        set(newValue) = _age.setValue(newValue)
}

这样咱们仍是须要为每一个属性建立ObservableProperty实例,并把setter和getter委托给它。kotlin中的委托功能不用这样写,可是须要更改下ObservableProperty方法的签名,匹配kotlin约定所需的方法

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

ObservableProperty这个类作了更改的地方:

  • getValue和setValue函数都被标记了operator
  • 这些函数增长了两个参数:一个用于接收属性的实例,用来设置或读取属性;另外一个用于表示属性自己,这个属性类型为KProperty
  • 把propertyName属性从主构造中移除

而后使用委托属性来绑定更该通知:

class Person(
    val name: String,
    age: Int
) : PropertyChangeAware() {

    var age: Int by ObservableProperty(age, changeSupport)
}

经过by关键字,kotlin编译器会自动执行以前手动编写的代码。右边的对象被称为委托。kotlin会自动将委托存储在隐藏的属性中,并在访问或修改属性时调用委托的getValue和setValue。

你不用手动去实现可观察的属性逻辑。kotlin标准库中已经包含相似ObservableProperty的类。标准库与PropertyChangeSupport类没有耦合。

使用Delegates.observable来实现属性修改的通知:

class Person(
    val name: String,
    age: Int
) : PropertyChangeAware() {

    private val observer = { property: KProperty<*>, oldValue: Int, newValue: Int ->
        changeSupport.firePropertyChange(property.name, oldValue, newValue)
    }
    var age: Int by Delegates.observable(age, observer)
}

by右边的表达式不必定是新建立的实例。也能够是函数调用,另外一个属性或者任何其它表达式。只要这个表达式的值,是可以被编译器用正确的参数类型来调用getValue和setValue的对象。

4. 委托属性的变化规则

接下来总结一下委托属性是怎么工做的?
假设有一个委托属性的类:

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

MyDelegate实例会被保存到一个隐藏的属性中,它被称为<delegate>。编译器也将用一个KProperty类型的对象来表明这个属性,它被称为<property>

class C {
  private val <delegate> = MyDelegate()

  val prop: Type
     get() = <delegate>.getValue(this, <property>)
     set(value: Type) = <delegate>.setValue(this, <property>, value)
}

5. 在map中保存属性值

委托属性另外一种常见用法,是用在有动态定义的属性集的对象中,这种对象有时被称为自订对象。

举一个例子:定义一个属性,把值存到map中

class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, arrtValue: String) {
        _attributes[attrName] = arrtValue
    }

    val name: String
        get() = _attributes["name"]!!  // 从map中手动检索属性
}

那么把它修改成委托属性很是简单,能够直接将map放在by关键字后面

class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, arrtValue: String) {
        _attributes[attrName] = arrtValue
    }

    //  将map做为委托属性
    val name: String by _attributes
}

由于标准库已经在标准Map和MutableMap接口上定义了getValue和setValue扩展函数。

若是个人文章对您有帮助,不妨点个赞鼓励一下(^_^)

相关文章
相关标签/搜索