用Kotlin去提升生產力:彙總Kotlin相對於Java的優點 kotlin_tips

怎麼用Kotlin去提升生產力:Kotlin Tips

https://github.com/heimashi/kotlin_tipsphp

彙總Kotlin相對於Java的優點,以及怎麼用Kotlin去簡潔、務實、高效、安全的開發,每一個tip都有詳細的說明和案例代碼,爭取把每一個tip分析得清楚易懂,會不斷的更新維護tips,歡迎fork進來加入咱們一塊兒來維護,有問題的話歡迎提Issues。html

  • 推薦一個Kotlin的實踐項目debug_view_kotlin,用kotlin實現的Android浮層調試控制檯,實時的顯示內存、FPS、App啓動時間、Activity啓動時間、文字Log

Tip1- 更簡潔的字符串

詳見案例代碼KotlinTip1java

Kotlin中的字符串基本Java中的相似,有一點區別是加入了三個引號"""來方便長篇字符的編寫。 而在Java中,這些都須要轉義,先看看java中的式例android

public void testString1() {
        String str1 = "abc";
        String str2 = "line1\n" +
                "line2\n" +
                "line3";
        String js = "function myFunction()\n" +
                "{\n" +
                " document.getElementById(\"demo\").innerHTML=\"My First JavaScript Function\";\n" +
                "}";
        System.out.println(str1);
        System.out.println(str2);
        System.out.println(js);
    }
複製代碼

kotlin除了有單個雙引號的字符串,還對字符串的增強,引入了三個引號,"""中能夠包含換行、反斜槓等等特殊字符:git

/* * kotlin對字符串的增強,三個引號"""中能夠包含換行、反斜槓等等特殊字符 * */
fun testString() {
    val str1 = "abc"
    val str2 = """line1\n line2 line3 """
    val js = """ function myFunction() { document.getElementById("demo").innerHTML="My First JavaScript Function"; } """.trimIndent()
    println(str1)
    println(str2)
    println(js)
}
複製代碼

同時,Kotlin中引入了字符串模版,方便字符串的拼接,能夠用$符號拼接變量和表達式程序員

/* * kotlin字符串模版,能夠用$符號拼接變量和表達式 * */
fun testString2() {
    val strings = arrayListOf("abc", "efd", "gfg")
    println("First content is $strings")
    println("First content is ${strings[0]}")
    println("First content is ${if (strings.size > 0) strings[0] else "null"}")
}
複製代碼

值得注意的是,在Kotlin中,美圓符號是特殊字符,在字符串中不能直接顯示,必須通過轉義,方法1是用反斜槓,方法二是{'$'}github

/* * Kotlin中,美圓符號$是特殊字符,在字符串中不能直接顯示,必須通過轉義,方法1是用反斜槓,方法二是${'$'} * */
fun testString3() {
    println("First content is \$strings")
    println("First content is ${'$'}strings")
}
複製代碼

Tip2- Kotlin中大多數控制結構都是表達式

首先,須要弄清楚一個概念語句和表達式,而後會介紹控制結構表達式的優勢:簡潔api

語句和表達式是什麼?

  • 表達式有值,而且能做爲另外一個表達式的一部分使用
  • 語句老是包圍着它的代碼塊中的頂層元素,而且沒有本身的值

Kotlin與Java的區別

  • Java中,全部的控制結構都是語句,也就是控制結構都沒有值
  • Kotlin中,除了循環(for、do和do/while)之外,大多數控制結構都是表達式(if/when等)

詳見案例代碼tip2緩存

Example1:if語句

java中,if 是語句,沒有值,必須顯式的return安全

/* * java中的if語句 * */
public int max(int a, int b) {
    if (a > b) {
        return a;
    } else {
        return b;
    }
}
複製代碼

kotlin中,if 是表達式,不是語句,由於表達式有值,能夠做爲值return出去

/* * kotlin中,if 是表達式,不是語句,相似於java中的三目運算符a > b ? a : b * */
fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
}
複製代碼

上面的if中的分支最後一行語句就是該分支的值,會做爲函數的返回值。這其實跟java中的三元運算符相似,

/* * java的三元運算符 * */
public int max2(int a, int b) {
    return a > b ? a : b;
}
複製代碼

上面是java中的三元運算符,kotlin中if是表達式有值,徹底能夠替代,故kotlin中已沒有三元運算符了,用if來替代。 上面的max函數還能夠簡化成下面的形式

/* * kotlin簡化版本 * */
fun max2(a: Int, b: Int) = if (a > b) a else b
複製代碼

Example2:when語句

Kotlin中的when很是強大,徹底能夠取代Java中的switch和if/else,同時,when也是表達式,when的每一個分支的最後一行爲當前分支的值 先看一下java中的switch

/* * java中的switch * */
    public String getPoint(char grade) {
        switch (grade) {
            case 'A':
                return "GOOD";
            case 'B':
            case 'C':
                return "OK";
            case 'D':
                return "BAD";
            default:
                return "UN_KNOW";
        }
    }
複製代碼

java中的switch有太多限制,咱們再看看Kotlin怎樣去簡化的

/* * kotlin中,when是表達式,能夠取代Java 中的switch,when的每一個分支的最後一行爲當前分支的值 * */
fun getPoint(grade: Char) = when (grade) {
    'A' -> "GOOD"
    'B', 'C' -> {
        println("test when")
        "OK"
    }
    'D' -> "BAD"
    else -> "UN_KNOW"
}
複製代碼

一樣的,when語句還能夠取代java中的if/else if,其是表達式有值,而且更佳簡潔

/* * java中的if else * */
    public String getPoint2(Integer point) {
        if (point > 100) {
            return "GOOD";
        } else if (point > 60) {
            return "OK";
        } else if (point.hashCode() == 0x100) {
            //...
            return "STH";
        } else {
            return "UN_KNOW";
        }
    }
