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例項會被儲存到一個隱藏的屬性中,它被稱為<delegate>. 編譯器也將用一個KProperty型別的物件來代表這個屬性,它被稱為<property>.
編譯器生成的程式碼如下:
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 }
小結:
- Kotlin允許使用對應名稱的函式來過載一些標準的數學運算,但是不能定義自己的運算子
- 比較對映為equals和compareTo方法的呼叫
- 通過定義名為get,set和contains的函式,就可以讓你自己的類與Kotlin的集合一樣,使用[]和in運算子
- 可以通過約定來建立區間,以及迭代集合和陣列
- 解構宣告可以展開單個物件用來初始化多個變數,這可以方便地用來從函式返回多個值。它們可以自動處理資料類, 可以通過給自己的類定義名為componentN的函式
- 委託屬性可以用來重用邏輯, 這些邏輯控制如何儲存,初始化,訪問和修改屬性值,這是用來構建框架的一個強大的工具
- lazy標準庫函式提供了一種實現惰性初始化屬性的簡單方法
- Delegates.observable 函式可以用來新增屬性更改的觀察者
- 委託屬性可以使用任意map來作為委託屬性委託,來靈活處理具有可變屬性集的物件
自己是從事了七年開發的Android工程師,不少人私下問我,2019年Android進階該怎麼學,方法有沒有?
沒錯,年初我花了一個多月的時間整理出來的學習資料,希望能幫助那些想進階提升Android開發,卻又不知道怎麼進階學習的朋友。【 包括高階UI、效能優化、架構師課程、NDK、Kotlin、混合式開發(ReactNative+Weex)、Flutter等架構技術資料 】,希望能幫助到您面試前的複習且找到一個好的工作,也節省大家在網上搜索資料的時間來學習。
資料獲取方式:加入Android架構交流QQ群聊:513088520 ,進群即領取資料!!!
點選連結加入群聊【Android移動架構總群】:加入群聊

資料大全