你還在把Java當成Android官方開發語言嗎?Kotlin瞭解一下!

導語:2017年Google IO大會宣佈使用Kotlin做爲Android的官方開發語言,相比較與典型的面相對象的JAVA語言,Kotlin做爲一種新式的函數式編程語言,也有人稱之爲Android平臺的Swift語言。java

本文由騰訊Bugly發表在騰訊雲+社區 編程

先讓咱們看下實現一樣的功能,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的擴展函數與高階函數,本文主要介紹一下擴展函數。app

1. Kotlin 擴展函數與擴展屬性(Kotlin Extensions)

Kotlin 可以擴展一個類的新功能而無需繼承該類,或者對任意的類使用像「裝飾者(Decorator)」這樣的設計模式。這些都是經過叫作「擴展(extensions)」的特殊聲明實現的。Kotlin擴展聲明既支持擴展函數也支持擴展屬性,本文主要討論擴展函數,至於擴展屬性實現的機制相似。jvm

擴展函數的聲明很是簡單,他的關鍵字是.,此外咱們須要一個「接受者類型(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聲明一個普通函數並沒有區別。ide

額外提一句,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),第一時間獲取更多海量技術實踐乾貨哦~

相關文章
相關標籤/搜索