複製代碼

再看看kotlin的版本,使用不帶參數的when,只須要6行代碼

/* * kotlin中,when是表達式,能夠取代java的if/else,when的每一個分支的最後一行爲當前分支的值 * */
fun getPoint2(grade: Int) = when {
    grade > 90 -> "GOOD"
    grade > 60 -> "OK"
    grade.hashCode() == 0x100 -> "STH"
    else -> "UN_KNOW"
}
複製代碼

Tip3- 更好調用的函數:顯式參數名/默認參數值

Kotlin的函數更加好調用,主要是表如今兩個方面:1,顯式的標示參數名,能夠方便代碼閱讀;2,函數能夠有默認參數值,能夠大大減小Java中的函數重載。 例如如今須要實現一個工具函數,打印列表的內容: 詳見案例代碼KotlinTip3

/* * 打印列表的內容 * */
fun <T> joinToString(collection: Collection<T>, separator: String, prefix: String, postfix: String): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
/* * 測試 * */
fun printList() {
    val list = listOf(2, 4, 0)
    // 不標明參數名
    println(joinToString(list, " - ", "[", "]"))
    // 顯式的標明參數名稱
    println(joinToString(list, separator = " - ", prefix = "[", postfix = "]"))
}
複製代碼

如上面的代碼所示,函數joinToString想要打印列表的內容,須要傳入四個參數:列表、分隔符、前綴和後綴。 因爲參數不少,在後續使用該函數的時候不是很直觀的知道每一個參數是幹什麼用的,這時候能夠顯式的標明參數名稱,增長代碼可讀性。 同時,定義函數的時候還能夠給函數默認的參數,以下所示:

/* * 打印列表的內容,帶有默認的參數,能夠避免java的函數重載 * */
fun <T> joinToString2(collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
/* * 測試 * */
fun printList3() {
    val list = listOf(2, 4, 0)
    println(joinToString2(list, " - "))
    println(joinToString2(list, " , ", "["))
}
複製代碼

這樣有了默認參數後,在使用函數時,若是不傳入該參數,默認會使用默認的值,這樣能夠避免Java中大量的函數重載。

Tip4- 擴展函數和屬性

擴展函數和屬性是Kotlin很是方便實用的一個功能,它可讓咱們隨意的擴展第三方的庫,你若是以爲別人給的SDK的api很差用,或者不能知足你的需求,這時候你能夠用擴展函數徹底去自定義。 例如String類中,咱們想獲取最後一個字符,String中沒有這樣的直接函數,你能夠用.後聲明這樣一個擴展函數: 詳見案例代碼KotlinTip4

/* * 擴展函數 * */
fun String.lastChar(): Char = this.get(this.length - 1)
/* * 測試 * */
fun testFunExtension() {
    val str = "test extension fun";
    println(str.lastChar())
}
複製代碼

這樣定義好lastChar()函數後,以後只須要import進來後,就能夠用String類直接調用該函數了,跟調用它本身的方法沒有區別。這樣能夠避免重複代碼和一些靜態工具類,並且代碼更加簡潔明瞭。 例如咱們能夠改造上面tip3中的打印列表內容的函數:

/* * 用擴展函數改造Tip3中的列表打印內容函數 * */
fun <T> Collection<T>.joinToString3(separator: String = ", ", prefix: String = "", postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, element) in withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

fun printList4() {
    val list = listOf(2, 4, 0)
    println(list.joinToString3("/"))
}
複製代碼

除了擴展函數,還能夠擴展屬性,例如我想實現String和StringBuilder經過屬性去直接得到最後字符:

/* * 擴展屬性 lastChar獲取String的最後一個字符 * */
val String.lastChar: Char
    get() = get(length - 1)
/* * 擴展屬性 lastChar獲取StringBuilder的最後一個字符 * */
var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value: Char) {
        setCharAt(length - 1, value)
    }
/* * 測試 * */
fun testExtension() {
    val s = "abc"
    println(s.lastChar)
    val sb = StringBuilder("abc")
    println(sb.lastChar)
}
複製代碼

定義好擴展屬性後,以後只需import完了就跟使用本身的屬性同樣方便了。

Why?Kotlin爲何能實現擴展函數和屬性這樣的特性?

在Kotlin中要理解一些語法,只要認識到Kotlin語言最後須要編譯爲class字節碼,Java也是編譯爲class執行,也就是能夠大體理解爲Kotlin須要轉成Java同樣的語法結構, Kotlin就是一種強大的語法糖而已,Java不具有的功能Kotlin也不能越界的。

  • 那Kotlin的擴展函數怎麼實現的呢?介紹一種萬能的辦法去理解Kotlin的語法:將Kotlin代碼轉化成Java語言去理解,步驟以下:
    • 在Android Studio中選擇Tools ---> Kotlin ---> Show Kotlin Bytecode 這樣就把Kotlin轉化爲class字節碼了
    • class碼閱讀不太友好,點擊左上角的Decompile就轉化爲Java
  • 再介紹一個小竅門,在前期對Kotlin語法不熟悉的時候,能夠先用Java寫好代碼,再利用AndroidStudio工具將Java代碼轉化爲Kotlin代碼,步驟以下:
    • 在Android Studio中選中要轉換的Java代碼 ---> 選擇Code ---> Convert Java File to Kotlin File

咱們看看將上面的擴展函數轉成Java後的代碼

/* * 擴展函數會轉化爲一個靜態的函數,同時這個靜態函數的第一個參數就是該類的實例對象 * */
public static final char lastChar(@NotNull String $receiver) {
    Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
    return $receiver.charAt($receiver.length() - 1);
}
/* * 獲取的擴展屬性會轉化爲一個靜態的get函數,同時這個靜態函數的第一個參數就是該類的實例對象 * */
public static final char getLastChar(@NotNull StringBuilder $receiver) {
    Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
    return $receiver.charAt($receiver.length() - 1);
}
/* * 設置的擴展屬性會轉化爲一個靜態的set函數,同時這個靜態函數的第一個參數就是該類的實例對象 * */
public static final void setLastChar(@NotNull StringBuilder $receiver, char value) {
    Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
    $receiver.setCharAt($receiver.length() - 1, value);
}
複製代碼

