Kotlin:代理真的很簡單啊!
我們知道在Kotlin里語法糖的存在都是為了解決之前Java某種現有的問題或者是簡化程式碼,之前我們已經討論了諸多語法糖,瞭解它們的實現以及如何優化。在我們常用的第三方庫中,一個比較常見的東西就是代理模式,但是這個東西寫起來略繁瑣,好在到了Kotlin這裡,在語言層面上支援代理,我們一起來了解下。
首先,什麼是代理,代理就是代為處理的意思,我們把要做的事情委託給萬事屋,由萬事屋來幫我們完成,日常我們請保潔,找代駕,都可以說她們是我們的代理。代理也分為類代理跟代理屬性,今天我們主要討論的是類代理。
為了讓大家有個直觀的理解,我們來看個小示例:
interface Calculator { fun calculate():Int } 複製程式碼
然後我們讓兩個類來實現這個介面:
class CalculatorBrain:Calculator { override fun calculate(): Int { TODO("not implemented") } } class SuperCalculator(val calculator: Calculator) : Calculator { override fun calculate(): Int { return calculator.calculate() } } 複製程式碼
在這裡,兩個類實現了介面並實現了自己的calculate
方法,只不過SuperCalculator
的實現是呼叫了其它方法。
最後我們就可以這樣用啦:
class Main { fun main(args: Array<String>) { val calculatorBrain = CalculatorBrain() val superCalculator = SuperCalculator(calculatorBrain) superCalculator.calculate() } } 複製程式碼
在上面例子中,calculatorBrain
就是superCaculator
的代理,任何時候我們讓superCalculator
做事它都會轉發給calculatorBrain
來做,事實上,任何實現了Calculator
介面的類都可以傳給SuperCalculator
,這種把代理物件顯式傳給其他物件的方式,我們就稱之為顯式代理
。
顯式代理任何面向物件的語言都能實現,而且每次都需要我們實現相同的介面,傳入代理物件,然後在重寫的方法中呼叫代理物件的方法,感覺又是一大波重複勞動,而Kotlin在語法層面上使用了by
關鍵字給我們提供了支援,語言級支援的代理就是隱式代理
。
我們來看看我們上面的例子使用Kotlin的特性改寫是什麼樣的:
class SuperCalculator(val calculator: Calculator) : Calculator by calculator 複製程式碼
沒錯,只要用by
把SuperCalculator
類改成這樣就行了!不用再實現介面方法再呼叫代理物件對應的方法了,老規矩我們來看看位元組碼:
public final class SuperCalculator implements Calculator { // access flags 0x12 private final LCalculator; calculator @Lorg/jetbrains/annotations/NotNull;() // invisible // access flags 0x11 public final getCalculator()LCalculator; @Lorg/jetbrains/annotations/NotNull;() // invisible L0 LINENUMBER 1 L0 ALOAD 0 GETFIELD SuperCalculator.calculator : LCalculator; ARETURN L1 LOCALVARIABLE this LSuperCalculator; L0 L1 0 MAXSTACK = 1 MAXLOCALS = 1 // access flags 0x1 public <init>(LCalculator;)V // annotable parameter count: 1 (visible) // annotable parameter count: 1 (invisible) @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 L0 ALOAD 1 LDC "calculator" INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V L1 LINENUMBER 1 L1 ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V ALOAD 0 ALOAD 1 PUTFIELD SuperCalculator.calculator : LCalculator; RETURN L2 LOCALVARIABLE this LSuperCalculator; L0 L2 0 LOCALVARIABLE calculator LCalculator; L0 L2 1 MAXSTACK = 2 MAXLOCALS = 2 // access flags 0x1 public calculate()I L0 ALOAD 0 GETFIELD SuperCalculator.calculator : LCalculator; INVOKEINTERFACE Calculator.calculate ()I (itf) IRETURN L1 LOCALVARIABLE this LSuperCalculator; L0 L1 0 MAXSTACK = 1 MAXLOCALS = 1 } 複製程式碼
咳咳,換湯不換藥,跟我們自己用Java實現代理大體相似,只不過減少了我們的重複勞動,編譯器為我們做了實現。可能有些同學看不太明白,我們直接反編譯成Java程式碼看看:
public final class SuperCalculator implements Calculator { @NotNull private final Calculator calculator; @NotNull public final Calculator getCalculator() { return this.calculator; } public SuperCalculator(@NotNull Calculator calculator) { Intrinsics.checkParameterIsNotNull(calculator, "calculator"); super(); this.calculator = calculator; } public int calculate() { return this.calculator.calculate(); } } 複製程式碼
這樣就比較清楚了,注意生成的位元組碼只有get方法沒有set方法,因為我在構造器裡把calculator
宣告成val,所以反編譯的程式碼裡面也使用final修飾了它。顯式代理由於代理物件都是我們自己傳入的我們可以自己新增set方法來實現隨意替換的手段,那這裡我把val改成var,能不能動態替換代理物件呢?我們來試一下:
class SuperCalculator(var calculator: Calculator) : Calculator by calculator 複製程式碼
直接來看反編譯後的Java程式碼:
public final class SuperCalculator implements Calculator { @NotNull private Calculator calculator; // $FF: synthetic field private final Calculator $$delegate_0; @NotNull public final Calculator getCalculator() { return this.calculator; } public final void setCalculator(@NotNull Calculator var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.calculator = var1; } public SuperCalculator(@NotNull Calculator calculator) { Intrinsics.checkParameterIsNotNull(calculator, "calculator"); super(); this.$$delegate_0 = calculator; this.calculator = calculator; } public int calculate() { return this.$$delegate_0.calculate(); } } 複製程式碼
我們看到確實多了一個set方法,但是生成的類裡面卻有兩個Calculator
物件,檢視生成的calculate方法也可以看到實際起作用的是$$delegate_0
,而呼叫set方法的時候並不會給它賦值,它使用final來宣告。這意味著Kotlin的代理是不支援在執行時動態替換的,這跟我們自己實現顯式代理有些區別。也建議大家在用kotlin寫代理時,構造器裡使用val,避免編譯器給我們生成無謂的欄位跟方法,減少開銷。
假設我們這時候又多了一個新型的量子計算器:
class QuantumCalculator(val calculator: Calculator) : Calculator by calculator 複製程式碼
我們也可以這樣使用它了:
fun main(args: Array<String>) { val calculatorBrain = CalculatorBrain() val superCalculator = SuperCalculator(calculatorBrain) superCalculator.calculate() val calculatorBrain1 = CalculatorBrain() val quantumCalculator = QuantumCalculator(calculatorBrain1) quantumCalculator.calculate() } 複製程式碼
在這裡我們每次建立一個新的Calculator
物件,就會新建立一個CalculatorBrain
物件,但是在這個場景下,傳入的代理物件行為都是一樣的,都是CalculatorBrain
,為了節約寶貴的記憶體,我們可以複用一個已有的CalculatorBrain
,在建立其他物件需要時使用它即可:
fun main(args: Array<String>) { val calculatorBrain = CalculatorBrain() val superCalculator = SuperCalculator(calculatorBrain) superCalculator.calculate() val quantumCalculator = QuantumCalculator(calculatorBrain) quantumCalculator.calculate() } 複製程式碼
像我們這裡,我們一開始就知道對於SuperCalculator
跟QuantumCalculator
這些類,我們不會傳入CalculatorBrain
外的其他物件作為代理,那每次使用時還要再傳入代理物件,而且是相同的代理物件,也很繁瑣,也算得上是樣板程式碼,我們可以直接建立個CalculatorBrain
的單例:
object CalculatorBrain: Calculator{ override fun calculate(): Int { TODO("not implemented") } } 複製程式碼
然後直接整合到這些類的聲明裡面:
class SuperCalculator() : Calculator by calculatorBrain class QuantumCalculator() : Calculator by calculatorBrain 複製程式碼
看,程式碼更加簡潔明瞭了。
我們來看看反編譯後Quantum
的Java程式碼:
public final class QuantumCalculator implements Calculator { // $FF: synthetic field private final CalculatorBrain $$delegate_0; public QuantumCalculator() { this.$$delegate_0 = CalculatorBrain.INSTANCE; } public int calculate() { return this.$$delegate_0.calculate(); } } 複製程式碼
我們可以看到,在預設建構函式裡,我們給一個用final宣告的代理賦值了,而值來自於CalculatorBrain.INSTANCE
,從命名來看,這是一個單例,我們這就來看看它的程式碼:
public final class CalculatorBrain implements Calculator { public static final CalculatorBrain INSTANCE; public int calculate() { String var1 = "not implemented"; throw (Throwable)(new NotImplementedError("An operation is not implemented: " + var1)); } private CalculatorBrain() { } static { CalculatorBrain var0 = new CalculatorBrain(); INSTANCE = var0; } } 複製程式碼
原始碼面前無祕密,這確實是一個單例。
現在,我們可以更簡單地使用這些類了:
fun main(args: Array<String>) { SuperCalculator().calculate() QuantumCalculator().calculate() } 複製程式碼
好了,關於類代理就探究這麼多了。我們大多都知道組合優於繼承,其實代理也是繼承很好的替代方案,而且很靈活。通過本次學習大家應該都明白了類代理的使用方法,下一次,我們一起來扒一扒代理屬性。