博客主頁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擴展函數。
若是個人文章對您有幫助,不妨點個贊鼓勵一下(^_^)