查看上面的代碼可知:對於擴展函數,轉化爲Java的時候其實就是一個靜態的函數,同時這個靜態函數的第一個參數就是該類的實例對象,這樣把類的實例傳入函數之後,函數內部就能夠訪問到類的公有方法。 對於擴展屬性也相似,獲取的擴展屬性會轉化爲一個靜態的get函數,同時這個靜態函數的第一個參數就是該類的實例對象,設置的擴展屬性會轉化爲一個靜態的set函數,同時這個靜態函數的第一個參數就是該類的實例對象。 函數內部能夠訪問公有的方法和屬性。頂層的擴展函數是static的,不能被override

  • 從上面轉換的源碼其實能夠看到擴展函數和擴展屬性適用的地方和缺陷

    • 擴展函數和擴展屬性內只能訪問到類的公有方法和屬性,私有的和protected是訪問不了的
    • 擴展函數不是真的修改了原來的類,定義一個擴展函數不是將新成員函數插入到類中,擴展函數的類型是"靜態的",不是在運行時決定類型,案例代碼StaticllyExtension.kt
      open class C
      
      class D : C()
      
      fun C.foo() = "c"
      
      fun D.foo() = "d"
      
      /* * https://kotlinlang.org/docs/reference/extensions.html * Extensions do not actually modify classes they extend. By defining an extension, you do not insert new members into a class, * but merely make new functions callable with the dot-notation on variables of this type. Extension functions are * dispatched statically. * */
      fun printFoo(c: C) {
        println(c.foo())
      }
      
      fun testStatically() {
        printFoo(C()) // print c
        printFoo(D()) // also print c
      }
      複製代碼
      上面的案例中即便調用printFoo(D())仍是打印出c,而不是d。轉成java中會看到下面的代碼,D類型在調用的時候會強制轉換爲C類型:
      public static final String foo(@NotNull C $receiver) {
        Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
        return "c";
      }
      
      public static final String foo(@NotNull D $receiver) {
        Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
        return "d";
      }
      
      public static final void printFoo(@NotNull C c) {
        Intrinsics.checkParameterIsNotNull(c, "c");
        String var1 = foo(c);
        System.out.println(var1);
      }
      public static final void testStatically() {
        printFoo(new C());
        printFoo((C)(new D()));
      }
      複製代碼
  • 聲明擴展函數做爲類的成員變量

    • 上面的例子擴展函數是做爲頂層函數,若是把擴展函數申明爲類的成員變量,即擴展函數的做用域就在類的內部有效,案例代碼ExtensionsAsMembers.kt
open class D {
}

class D1 : D() {
}

open class C {
    open fun D.foo() {
        println("D.foo in C")
    }

    open fun D1.foo() {
        println("D1.foo in C")
    }

    fun caller(d: D) {
        d.foo()   // call the extension function
    }
}

class C1 : C() {
    override fun D.foo() {
        println("D.foo in C1")
    }

    override fun D1.foo() {
        println("D1.foo in C1")
    }
}

fun testAsMembers() {
    C().caller(D())   // prints "D.foo in C"
    C1().caller(D())  // prints "D.foo in C1" - dispatch receiver is resolved virtually
    C().caller(D1())  // prints "D.foo in C" - extension receiver is resolved statically
    C1().caller(D1()) // prints "D.foo in C1"
}
複製代碼

函數caller的類型是D,即便調用C().caller(D1()),打印的結果仍是D.foo in C,而不是D1.foo in C,不是運行時來動態決定類型,成員擴展函數申明爲open, 一旦在子類中被override,就調用不到在父類中的擴展函數,在子類中的做用域內的只能訪問到override後的函數,不能像普通函數override那樣經過super關鍵字訪問了。

  • 下面再舉幾個擴展函數的例子,讓你們感覺一下擴展函數的方便:
/* * show toast in activity * */
fun Activity.toast(msg: String) {
    Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}

val Context.inputMethodManager: InputMethodManager?
    get() = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager

/* * hide soft input * */
fun Context.hideSoftInput(view: View) {
    inputMethodManager?.hideSoftInputFromWindow(view.windowToken, 0)
}


/** * screen width in pixels */
val Context.screenWidth
    get() = resources.displayMetrics.widthPixels

/** * screen height in pixels */
val Context.screenHeight
    get() = resources.displayMetrics.heightPixels

/** * returns dip(dp) dimension value in pixels * @param value dp */
fun Context.dip2px(value: Int): Int = (value * resources.displayMetrics.density).toInt()
複製代碼

Tip5- 懶初始化by lazy 和 延遲初始化lateinit

懶初始化by lazy

懶初始化是指推遲一個變量的初始化時機,變量在使用的時候纔去實例化,這樣會更加的高效。由於咱們一般會遇到這樣的狀況,一個變量直到使用時才須要被初始化,或者僅僅是它的初始化依賴於某些沒法當即得到的上下文。 詳見案例代碼KotlinTip5

/* * 懶初始化api實例 * */
val purchasingApi: PurchasingApi by lazy {
    val retrofit: Retrofit = Retrofit.Builder()
            .baseUrl(API_URL)
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
    retrofit.create(PurchasingApi::class.java)
}
複製代碼

像上面的代碼,retrofit生成的api實例會在首次使用到的時候纔去實例化。須要注意的是by lazy通常只能修飾val不變的對象,不能修飾var可變對象。

class User(var name: String, var age: Int)

