博客主页java
java在标准库中有一些与特定的类相关联的语言特性,如实现了java.lang.Iterable接口的对象能够在for循环中使用,实现了java.lang.AutoCloseable接口的对象能够在try-with-resources语句中使用。算法
但在kotlin中,一些功能是与特定的函数名相关,而不是与特定的类型绑定。kotlin使用约定的原则,不像java依赖类型。kotlin能够经过扩展函数机制来为现有的类增添新的方法,能够把任意约定方法定义为扩展函数。数据库
java中,算术运算符只能用于基本数据类型,+运算符能够与String值一块儿使用。若是给集合添加元素时,想要可以用 += 运算符就完美。在kotlin中,是能够这样作的。segmentfault
先来看一个例子:定义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
+= 、-=等这些运算符称为复合赋值运算符。
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]
预先定义一个名称来声明函数(成员函数或者扩展函数),并用修饰符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函数。
若是在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方法调用,编译器会自动对返回值取反。
在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运算符来检查元素是否在集合区间内,也能够迭代集合。
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方法的调用。
集合支持的另外一个运算符是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左边的对象将会做为函数入参。
建立一个区间,使用 .. 语法。如: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
在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在标准库只容许使用此语法来访问一个对象的前五个元素。
接收一个函数返回多个值,可使用标准库中的 Pair 和 Triple 类。
解构声明不只能够用做函数中的顶层语句,还能够在其它能够声明变量的地方,如: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() // ... }
委托属性的基本语法:
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)来实现属性的修改
惰性初始化:当第一次访问该属性的时候,才根据须要建立对象的一部分。
例如:一个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函数是线程安全的。
在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这个类作了更改的地方:
而后使用委托属性来绑定更该通知:
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的对象。
接下来总结一下委托属性是怎么工做的?
假设有一个委托属性的类:
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) }
委托属性另外一种常见用法,是用在有动态定义的属性集的对象中,这种对象有时被称为自订对象。
举一个例子:定义一个属性,把值存到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扩展函数。
若是个人文章对您有帮助,不妨点个赞鼓励一下(^_^)