導語:2017年Google IO大會宣布使用Kotlin作為Android的官方開發語言,相比較與典型的面相對象的JAVA語言,Kotlin作為一種新式的函數式編程語言,也有人稱之為Android平台的Swift語言。
先讓我們看下實現同樣的功能,Java和Kotiln的對比:
// JAVA,20多行代碼,充斥着findViewById,類型轉換,匿名內部類這樣的無意義代碼
public class MainJavaActivity extends Activity {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView label = (TextView) findViewById(R.id.label);
Button btn = (Button) findViewById(R.id.btn);
label.setText("hello");
label.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("Glen","onClick TextView");
}
});
btn.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Log.d("Glen","onClick Button");
}
});
}
}
再來看Kotlin
// Kotlin,沒有了冗余的findViewById,我們可以直接對資源id進行操作,也不需要匿名內部類的聲明,更關注函數的實現本身,拋棄了復雜的格式
class MainKotlinActivity:Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
R.id.label.setText("hello")
R.id.label.onClick { Log.d("Glen","onClick TextView") }
R.id.btn.onClick { Log.d("Glen","onClick Button") }
}
}
實現這些需要借助Kotlin的擴展函數與高階函數,本文主要介紹一下擴展函數。
1. Kotlin 擴展函數與擴展屬性(Kotlin Extensions)
Kotlin 能夠擴展一個類的新功能而無需繼承該類,或者對任意的類使用像“裝飾者(Decorator)”這樣的設計模式。這些都是通過叫做“擴展(extensions)”的特殊聲明實現的。Kotlin擴展聲明既支持擴展函數也支持擴展屬性,本文主要討論擴展函數,至於擴展屬性實現的機制類似。
擴展函數的聲明非常簡單,他的關鍵字是.,此外我們需要一個“接受者類型(recievier type)”來作為他的前綴。以類MutableList<Int>
為例,現在為它擴展一個swap方法,如下:
fun MutableList<Int>.swap(index1:Int,index2:Int) {
val tmp = this[index1]
this[index1] = this[index2]
this[index2] = tmp
}
MutableList<T>
是kotlin提供的基礎庫collection
中的List容器類,這里在聲明里作為“接受者類型”,.作為聲明關鍵字,swap
是擴展函數名,其余和Kotlin聲明一個普通函數並無區別。
額外提一句,Kotlin的this語法
要比JAVA更靈活,這里擴展函數體里的this
代表的是接受者類型對象。
如果我們想要調用這個擴展函數,可以這樣:
fun use(){
val list = mutableListOf(1,2,3)
list.swap(1,2)
}
2. Kotlin擴展函數是怎么實現的
擴展函數的調用看起來就像是原生方法一樣自然,使用起來也非常順手,但是這樣的方法會不會帶來性能方面的掣肘呢?有必要探究一下Kotlin是如何實現擴展函數的,直接分析Kotlin源碼難度還是挺大,還好Android Studio提供了一些工具,我們可以通過Kotlin ByteCode
指令,查看Kotlin語言轉換的字節碼文件,仍以MutableList<Int>
,swap
為例,轉換為字節碼之后的文件如下:
// ================com/example/glensun/demo/extension/MutableListDemoKt.class =================
// class version 50.0 (50)
// access flags 0x31
public final class com/example/glensun/demo/extension/MutableListDemoKt {
// access flags 0x19
// signature (Ljava/util/List<Ljava/lang/Integer;>;II)V
// declaration: void swap(java.util.List<java.lang.Integer>, int, int)
public final static swap(Ljava/util/List;II)V
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 0
LDC "$receiver"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 8 L1
ALOAD 0
ILOAD 1
INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object;
CHECKCAST java/lang/Number
INVOKEVIRTUAL java/lang/Number.intValue ()I
ISTORE 3
L2
LINENUMBER 9 L2
ALOAD 0
ILOAD 1
ALOAD 0
ILOAD 2
INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object;
INVOKEINTERFACE java/util/List.set (ILjava/lang/Object;)Ljava/lang/Object;
POP
L3
LINENUMBER 10 L3
ALOAD 0
ILOAD 2
ILOAD 3
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
INVOKEINTERFACE java/util/List.set (ILjava/lang/Object;)Ljava/lang/Object;
POP
L4
LINENUMBER 11 L4
RETURN
L5
LOCALVARIABLE tmp I L2 L5 3
LOCALVARIABLE $receiver Ljava/util/List; L0 L5 0
LOCALVARIABLE index1 I L0 L5 1
LOCALVARIABLE index2 I L0 L5 2
MAXSTACK = 4
MAXLOCALS = 4
@Lkotlin/Metadata;(mv={1, 1, 7}, bv={1, 0, 2}, k=2, d1={"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\u0008\n\u0002\u0008\u0003\u001a \u0010\u0000\u001a\u00020\u0001*\u0008\u0012\u0004\u0012\u00020\u00030\u00022\u0006\u0010\u0004\u001a\u00020\u00032\u0006\u0010\u0005\u001a\u00020\u0003\u00a8\u0006\u0006"}, d2={"swap", "", "", "", "index1", "index2", "production sources for module app"})
// compiled from: MutableListDemo.kt
}
// ================META-INF/production sources for module app.kotlin_module =================
這里的字節碼已經相當直觀,更令人驚喜的是Android Studio還具備將字節碼轉為JAVA文件的能力,點擊上面的Decompile按鈕,可以得到如下JAVA代碼:
import java.util.List;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
@Metadata(
mv = {1, 1, 7},
bv = {1, 0, 2},
k = 2,
d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u001a \u0010\u0000\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00030\u00022\u0006\u0010\u0004\u001a\u00020\u00032\u0006\u0010\u0005\u001a\u00020\u0003¨\u0006\u0006"},
d2 = {"swap", "", "", "", "index1", "index2", "production sources for module app"}
)
public final class MutableListDemoKt {
public static final void swap(@NotNull List $receiver, int index1, int index2) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
int tmp = ((Number)$receiver.get(index1)).intValue();
$receiver.set(index1, $receiver.get(index2));
$receiver.set(index2, Integer.valueOf(tmp));
}
}
從得到的JAVA文件分析,擴展函數的實現非常簡單,它沒有修改接受者類型的成員,僅僅是通過靜態方法來實現的。這樣,我們雖然不必擔心擴展函數會帶來額外的性能消耗,但是它也不會帶來性能上的優化。
3.更復雜的情況
下面來討論一些更特殊的情況。
3.1 當發生繼承時,擴展函數由於本質上是靜態方法,它會嚴格按照參數類型去執行調用,而不會去優先執行或者主動執行父類的方法,如下的例子所示:
open class A
class B:A()
fun A.foo() = "a"
fun B.foo() = "b"
fun printFoo(a:A){
println(a.foo())
}
println(B())
上述例子的輸出結果是a,因為擴展函數的入參類型是A,他將會嚴格按照入參類型執行函數調用。
3.2 如果擴展函數和現有的類成員發生沖突,kotlin將會默認使用類成員,這一步選擇是在編譯期處理的,生成的字節碼是將會是調用類成員的方法,如下例子:
class C{
fun foo() {println("Member")}
}
fun C.foo() {println("Extension")}
println(C().foo())
上述的例子將會輸出Member
。Kotlin不允許擴展一個已有的成員,原因也很好理解,我們不希望擴展函數成為調用三方sdk的漏洞,不過如果你試圖使用重載的方式創建擴展函數,這樣是可行的。
3.3 Kotlin嚴格區分了可能為空和不為空的入參類型,同樣也應用在擴展函數的中,為了聲明一個可能為空的接受者類型,可以參考如下例子:
fun <T> MutableList<T>?.swap(index1:Int,index2:Int){
if(this == null){
println(null)
return
}
val tmp = this[index1]
this[index1] = this[index2]
this[index2] = tmp
}
3.4 我們有時候還希望能夠添加類似JAVA的“靜態函數”的擴展函數,這時需要借助“伴隨對象(Companion Object)”來實現,如下這個例子:
class D{
companion object{
val m = 1
}
}
fun D.Companion.foo(){
println("$m in extension")
}
D.foo()
上面的例子會輸出1 in extension
,注意這里調用foo這個擴展函數時,並不需要類D的實例,類似於JAVA的靜態方法。
3.5 如果留意前面的例子,我們會發現kotlin的this
語法和JAVA不同,使用范圍更靈活,僅以擴展函數為例,當在擴展函數里調用this
時,指代的是接受者類型的實例,那么如果這個擴展函數聲明在一個類內部,我們如何通過this
獲取到類的實例呢?可以參考下面的例子:
class E{
fun foo(){
println("foo in Class E")
}
}
class F{
fun foo(){
println("foo in Class F")
}
fun E.foo2(){
this.foo()
this@F.foo()
}
}
E().foo2()
這里使用了kotlin的this指定語法,關鍵字是@,后接指定的類型,上述例子的輸出結果是
foo in Class E
foo in Class F
4. 擴展函數的作用域
一般來說,我們習慣將擴展函數直接定義在包內,例如:
package com.example.extension
fun MutableList<Int>.swap(index1:Int,index2:Int) {
val tmp = this[index1]
this[index1] = this[index2]
this[index2] = tmp
}
這樣,在同一個包內可以直接調用改擴展函數,如果我們需要跨包調用擴展函數,我們需要通過import來指明,以上述的例子為例,可以通過import com.example.extension.swap
來指定這個擴展函數,也可以通過import com.example.extension.*
表示引入該包內的所有擴展函數。得益於Android Studio具備的自動聯想能力,通常不需要我們主動輸入import
指令。
有時候,我們也會把擴展函數定義在類的內部,例如:
class G {
fun Int.foo(){
println("foo in Class G")
}
}
這里的Int.foo()
是一個定義在類G內部的擴展函數,在這個擴展函數里,我們直接使用Int
類型作為接受者類型,因為我們將擴展函數定義在了類的內部,即使我們設置訪問權限為public
,它也只能在該類或者該類的子類中被訪問,如果我們設置訪問權限為private,那么在子類中也不能訪問這個擴展函數。
5. 擴展函數的實際應用
5.1 Utils工具類
在JAVA中,我們習慣將工具類命名成*Utils
,例如FileUtils
,StringUtils
等等,著名的java.util.Collections
也是這么實現的。調用這些方法的時候,總覺得這些類名礙手礙腳的,例如這樣:
// Java
Collections.swap(list, Collections.binarySearch(list, Collections.max(otherList)), Collections.max(list));
Collections.max(list));
通過靜態引用,能讓情況看起來好一點,例如這樣:
// Java
swap(list, binarySearch(list, max(otherList)), max(list));
但是這樣既沒有IDE的自動聯想提示,方法調用的主體也顯得不明確。如果能做成下面這樣就好了:
// Java
list.swap(list.binarySearch(otherList.max()), list.max());
但是list是JAVA默認的基礎類,在JAVA語言里,如果不使用繼承,肯定是沒法做到這樣的,而在Kotlin中就可以借助擴展函數來實現啦。
5.2 Android View 膠水代碼
回到最開始的例子,對於Android開發來說,對findViewById()
這個方法一定不會陌生,為了獲取一個View對象,我們總得先調用findViewById()
然后再執行類型轉換,這樣無意義的膠水代碼讓Activity
或者Fragment
顯得臃腫無比,例如:
// JAVA
public class MainJavaActivity extends Activity {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView label = (TextView) findViewById(R.id.label);
Button btn = (Button) findViewById(R.id.btn);
label.setText("hello");
label.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("Glen","onClick TextView");
}
});
btn.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Log.d("Glen","onClick Button");
}
});
}
}
我們考慮利用擴展函數結合泛型,避免頻繁的類型轉換,擴展函數定義如下:
//kotlin
fun <T : View> Activity.find(@IdRes id: Int): T {
return findViewById(id) as T
}
調用的時候,如下:
// Kotlin
...
TextView label = find(R.id.label);
Button btn = find(R.id.btn);
...
只是我們還是需要獲取到label
,btn
,這樣無意義的中間變量,如果在Int類上擴展,可以直接對R.id.*
操作,這樣更直接,再結合高階函數,函數定義如下:
//Kotlin
fun Int.setText(str:String){
val label = find<TextView>(this).apply {
text = str
}
}
fun Int.onClick(click: ()->Unit){
val tmp = find<View>(this).apply {
setOnClickListener{
click()
}
}
}
我們就可以這樣調用:
//Kotlin
R.id.label.setText("hello")
R.id.label.onClick { Log.d("Glen","onClick TextView") }
R.id.btn.onClick { Log.d("Glen","onClick Button") }
通常這些擴展函數可以放到基類中,根據擴展函數的作用域知識,我們可以在所有子類中都調用到這些方法,所以kotlin的Activity
可以寫成:
// Kotlin
class MainKotlinActivity:KotlinBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
R.id.label.setText("hello")
R.id.label.onClick { Log.d("Glen","onClick TextView") }
R.id.btn.onClick { Log.d("Glen","onClick Button") }
}
}
從原來JAVA冗余的20多行代碼,精簡到只需要3行代碼,而且代碼可讀性更高,更加直觀,這便是函數式編程語言Kotlin的強大威力。
問答
什么是Kotlin的“接收器”?
相關閱讀
你為什么需要 Kotlin
手Q Android線程死鎖監控與自動化分析實踐
為什么說Kotlin的可讀性比Java好?
**此文已由作者授權騰訊雲+社區發布,原文鏈接:https://cloud.tencent.com/developer/article/1146533?fromSource=waitui **
歡迎大家前往騰訊雲+社區或關注雲加社區微信公眾號(QcloudCommunity),第一時間獲取更多海量技術實踐干貨哦~