/* * 懶初始化by lazy * */
val user1: User by lazy {
    User("jack", 15)
}
複製代碼

延遲初始化lateinit

另外,對於var的變量,若是類型是非空的,是必須初始化的,否則編譯不經過,這時候須要用到lateinit延遲初始化,使用的時候再去實例化。

/* * 延遲初始化lateinit * */
lateinit var user2: User

fun testLateInit() {
    user2 = User("Lily", 14)
}
複製代碼

by lazy 和 lateinit 的區別

  • by lazy 修飾val的變量
  • lateinit 修飾var的變量,且變量是非空的類型

Tip6- 不用再手寫findViewById

在Android的View中,會有不少代碼是在聲明一個View,而後經過findViewById後從xml中實例化賦值給對應的View。在kotlin中能夠徹底解放出來了,利用kotlin-android-extensions插件,不用再手寫findViewById。步驟以下: 詳見案例代碼KotlinTip6

  • 步驟1,在項目的gradle中 apply plugin: 'kotlin-android-extensions'
  • 步驟2,按照原來的習慣書寫佈局xml文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">

    <TextView android:id="@+id/tip6Tv" android:layout_width="match_parent" android:layout_height="wrap_content" />

    <ImageView android:id="@+id/tip6Img" android:layout_width="match_parent" android:layout_height="wrap_content" />

    <Button android:id="@+id/tip6Btn" android:layout_width="match_parent" android:layout_height="wrap_content" />

</LinearLayout>
複製代碼
  • 步驟3,在java代碼中import對應的佈局就能夠開始使用了,View不用提早聲明,插件會自動根據佈局的id生成對應的View成員(其實沒有生成屬性,原理見下面)
import com.sw.kotlin.tips.R
/* * 導入插件生成的View * */
import kotlinx.android.synthetic.main.activity_tip6.*


class KotlinTip6 : Activity() {

    /* * 自動根據layout的id生成對應的view * */
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_tip6)
        tip6Tv.text = "Auto find view for TextView"
        tip6Img.setImageBitmap(null)
        tip6Btn.setOnClickListener {
            test()
        }
    }

    private fun test(){
        tip6Tv.text = "update"
    }

}
複製代碼

像上面代碼這樣,Activity裏的三個View自動生成了,不用再去聲明,而後findViewById,而後轉型賦值,是否是減小了不少不必的代碼,讓代碼很是的乾淨。

Why?原理是什麼?插件幫咱們作了什麼?

要看原理仍是將上面的代碼轉爲java語言來理解,參照tips4提供的方式轉換爲以下的java代碼:

public final class KotlinTip6 extends Activity {
   private HashMap _$_findViewCache;

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131296284);
      TextView var10000 = (TextView)this._$_findCachedViewById(id.tip6Tv);
      Intrinsics.checkExpressionValueIsNotNull(var10000, "tip6Tv");
      var10000.setText((CharSequence)"Auto find view for TextView");
      ((ImageView)this._$_findCachedViewById(id.tip6Img)).setImageBitmap((Bitmap)null);
      ((Button)this._$_findCachedViewById(id.tip6Btn)).setOnClickListener((OnClickListener)(new OnClickListener() {
         public final void onClick(View it) {
            KotlinTip6.this.test();
         }
      }));
   }

   private final void test() {
      TextView var10000 = (TextView)this._$_findCachedViewById(id.tip6Tv);
      Intrinsics.checkExpressionValueIsNotNull(var10000, "tip6Tv");
      var10000.setText((CharSequence) "update");
   }

   public View _$_findCachedViewById(int var1) {
      if (this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
      if (var2 == null) {
         var2 = this.findViewById(var1);
         this._$_findViewCache.put(Integer.valueOf(var1), var2);
      }

      return var2;
   }

   public void _$_clearFindViewByIdCache() {
      if (this._$_findViewCache != null) {
         this._$_findViewCache.clear();
      }
   }
}
複製代碼

如上面的代碼所示,在編譯階段,插件會幫咱們生成視圖緩存,視圖由一個Hashmap結構的_$_findViewCache變量緩存, 會根據對應的id先從緩存裏查找,緩存沒命中再去真正調用findViewById查找出來,再存在HashMap中。

在fragment中findViewByID

在fragment中也相似,有一點區別,例子以下:

class Tip6Fragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater?.inflate(R.layout.fragment_tip6, container, false)
        /* * 這時候不能在onCreateView方法裏用view,須要在onViewCreate裏,原理是插件用了getView來findViewById * */
        // tip6Tv.text = "test2"
        return view
    }

    /* * 須要在onViewCreate裏,原理是插件用了getView來findViewById * */
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        tip6Tv.text = "test"
    }
}
複製代碼

如上所示,Fragment須要注意,不能在onCreateView方法裏用view,否則會出現空指針異常,須要在onViewCreate裏,原理是插件用了getView來findViewById, 咱們看看將上面的代碼轉成java後的代碼:

public final class Tip6Fragment extends Fragment {
   private HashMap _$_findViewCache;

   @Nullable
   public View onCreateView(@Nullable LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
      View view = inflater != null?inflater.inflate(2131296286, container, false):null;
      return view;
   }

   public void onViewCreated(@Nullable View view, @Nullable Bundle savedInstanceState) {
      super.onViewCreated(view, savedInstanceState);
      TextView var10000 = (TextView)this._$_findCachedViewById(id.tip6Tv);
      Intrinsics.checkExpressionValueIsNotNull(var10000, "tip6Tv");
      var10000.setText((CharSequence)"test");
   }

