重載算數運算符

下面以一個栗子開始,我們先定義一個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<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) : 否則,只有兩個參數都是空引用,結果才是truea == 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
}

小結:

  1. Kotlin允許使用對應名稱的函數來重載一些標準的數學運算,但是不能定義自己的運算符
  2. 比較映射為equals和compareTo方法的調用
  3. 通過定義名為get,set和contains的函數,就可以讓你自己的類與Kotlin的集合一樣,使用[]和in運算符
  4. 可以通過約定來創建區間,以及迭代集合和數組
  5. 解構聲明可以展開單個對象用來初始化多個變數,這可以方便地用來從函數返回多個值。它們可以自動處理數據類, 可以通過給自己的類定義名為componentN的函數
  6. 委託屬性可以用來重用邏輯, 這些邏輯控制如何存儲,初始化,訪問和修改屬性值,這是用來構建框架的一個強大的工具
  7. lazy標準庫函數提供了一種實現惰性初始化屬性的簡單方法
  8. Delegates.observable 函數可以用來添加屬性更改的觀察者
  9. 委託屬性可以使用任意map來作為委託屬性委託,來靈活處理具有可變屬性集的對象

自己是從事了七年開發的Android工程師,不少人私下問我,2019年Android進階該怎麼學,方法有沒有?

沒錯,年初我花了一個多月的時間整理出來的學習資料,希望能幫助那些想進階提升Android開發,卻又不知道怎麼進階學習的朋友。【包括高級UI、性能優化、架構師課程、NDK、Kotlin、混合式開發(ReactNative+Weex)、Flutter等架構技術資料】,希望能幫助到您面試前的複習且找到一個好的工作,也節省大家在網上搜索資料的時間來學習。

喜歡我的文章可以點贊+關注我的【個人主頁】獲取免費資料,後續我將繼續分享更多Android技術乾貨,感謝支持!

資料大全

推薦閱讀:
相关文章