   public View _$_findCachedViewById(int var1) {
      if (this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
      if (var2 == null) {
         View var10000 = this.getView();
         if (var10000 == null) {
            return null;
         }

         var2 = var10000.findViewById(var1);
         this._$_findViewCache.put(Integer.valueOf(var1), var2);
      }

      return var2;
   }

   public void _$_clearFindViewByIdCache() {
      if (this._$_findViewCache != null) {
         this._$_findViewCache.clear();
      }

   }

   // $FF: synthetic method
   public void onDestroyView() {
      super.onDestroyView();
      this._$_clearFindViewByIdCache();
   }
}
複製代碼

跟Activity中相似,會有一個View的HashMap,關鍵不一樣的地方在__findCachedViewById裏面,須要getView得到當前Fragment的View,
故在onViewCreated中getView仍是空的,原理就好理解了。另外在onDestroyView會調用__clearFindViewByIdCache方法清掉緩存。

Tip7- 利用局部函數抽取重複代碼

Kotlin中提供了函數的嵌套,在函數內部還能夠定義新的函數。這樣咱們能夠在函數中嵌套這些提早的函數,來抽取重複代碼。以下面的案例所示: 詳見案例代碼KotlinTip7

class User(val id: Int, val name: String, val address: String, val email: String)

fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}: empty Name")
    }
    if (user.address.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}: empty Address")
    }
    if (user.email.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}: empty Email")
    }
    // save to db ...
}
複製代碼

上面的代碼在判斷name、address等是否爲空的處理其實很相似。這時候,咱們能夠利用在函數內部嵌套的聲明一個通用的判空函數將相同的代碼抽取到一塊兒:

/* * 利用局部函數抽取相同的邏輯,去除重複的代碼 * */
fun saveUser2(user: User) {
    fun validate(value: String, fildName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user ${user.id}: empty $fildName")
        }
    }

    validate(user.name, "Name")
    validate(user.address, "Address")
    validate(user.email, "Email")
    // save to db ...
}
複製代碼

除了利用嵌套函數去抽取,此時,其實也能夠用擴展函數來抽取,以下所示:

/* * 利用擴展函數抽取邏輯 * */
fun User.validateAll() {
    fun validate(value: String, fildName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user $id: empty $fildName")
        }
    }

    validate(name, "Name")
    validate(address, "Address")
    validate(email, "Email")
}

fun saveUser3(user: User) {
    user.validateAll()
    // save to db ...
}
複製代碼

Tip8- 使用數據類來快速實現model類

在java中要聲明一個model類須要實現不少的代碼,首先須要將變量聲明爲private,而後須要實現get和set方法,還要實現對應的hashcode equals toString方法等,以下所示: 詳見案例代碼Tip8

public static class User {

        private String name;
        private int age;
        private int gender;
        private String address;
        
        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public int getGender() {
            return gender;
        }

        public void setGender(int gender) {
            this.gender = gender;
        }

        public String getAddress() {
            return address;
        }

        public void setAddress(String address) {
            this.address = address;
        }

        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", gender=" + gender +
                    ", address='" + address + '\'' +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            User user = (User) o;

            if (age != user.age) return false;
            if (gender != user.gender) return false;
            if (name != null ? !name.equals(user.name) : user.name != null) return false;
            return address != null ? address.equals(user.address) : user.address == null;
        }

        @Override
        public int hashCode() {
            int result = name != null ? name.hashCode() : 0;
            result = 31 * result + age;
            result = 31 * result + gender;
            result = 31 * result + (address != null ? address.hashCode() : 0);
            return result;
        }
    }
複製代碼

這段代碼Java須要70行左右,而若是用kotlin,只須要一行代碼就能夠作到。

/* * Kotlin會爲類的參數自動實現get set方法 * */
class User(val name: String, val age: Int, val gender: Int, var address: String)

/* * 用data關鍵詞來聲明一個數據類,除了會自動實現get set,還會自動生成equals hashcode toString * */
data class User2(val name: String, val age: Int, val gender: Int, var address: String)
複製代碼

對於Kotlin中的類,會爲它的參數自動實現get set方法。而若是加上data關鍵字,還會自動生成equals hashcode toString。原理其實數據類中的大部分代碼都是模版代碼,Kotlin聰明的將這個模版代碼的實現放在了編譯器處理的階段。

Tip9- 用類委託來快速實現裝飾器模式

經過繼承的實現容易致使脆弱性,例如若是須要修改其餘類的一些行爲,這時候Java中的一種策略是採用裝飾器模式:建立一個新類,實現與原始類同樣的接口並將原來的類的實例做爲一個成員變量。 與原始類擁有相同行爲的方法不用修改,只須要直接轉發給原始類的實例。以下所示: 詳見案例代碼KotlinTip9

/* * 常見的裝飾器模式,爲了修改部分的函數,卻須要實現全部的接口函數 * */
class CountingSet<T>(val innerSet: MutableCollection<T> = HashSet<T>()) : MutableCollection<T> {

    var objectAdded = 0
    
    override val size: Int
        get() = innerSet.size

    /* * 須要修改的方法 * */
    override fun add(element: T): Boolean {
        objectAdded++
        return innerSet.add(element)
    }

    /* * 須要修改的方法 * */
    override fun addAll(elements: Collection<T>): Boolean {
        objectAdded += elements.size
        return innerSet.addAll(elements)
    }

    override fun contains(element: T): Boolean {
        return innerSet.contains(element)
    }

    override fun containsAll(elements: Collection<T>): Boolean {
        return innerSet.containsAll(elements)
    }

    override fun isEmpty(): Boolean {
        return innerSet.isEmpty()
    }

    override fun clear() {
        innerSet.clear()
    }

    override fun iterator(): MutableIterator<T> {
        return innerSet.iterator()
    }

    override fun remove(element: T): Boolean {
        return innerSet.remove(element)
    }

    override fun removeAll(elements: Collection<T>): Boolean {
        return innerSet.removeAll(elements)
    }

    override fun retainAll(elements: Collection<T>): Boolean {
        return innerSet.retainAll(elements)
    }

}
複製代碼

如上所示,想要修改HashSet的某些行爲函數add和addAll,須要實現MutableCollection接口的全部方法,將這些方法轉發給innerSet去具體的實現。雖然只須要修改其中的兩個方法,其餘代碼都是模版代碼。 只要是重複的模版代碼,Kotlin這種全新的語法糖就會想辦法將它放在編譯階段再去生成。 這時候能夠用到類委託by關鍵字,以下所示:

/* * 經過by關鍵字將接口的實現委託給innerSet成員變量,須要修改的函數再去override就能夠了 * */
class CountingSet2<T>(val innerSet: MutableCollection<T> = HashSet<T>()) : MutableCollection<T> by innerSet {

    var objectAdded = 0

    override fun add(element: T): Boolean {
        objectAdded++
        return innerSet.add(element)
    }

    override fun addAll(elements: Collection<T>): Boolean {
        objectAdded += elements.size
        return innerSet.addAll(elements)
    }
}
複製代碼

經過by關鍵字將接口的實現委託給innerSet成員變量,須要修改的函數再去override就能夠了,經過類委託將10行代碼就能夠實現上面接近100行的功能,簡潔明瞭,去掉了模版代碼。

Tip10- Lambda表達式簡化OnClickListener

詳見案例代碼KotlinTip10 lambda表達式能夠簡化咱們的代碼。以Android中常見的OnClickListener來講明,在Java中咱們通常這樣設置:

TextView textView = new TextView(context);
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // handle click
            }
        });
複製代碼

Java中須要聲明一個匿名內部類去處理,這種狀況能夠用lambda表達式來簡化。

  • lambda表達式通常長這樣
    • { x:Int, y:Int -> x+y }
    • 參數 -> 表達式 而且始終在大括號中
    • it做爲默認參數名
    • lambda捕捉,當捕捉final變量時,它的值和lambda代碼一塊兒存儲
    • 非final變量,它的值被封裝在一個特殊的包裝器中,這個包裝器的引用會和lambda代碼一塊兒存儲

咱們來看看Kotlin中的例子:

val textView = TextView(context)

    /* * 傳統方式 * */
    textView.setOnClickListener(object : android.view.View.OnClickListener {
        override fun onClick(v: android.view.View?) {
            // handle click
        }
    })

    /* * lambda的方式 * */
    textView.setOnClickListener({ v ->
        {
            // handle click
        }
    })
複製代碼

當lambda的參數沒有使用時能夠省略,省略的時候用it來替代

/* * lambda的參數若是沒有使用能夠省略,省略的時候用it來替代 * */
    textView.setOnClickListener({
        // handle click
    })
複製代碼

lambda在參數的最後一個的狀況能夠將之提出去

/* * lambda在參數的最後一個的狀況能夠將之提出去 * */
    textView.setOnClickListener() {
        // handle click
    }
複製代碼

lambda提出去以後,函數若是沒有其餘參數括號能夠省略

/* * lambda提出去以後,函數若是沒有其餘參數括號能夠省略 * */
    textView.setOnClickListener {
        // handle click
    }
複製代碼

咱們再看看若是本身去實現一個帶lambda參數的函數應該怎麼去定義:

interface OnClickListener {
    fun onClick()
}

class View {
    var listener: OnClickListener? = null;

    /* * 傳統方式 * */
    fun setOnClickListener(listener: OnClickListener) {
        this.listener = listener
    }

    fun doSth() {
        // some case:
        listener?.onClick()
    }

    /* * 聲明lambda方式,listener: () -> Unit * */
    fun setOnClickListener(listener: () -> Unit) {

    }
}
複製代碼

在函數參數中須要聲明lambda的類型後,再調用該函數的時候就能夠傳入一個lambda表達式了。

Tip11- with函數來簡化代碼

  • with 函數原型:
inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
複製代碼
  • with函數並非擴展函數,返回值是最後一行,能夠直接調用對象的方法

Kotlin中能夠用with語句來省略同一個變量的屢次聲明,例以下面的函數 詳見案例代碼KotlinTip11

/* *打印字母表函數,在函數內result變量在好幾處有使用到 * */
fun alphabet(): String {
    val result = StringBuilder()
    result.append("START\n")
    for (letter in 'A'..'Z') {
        result.append(letter)
    }
    result.append("\nEND")
    return result.toString()
}
複製代碼

在上面的函數中,result變量出現了5次,若是用with語句,能夠將這5次都不用再出現了,咱們來一步一步地看是怎麼實現的:

/* * 經過with語句,將result做爲參數傳入,在內部就能夠經過this來表示result變量了 * */
fun alphabet2(): String {
    val result = StringBuilder()
    return with(result) {
        this.append("START\n")
        for (letter in 'A'..'Z') {
            this.append(letter)
        }
        this.append("\nEND")
        this.toString()
    }
}
複製代碼

經過with語句,將result做爲參數傳入,在內部就能夠經過this來表示result變量了,並且這個this是能夠省略的

/* * 經過with語句,將result參數做爲參數,在內部this也能夠省略掉 * */
fun alphabet3(): String {
    val result = StringBuilder()
    return with(result) {
        append("START\n")
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\nEND")
        toString()
    }
}
複製代碼

在內部this省略掉後,如今只有一個result了,這個其實也是不必的,因而出現了下面的最終版本:

/* * 經過with語句,能夠直接將對象傳入,省掉對象的聲明 * */
fun alphabet4(): String {
    return with(StringBuilder()) {
        append("START\n")
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\nEND")
        toString()
    }
}
複製代碼

像上面這樣,咱們能夠把同一個變量的顯式調用從5次變爲0次,發現Kotlin的魅力了吧。

Tip12- apply函數來簡化代碼

  • apply 函數原型:
inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
複製代碼
  • apply函數,在函數範圍內,能夠任意調用該對象的任意方法,並返回該對象

除了用上面的with能夠簡化同一個變量的屢次聲明,還能夠用apply關鍵字,咱們來改造一下tip11中的函數: 詳見案例代碼KotlinTip12

/* * 用apply語句簡化代碼,在apply的大括號裏能夠訪問類的公有屬性和方法 * */
fun alphabet5() = StringBuilder().apply {
    append("START\n")
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nEND")
}.toString()
複製代碼

像上面這樣的,經過apply後,在apply的大括號裏能夠訪問類的公有屬性和方法。這在對應類的初始化是很是方便的,例以下面的例子

/* * 用apply語句簡化類的初始化,在類實例化的時候,就能夠經過apply把須要初始化的步驟所有實現,很是的簡潔 * */
fun testApply(context: Context) {
    var imgView = ImageView(context).apply {
        setBackgroundColor(0)
        setImageBitmap(null)
    }

    var textView = TextView(context).apply {
        text = "content"
        textSize = 20.0f
        setPadding(10, 0, 0, 0)
    }
    
    var user = User().apply { 
        age = 15
        name = "Jack"
        val a = address
        address = "bbb"
    }
}
複製代碼

在類實例化的時候,就能夠經過apply把須要初始化的步驟所有實現,很是的簡潔

Tip13- 在編譯階段避免掉NullPointerException

NullPointerException是Java程序員很是頭痛的一個問題,咱們知道Java 中分受檢異常和非受檢異常,NullPointerException是非受檢異常,也就是說NullPointerException不須要顯示的去catch住, 每每在運行期間,程序就可能報出一個NullPointerException而後crash掉,Kotlin做爲一門高效安全的語言,它嘗試在編譯階段就把空指針問題顯式的檢測出來,把問題留在了編譯階段,讓程序更加健壯。 詳見案例代碼KotlinTip13

  • Kotlin中類型分爲可空類型和不可空類型,經過?表明可空,不帶?表明不可爲空
fun testNullType() {
    val a: String = "aa"
    /* * a是非空類型,下面的給a賦值爲null將會編譯不經過 * */
    // a = null
    a.length

    /* * ?聲明是可空類型,能夠賦值爲null * */
    var b: String? = "bb"
    b = null
    
    /* * b是可空類型,直接訪問可空類型將編譯不經過,須要經過?.或者!!.來訪問 * */
    // b.length
    b?.length
    b!!.length
}
複製代碼
  • 對於一個不可爲空類型:若是直接給不可爲空類型賦值一個可能爲空的對象就在編譯階段就不能經過
  • 對於一個可空類型:經過?聲明,在訪問該類型的時候直接訪問不能編譯經過,須要經過?.或者!!.
    • ?. 表明着若是該類型爲空的話就返回null不作後續的操做,若是不爲空的話纔會去訪問對應的方法或者屬性
    • !!. 表明着若是該類型爲空的話就拋出NullPointerException,若是不爲空就去訪問對應的方法或者屬性, 因此只有在不多的特定場景才用這種符號,表明着程序不處理這種異常的case了,會像java代碼同樣拋出NullPointerException。 並且代碼中必定不用出現下面這種代碼,會讓代碼可讀性不好並且若是有空指針異常,咱們也不能立刻發現是哪空了:
/* * 不推薦這樣的寫法:鏈式的連續用!!. * */
    val user = User()
    user!!.name!!.subSequence(0,5)!!.length
複製代碼

對應一個可空類型,每次對它的訪問都須要帶上?.判斷

val user: User? = User()

    /* * 每次訪問都用用?.判斷 * */
    user?.name
    user?.age
    user?.toString()
複製代碼

但這樣多了不少代碼,kotlin作了一些優化,

/* * 或者提早判斷是否爲空,若是不爲空在這個分支裏會自動轉化爲非空類型就能夠直接訪問了 * */
    if (user != null) {
        user.name
        user.age
        user.toString()
    }
複製代碼

經過if提早判斷類型是否爲空,若是不爲空在這個分支裏會自動轉化爲非空類型就能夠直接訪問了。

let語句簡化對可空對象對訪問

  • let 函數原型:
inline fun <T, R> T.let(block: (T) -> R): R = block(this)
複製代碼
  • let函數默認當前這個對象做爲閉包的it參數,返回值是函數裏面最後一行,或者指定return。

上面的代碼還能夠用?.let語句進行,以下所示:

/* * 經過let語句,在?.let以後,若是爲空不會有任何操做,只有在非空的時候纔會執行let以後的操做 * */
    user?.let {
        it.name
        it.age
        it.toString()
    }
複製代碼

經過let語句,在?.let以後,若是爲空不會有任何操做,只有在非空的時候纔會執行let以後的操做

Elvis操做符 ?: 簡化對空值的處理

若是值可能爲空,對空值的處理可能會比較麻煩,像下面這樣:

/* * 對空值的處理 * */
fun testElvis(input: String?, user: User?) {
    val a: Int?
    if (input == null) {
        a = -1
    } else {
        a = input?.length
    }

    if (user == null) {
        var newOne = User()
        newOne.save()
    } else {
        user.save()
    }
}
複製代碼

Elvis操做符?:可以簡化上面的操做,?:符號會在符號左邊爲空的狀況纔會進行下面的處理,不爲空則不會有任何操做。跟?.let正好相反,例如咱們能夠用兩行代碼來簡化上面從操做:

/** * Elvis操做符 ?: 簡化對空值的處理 */
fun testElvis2(input: String?, user: User?) {
    val b = input?.length ?: -1;
    user?.save() ?: User().save()
}
複製代碼

Tip14- 運算符重載

Kotlin支持對運算符的重載,這對於對一些對象的操做更加靈活直觀。

  • 使用operator來修飾plus\minus函數
  • 可重載的二元算術符
    • A * B times
    • A / B div
    • A % B mod
    • A + B plus
    • A - B minus

如下面對座標點Point的案例說明怎麼去重載運算符: 詳見案例代碼KotlinTip14

class Point(val x: Int, val y: Int) {

    /* * plus函數重載對Point對象的加法運算符 * */
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }

    /* * minus函數重載對Point對象的減法運算符 * */
    operator fun minus(other: Point): Point {
        return Point(x - other.x, y - other.y)
    }

    override fun toString(): String {
        return "[x:$x, y:$y]"
    }

}
複製代碼

如上所示,經過plus函數重載對Point對象的加法運算符,經過minus函數重載對Point對象的減法運算符,而後就能夠用+、-號對兩個對象進行操做了:

fun testOperator() {
    val point1 = Point(10, 10)
    val point2 = Point(4, 4)
    val point3 = point1 + point2
    println(point3)
    println(point1 - point2)
}
複製代碼

Tip15- 高階函數簡化代碼

  • 高階函數:以另外一個函數做爲參數或者返回值的函數
  • 函數類型
    • (Int, String) -> Unit
    • 參數類型->返回類型 Unit不能省略
val list = listOf(2, 5, 10)
    /* * 傳入函數來過濾 * */
    println(list.filter { it > 4 })
      
    /* * 定義函數類型 * */
    val sum = { x: Int, y: Int -> x + y }
    val action = { println(42) }

    val sum2: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
    val action2: () -> Unit = { println(42) }      
複製代碼

函數做爲參數

函數做爲參數,即高階函數中,函數的參數能夠是一個函數類型,例如要定義一個函數,該函數根據傳入的操做函數來對2和3作相應的處理。 詳見案例代碼KotlinTip15

/* * 定義對2和3的操做函數 * */
fun twoAndThree(operator: (Int, Int) -> Int) {
    val result = operator(2, 3)
    println("Result:$result")
}

fun test03() {
    twoAndThree { a, b -> a + b }
    twoAndThree { a, b -> a * b }
}
複製代碼

operator是函數類型,函數的具體類型爲(Int, Int) -> Int,即輸入兩個Int返回一個Int值。定義完了後就能夠像上面這樣使用了。 再舉一個例子,實現String類的字符過濾:

/* * 函數做爲參數,實現String類的字符過濾 * */
fun String.filter(predicate: (Char) -> Boolean): String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        if (predicate(element)) sb.append(element)
    }
    return sb.toString()
}

fun test04() {
    println("12eafsfsfdbzzsa".filter { it in 'a'..'f' })
}
複製代碼

像上面這樣predicate是函數類型,它會根據傳入的char來判斷獲得一個Boolean值。

函數做爲返回值

函數做爲返回值也很是實用,例如咱們的需求是根據不一樣的快遞類型返回不一樣計價公式,普通快遞和高級快遞的計價規則不同,這時候咱們能夠將計價規則函數做爲返回值:

enum class Delivery {
    STANDARD, EXPEDITED
}

/* * 根據不一樣的運輸類型返回不一樣的快遞方式 * */
fun getShippingCostCalculator(delivery: Delivery): (Int) -> Double {
    if (delivery == Delivery.EXPEDITED) {
        return { 6 + 2.1 * it }
    }
    return { 1.3 * it }
}

fun test05() {
    val calculator1 = getShippingCostCalculator(Delivery.EXPEDITED)
    val calculator2 = getShippingCostCalculator(Delivery.STANDARD)
    println("Ex costs ${calculator1(5)}")
    println("St costs ${calculator2(5)}")
}
複製代碼

若是是普通快遞,採用1.3 * it的規則計算價格,若是是高級快遞按照6 + 2.1 * it計算價格,根據不一樣的類型返回不一樣的計價函數。

Tip16- 用Lambda來簡化策略模式

策略模式是常見的模式之一,java的例子以下。 詳見案例代碼Tip16

/** * 定義策略接口 */
    public interface Strategy {
        void doSth();
    }

    /** * A策略 */
    public static class AStrategy implements Strategy {
        @Override
        public void doSth() {
            System.out.println("Do A Strategy");
        }
    }

    /** * B策略 */
    public static class BStrategy implements Strategy {
        @Override
        public void doSth() {
            System.out.println("Do B Strategy");
        }
    }

    /** * 策略實施者 */
    public static class Worker {

        private Strategy strategy;

        public Worker(Strategy strategy) {
            this.strategy = strategy;
        }

        public void work() {
            System.out.println("START");
            if (strategy != null) {
                strategy.doSth();
            }
            System.out.println("END");
        }
    }
複製代碼

如上面的例子所示,有A、B兩種策略,Worker根據不一樣的策略作不一樣的工做,使用策略時:

Worker worker1 = new Worker(new AStrategy());
    Worker worker2 = new Worker(new BStrategy());
    worker1.work();
    worker2.work();
複製代碼

在java中實現這種策略模式不免須要先定義好策略的接口,而後根據接口實現不一樣的策略, 在Kotlin中徹底能夠用用Lambda來簡化策略模式,上面的例子用Kotlin實現:

/** * 策略實施者 * @param strategy lambda類型的策略 */
class Worker(private val strategy: () -> Unit) {
    fun work() {
        println("START")
        strategy.invoke()
        println("END")
    }
}

/* * 測試 * */
fun testStrategy() {
    val worker1 = Worker({
        println("Do A Strategy")
    })
    val bStrategy = {
        println("Do B Strategy")
    }
    val worker2 = Worker(bStrategy)
    worker1.work()
    worker2.work()
}
複製代碼

不須要先定義策略的接口,直接把策略以lambda表達式的形式傳進來就好了。

參考文檔

  • 《Kotlin in Action》
  • https://kotlinlang.org/docs/reference/
  • https://savvyapps.com/blog/kotlin-tips-android-development
相關文章
相關標籤/搜索