Kotlin 介紹

Kotlin (0:00)

你們好,我是 Michael Pardo,今天我要給你們展現一下 Kotlin 這門語言,同時看看他如何讓你在 Android 開發的時候更開心,更有效率。javascript

Kotlin 是一個基於 JVM 實現的靜態語言。Kotlin 是 JetBrains 創造並在持續維護這門語言,對,就是那個創造了 Android Studio 和 IntelliJ 的公司。html

Kotlin 有幾個核心的目標:java

  1. 簡約:幫你減小實現同一個功能的代碼量。
  2. 易懂:讓你的代碼更容易閱讀,同時易於理解。
  3. 安全:移除了你可能會犯錯誤的功能。
  4. 通用:基於 JVM 和 Javascript,你能夠在不少地方運行。
  5. 互操做性:這就意味着 Kotlin 和 Java 能夠相互調用,同時 Jetbrains 的目標是讓他們 100% 兼容。

爲何不等 Java 8?android

Java 和 Android: 一段歷史 (1:11)

咱們來看看 Java 和 Android 的歷史以及他們的關係。 在 2006 年,Java 6 發佈了。幾年以後,Android 1 的 Alpha 版本發佈了,四年後,Java 7 發佈了。Android 在 2 年後緊隨其後的開始支持 Java 7。去年,Java 8 又發佈了。express

你想一想,你何時才能用上 Java 8? 可能你學的很快,而後就能用上 Java 8。可是 Android 怎麼說都得幾年後才能開始支持 Java 8,你們適應 Java 8 又須要很長時間。Android 如今的碎片化很嚴重,Java 7 只支持 API 19 及以上。若是用了 Java 7,那你的 App 用戶羣一會兒就少了一半。即使咱們如今有了 Java 8,100% 的覆蓋到了全部的用戶設備上,可是 Java 自己仍是有些問題的。設計模式

咱們來看看 Java 的一些問題。數組

Java 有哪些問題? (2:22)

  • 空引用(Null references):連空引用的發明者都成這是個 billion-dollar 錯誤(參見)。不論你費多大的功夫,你都沒法避免它。由於 Java 的類型系統就是不安全的。
  • 原始類型(Raw types):咱們在開發的時候老是會爲了保持兼容性而卡在範型原始類型的問題上,咱們都知道要努力避免 raw type 的警告,可是它們畢竟是在語言層面上的存在,這一定會形成誤解和不安全因素。
  • 協變數組(Covariant arrays):你能夠建立一個 string 類型的數組和一個 object 型的數組,而後把 string 數組分配給 object 數組。這樣的代碼能夠經過編譯,可是一旦你嘗試在運行時分配一個數給那個數組的時候,他就會在運行時拋出異常。
  • Java 8 存在高階方法( higher-order functions ),可是他們是經過 SAM 類型 實現的。SAM 是一個單個抽象方法,每一個函數類型都須要一個對應的接口。若是你想要建立一個並不存在的 lambda 的時候或者不存着對應的函數類型的時候,你要本身去建立函數類型做爲接口。
  • 泛型中的通配符:詭異的泛型老是難以操做,難以閱讀,書寫,以及理解。對編譯器而言,異常檢查也變得很困難。

咱們來探索下 Kotlin 是如何解決上面的提到的這些問題的。安全

Kotlin To The Rescue! (4:24)

剛纔咱們提到過的這些缺陷,Kotlin 一般直接移除了那些特性。同時它也加了一些新的特性:app

  • Lambda 表達式
  • 數據類 (Data classes)
  • 函數字面量和內聯函數(Function literals & inline functions)
  • 函數擴展 (Extension functions)
  • 空安全(Null safety)
  • 智能轉換(Smart casts)
  • 字符串模板(String templates)
  • 主構造函數(Primary constructors)
  • 類委託(Class delegation)
  • 類型推斷(Type inference)
  • 單例(Singletons)
  • 聲明點變量(Declaration-site variance)
  • 區間表達式(Range expressions)

咱們將在這篇文章裏說起以上大多數特性。Kotlin 之因此能跟隨者 JVM 的生態系統不斷地進步,是由於他沒有任何限制。它編譯出來的正是 JVM 字節碼。在 JVM 看來,它就跟其餘語言同樣樣的。事實上,若是你在 IntelliJ 或者 Android Studio 上用 Kotlin 的插件,它自帶裏一個字節碼查看器,能夠顯示每一個方法生成的 JVM 字節碼。dom

Hello, Kotlin (5:19)

咱們來看看基本語法。下面是一個最簡單的,用 Kotlin 書寫的 Hello World:

fun main(args: Array<String>): Unit {
  println("Hello, World!")
}

只有一個函數和一個 print 語句。不須要包聲明和類引用聲明。這就是一個 Kotlin Hello World 程序的全部代碼。聲明函數的關鍵字是 funfun 後面跟的是函數的名稱,而後括號包裹起來的是函數參數,這個跟 Java 相似。

然而,在 Kotlin 裏,得把參數名放在前面,參數類型放在後面,用一個冒號隔開。函數的返回類型在最後,這個跟 Java 放在前面形式不太同樣。若是一個函數沒有返回任何類型,能夠返回一個 Unit 類型,固然也能夠省略。調用 Kotlin 標準庫中的函數 println 就能打印 Hello World 出來,實際上它最終調用了 Java 的 system.out.println

fun main(args: Array<String>) {
  println("Hello, World!")
}

接下來,咱們來從 「Hello World」 中提取 「World」 這個詞,並把這個詞放到一個變量中。var 關鍵字後跟的是變量的名稱。Kotlin 支持字符串內插入變量,只用在字符串內用 $ 符號開頭,隨後跟上輸出變量的變量名便可,就像以下這樣:

fun main(args: Array<String>) {
  var name = "World"
  println("Hello, $name!")
}

隨後,咱們來檢查咱們是否給 main 函數傳遞了參數。先來判斷這個字符串數組是否是空,若是不爲空,咱們把第一個字符串分配給 name 變量。Kotlin 裏有個 val 類型的聲明方法,相似 Java 裏的 final,也就是常量。

fun main(args: Array<String>) {
  val name = "World"
  if (args.isNotEmpty()) {
    name = args[0]
  }

  println("Hello, $name!")
}

在咱們編譯這個程序的時候,咱們遇到一個問題:沒法從新分配新的值給一個常量。一種解決方法是用內聯的 if-else 方法。Kotlin 裏的多數的代碼塊都支持返回值。若是語句進入了 if 代碼塊兒,也就是說 args 非空,那麼就返回 arg[0],不然返回 「World」。 if-else 語句結束後,就直接賦值給咱們以前聲明的 name 常量,下面的例子就是條件賦值代碼塊:

fun main(args: Array<String>) {.
  val name = if (args.isNotEmpty()) {
    args[0]
  } else {
    "World"
  }

  println("Hello, $name!")
}

咱們能夠把上面的代碼用一行來書寫,看起來有點像 Java 裏的三目運算符。移除掉那些大括號後,看起至關漂亮:

val name = if (args.isNotEmpty()) args[0] else "World"

類語法 (5:19)

咱們來看看類。類的定義要經過 class 關鍵字,跟 Java 裏的同樣,關鍵字後是類名。Kotlin 有一個主構造函數,咱們能夠直接將構造函數參數列表寫在類的聲明處,還能夠直接用 var 或者 val關鍵字將參數聲明爲成員變量(又稱:類屬性),以下:

class Person(var name: String)

繼續以前的例子,有了主構造函數之後,咱們就再也不須要成員變量賦值語句了。在 Kotlin 裏建立實例的時候,沒必要使用 new 關鍵字。你只須要指明建立的類型名就能夠建立實例了。

class Person(var name: String)

fun main(args: Array<String>) {
  val person = Person("Michael")
  println("Hello, $name!")
}

很容易發現,字符串插值其實是錯誤的,由於 name 指向的是一個不存在的變量了。咱們能夠用剛纔提到的 字符串插值表達式 ,即用 $ 符號和大括號包裹想要插入的變量,來修復這個問題:

class Person(var name: String)

fun main(args: Array<String>) {
  val person = Person("Michael")
  println("Hello, ${person.name}!")
}

下面是 enum 類。枚舉跟 Java 裏的枚舉很像。定義一個枚舉的方法以下:

enum class Language(val greeting: String) {
  EN("Hello"), ES("Hola"), FR("Bonjour")
}

咱們來給 Person 類增長一個叫 lang 的屬性,表明一我的的所說的語言。

class Person(var name: String, var lang: Language = Language.EN)

Kotlin 支持參數默認值,如上:language 的默認值就是 Language.EN,這樣就能夠在建立實例的時候忽略這個參數,除非你要改變 language 的屬性值。咱們來把這個例子變得更面向對象一些,給 person 增長一個打招呼的方法,簡單地輸出特定語言打招呼的方法還有人名:

enum class Language(val greeting: String) {
  EN("Hello"), ES("Hola"), FR("Bonjour")
}

class Person(var name: String, var lang: Language = Language.EN) {
  fun greet() = println("${lang.greeting}, $name!")
}

fun main(args: Array<String>) {
  val person = Person("Michael")
  person.greet()
}

如今在 main 函數裏調用 person.greet() 方法,看看是否是很酷?!

集合和迭代 (11:32)

val people = listOf(
  Person("Michael"),
  Person("Miguel", Language.SP),
  Person("Michelle", Language.FR)
)

咱們能夠用標準庫函數 listOf 方法建立一個 person 列表。遍歷這些 person 能夠用 for-in 關鍵字:

for (person in people) {
  person.greet()
}

隨後,咱們能夠在每次遍歷的時候執行 person.greet() 方法,甚至能夠更簡單,直接調用 people 集合的擴展方法 forEach,傳入一個 lambda 表達式,在表達式裏用 it 表明每次遍歷到的person 對象,而後調用它們的 greet 方法。

people.forEach { it.greet() }

咱們來建立兩個新的類,每一個都傳入一個默認的語系。咱們能夠再也不像剛纔那樣重複聲明,能夠直接用繼承的方法來實現。下面是一個擴展版本的 Hello World。展現了不少 Kotlin 的特性:

enum class Language(val greeting: String) {
  EN("Hello"), ES("Hola"), FR("Bonjour")
}

open class Person(var name: String, var lang: Language = Language.EN) {
  fun greet() = println("${lang.greeting}, $name!")
}

class Hispanophone(name: String) : Person(name, Language.ES)
class Francophone(name: String) : Person(name, Language.FR)

fun main(args: Array<String>) {
  listOf(
    Person("Michael"),
    Hispanophone("Miguel"),
    Francophone("Michelle")
  ).forEach { it.greet() }
}

Kotlin 在 Java 上加了什麼特性? (13:11)

下面,咱們來看看 Kotlin 在 Java 之上加了哪些更好用的新特性。

類型推導 (13:18)

你可能在其餘語言中看到過類型推導。在 Java 裏,咱們須要本身聲明類型,變量名,以及數值。在 Kotlin 裏,順序有些不同,你先聲明變量名,而後是類型,而後是分配值。不少狀況下,你不須要聲明類型。一個字符串字面量足以指明這是個字符串類型。字符,整形,長整形,單浮點數,雙浮點數,布爾值都是能夠無需顯性聲明類型的。

var string: String = ""
var string = ""
var char = ' '

var int = 1
var long = 0L
var float = 0F
var double = 0.0

var boolean = true

var foo = MyFooType()

只要 Kotlin 能夠推導,這個規則一樣適用與其餘一些類型。一般,若是是局部變量,當你在聲明一個值或者變量的時候你不須要指明類型。在一些沒法推導的場景裏,你才須要用完整的聲明變量語法指明變量類型。

空安全(null-safety) (14:22)

Kotlin 一個強大的特性是空安全。咱們來看幾個例子:

String a = null; System.out.println(a.length());

在 Java 裏,聲明一個 string 類型,賦一個 null 給這個變量。一旦咱們要打印這個字符串的時候,會在運行時曝出空指針錯誤,由於咱們在嘗試去讀一個空值。下面是這個問題的 kotlin 寫法,咱們定義一個空值,可是在咱們嘗試操做它以前,Kotlin 的編譯器就告訴了咱們問題所在:

val a:String = null

曝出的錯誤是:咱們在嘗試着給一個非空類型分配一個 null。在 Kotlin 的類型體系裏,有空類型和非空類型。類型系統識別出了 string 是一個非空類型,而且阻止編譯器讓它以空的狀態存在。想要讓一個變量爲空,咱們須要在聲明後面加一個 ? 號,同時賦值爲 null。

val a: String? = null
println(a.length())

如今,咱們修復了這個問題,繼續向下:就像在 Java 裏同樣,咱們嘗試打印 stirng 的長度,可是咱們遇到了跟 Java 同樣的問題,這個字符串有可能爲空,不過幸虧的是:Kotlin 編譯器幫助咱們發現了這個問題,而不像 Java 那樣,在運行時爆出這個錯誤。

編譯器在長度輸出的代碼前中止了。想要讓編譯器編譯下去,咱們得在調用 length 方法的時候考慮到可能爲空的狀況,要麼賦值給這個 string,要麼用一個問號在變量名後,這樣,代碼執行時在讀取變量的時候檢查它是否爲空。

val a: String? = null
println(a?.length())

若是值是空,則會返回空。若是不是空值,就返回真實的值。print 遇到 null 會輸出空。

Ternary Null (16:19)

int length = a != null ? a.length() : -1

上面的代碼你可能在 Java 裏見到過。用三目運算符取值,檢查是否爲空,若是爲空則返回真實的長度,不然返回 -1,Kotlin 裏又相同的實現:

var length = if(a!= null) a.length() else -1

若是 a 不是 null, 那麼就能夠直接讀值,不然返回默認值。這裏用 elvis操做符 實現的簡寫:

var length = a?.length() ?: -1

咱們用 ?號作了一個內聯空檢查。若是你還記得剛纔我說的,若是 a 是 null,第一個 表達式就會返回 null ,若是 elivs 操做符 左側是空,那麼他就會返回右側,不然直接返回左側的值。

智能轉換 (17:30)

Kotlin 支持類型智能轉換的特性。 若是一個局部對象傳入一個類型檢查,你能夠直接經過這個類型來操做,而不須要再本身作轉換,看下面的例子你就明白了:

if (x is String) {
  print(x.length())
}

咱們檢查了 x 是否是一個字符串,若是是,就打印它的長度。作類型檢查,咱們須要用到 is 關鍵字,其實跟 Java 裏的 instanceOf 同樣。道理很簡單,咱們既然經過了類型檢查,咱們就能把它當作這個類型來使用。

if (x !is String) {
  return
}

print(x.size())

逆操做也沒有問題。上面這個例子檢查了是不是一個非字符串變量。若是不是,則直接返回。在咱們判斷了之後就能夠認爲它是一個字符串了,咱們也無需在作顯式的類型轉換。

if (x !is String || x.size() == 0) {
  return
}

上面的例子裏,咱們檢查了一個字符串是不是一個非字符串變量,若是左側的值是 false,就會調用右側的 or 判斷。
when 語句也適用上面的規則,when 實際上是一個加強版的 switch

when(x) {
  is Int -> print(x + 1)
  is String -> print(x.size() + 1)
  is Array<Int> -> print(x.sum())
}

咱們只要作了類型檢查,類型一切都會自動轉換。從類型檢查到類型自動轉換,就是 Kotlin 的智能轉換。

字符串模板 (19:07)

不少語言都有字符串模板和字符串插值。下面的例子大概就是你在 Java 裏常常用到的:

val apples = 4 println("I have " + apples + " apples.")

你能夠把 apples 變量和其餘字符串串聯起來。用更符合 Kotlin 風格的方式,你能夠用插值的方法,在字符串中用 $ 符號前綴加變量名來表明這個字符串內容。

val apples = 4
println("I have $apples apples.")

你也能夠用下面的表達式:

val apples = 4
val bananas = 3

println("I have $apples apples and " + (apples + bananas) + " fruits.") // Java-esque
println("I have $apples apples and ${apples+bananas} fruits.") // Kotlin

在 Java 中,可能最多見的方案是在須要顯示個數的地方,用加號操做蘋果和香蕉的個數,而後將字符串都串起來。但在 Kotlin 中,能夠用前綴 $ 符號加上大括號將操做語句包裹起來表明操做語句的結果。

區間表達式 (20:00)

你可能在其餘的語言裏見到過這樣的表達式。的確, Kotlin 的很多特性是借鑑自其餘語言裏。下面這個表達式:若是 i 大於等於 1,而且小於等於 10,就將其打印出來。咱們檢測的範圍是 1 到 10。

if (1 <= i && i <= 10) {
  println(i)
}

其實咱們能夠用 intRange 函數來完成這個操做。咱們傳入 1 和 10,而後調用 contains 函數來判斷是否在這個範圍裏。咱們打印出 i 便可。

if (IntRange(1, 10).contains(i)) {
  println(i)
}

這個還能夠用擴展函數來實現,1.rangeTo 建立了一個 1 到 10 的 intRange,咱們能夠用 contain 來判斷它。

更完美的而簡潔的寫法,是用下面的操做符:

if(i in 1..10) { ... }

.. 是 rangeTo 的一個別名,它實際背後工做原理仍是 rangeTo

咱們還可遍歷一個區間,好比:能夠用 step 關鍵字來決定每次遍歷時候的跳躍幅度:

for(i in 1..4 step 2) { ... }

也能夠逆向迭代,或者逆向遍歷而且控制每次的 step:

for (i in 4 downTo 1 step 2) { ... }

在 Kotlin 裏,也能夠結合不一樣的函數來實現你想要的區間遍歷。能夠遍歷不少不一樣的數據類型,好比建立 strings 或者你本身的類型。只要符合邏輯就行。

高階函數 (22:55)

不少語言已經支持了高階函數,好比 Java 8,可是你並不能用上 Java 8。若是你在用 Java 6 或者 Java 7,下面的例子實現了一個具備過濾功能的函數:

public interface Function<T, R> { R call(T t); } public static <T> List<T> filter(Collection<T> items, Function<T, Boolean> f) { final List<T> filtered = new ArrayList<T>(); for (T item : items) if (f.call(item)) filtered.add(item); return filtered; } filter(numbers, new Function<Integer, Boolean>() { @Override public Boolean call(Integer value) { return value % 2 == 0; } });

咱們首先要聲明一個函數接口,接受參數類型爲 T,返回類型爲 R。咱們用接口中的方法遍歷操做了目標集合,建立了一個新的列表,把符合條件的過濾了出來。

fun <T> filter(items: Collection<T>, f: (T) -> Boolean): List<T> {
  val filtered = arrayListOf<T>()
  for (item in items) if (f(item)) filtered.add(item)
  return filtered
}

上面的代碼是在 Kotlin 下的實現,是否是簡單不少?咱們調用的時候以下:
kotlin filter(numbers, { value -> value % 2 == 0 })

你可能也發現了,咱們沒有定義任何的函數接口,這是由於在 Kotlin 中,函數也是一種數據類型。看到 f:(T) -> Boolean 這個語句了嗎?這就是函數類型做爲參數的寫法,f 是函數別名,T是函數接受參數,Boolean 是這個函數的返回值。定義完成後,咱們隨後就能跟調用其餘函數同樣調用 f。調用 filter 的時候,咱們是用 lambda 表達式來傳入過濾函數的,即:{value ->value % 2 = 0}

因爲函數類型參數是能夠經過函數聲明的簽名來推導的,因此其實還有下面的一種寫法,大括號內就是第二個參數的函數體:

filter(numbers) {
  it % 2 == 0
}

內聯函數 (25:27)

內聯函數和高階函數常常一塊兒見到。在某些場景下,當你用到泛型的時候,你能夠給函數加上inline 關鍵字。在編譯時,它會用 lambda 表達式替換掉整個函數,整個函數的代碼會成爲內聯代碼。

若是代碼是這樣的:

inline fun <T> filter(items: Collection<T>, f: (T) -> Boolean): List<T> {
  val filtered = arrayListOf<T>()
  for (item in items) if (f(item)) filtered.add(item)
  return filtered
}

filter(numbers) { it % 2 == 0 }

由 inline 關鍵字在編譯後會變成以下這樣:

val filtered = arrayListOf<T>()
for (item in items) if (it % 2 == 0) filtered.add(item)

這也意味着咱們能實現一些常規函數實現不了的。好比:下面這個函數接受一個 lambda 表達式,但並不能直接返回:

fun call(f: () -> Unit) {
  f()
}

call {
  return // Not allowed
}

可是若是咱們的函數變成內聯函數,如今咱們就能直接返回了,由於它是內聯函數,會自動和其餘代碼混合在一塊兒:

inline fun call(f: () -> Unit) {
  f()
}
call {
  return // Now allowed
}

內聯函數也容許用 reified 類型。下面這個例子就是一個真實場景下的函數,經過一個 View 尋找類型爲 T 的父元素:

inline fun <T : Any> View.findViewParent(): T? {
  var parent = getParent()
  while (parent != null && parent !is T) {
    parent = parent.getParent()
  }
  return parent as T // Cast warning
}

這個函數還有些問題。因爲泛型類型被擦除了,因此咱們沒法檢測類型,即使咱們手工來作檢查,依然會出現 warning。

解決方案是:咱們給函數參數類型加上 reified 關鍵字。由於函數會被編譯成內聯代碼,因此咱們如今就能手工檢查類型消除警告了:

inline fun <reified T : Any> View.findViewParent(): T? {
  var parent = getParent()
  while (parent != null && parent !is T) {
    parent = parent.getParent()
  }
  return parent as T // Type cast allowed
}

函數擴展 (27:20)

函數擴展是 Kotlin 最強大的特性之一。下面是一個工具函數,檢測 App 是否運行在 Lollipop 或者更高的 Api 之上,它接受一個整數參數:

public fun isLollipopOrGreater(code: Int): Boolean {
  return code >= Build.VERSION_CODES.LOLLIPOP
}

經過 被擴展類型.函數 的寫法,就能將函數變成被擴展類型的一部分,寫法以下:

public fun Int.isLollipopOrGreater(): Boolean {
  return this >= Build.VERSION_CODES.LOLLIPOP
}

咱們不在須要參數,想要在函數體內調用整數對象須要用 this 關鍵字。下面就是咱們的調用方法,咱們能夠直接在整數類型上調用這個方法:

16.isLollipopOrGreater()

函數擴展能夠是任何整形,字面量或者包裝類型,也能夠在標記爲 final 的類上作相似操做。由於擴展函數不是真的給類增長代碼,任何人都沒有辦法去修改一個類,它其實是建立了一個靜態方法,用語法糖來讓擴展函數看着像是類自帶的方法同樣。

Kotlin 在 Java 集合中充分利用了擴展函數,這有一個例子操做集合:

final Function<Customer, Order> customerMapper = // ...
final Function<Order, Boolean> orderFilter = // ...
final Function<Order, Float> orderSorter = // ...
final List<Order> vipOrders = sortBy(filter(map(customers,
    customerMapper),
    orderFilter),
    orderSorter);

咱們對一個 customer 集合,執行了 map, filter, 以及 sort 操做。嵌套的寫法混亂並且難以閱讀。下面是標準庫的擴展函數寫法,是否是簡潔了不少:

val vipOrders = customers
    .map { it.lastOrder }
    .filter { it.total >= 500F }
    .sortBy { it.total }

屬性 (30:55)

Kotlin 把屬性也變成了語言特性。

class Customer { private String firstName; private String lastName; private String email; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public String getEmail() { return email; } public void setFirstName(String firstName) { this.firstName = firstName } public void setLastName(String lastName) { this.lastName = lastName } public void setEmail(String email) { this.email = email } }

上面是一個典型的 Java bean 類。可看到不少成員變量,和不少 getter, setter 方法,這但是隻有三個屬性的時候,就生成了這麼多代碼。來看看 Kotlin 的寫法:

class Customer {
 var firstName: String = // ...
 var lastName: String = // ...
 var email: String = // ...
}

你只須要將成員變量定義成一個變量便可,默認是 public 的。編譯器會自動生成 getter 和setter 方法。

主構造函數 (31:49)

Kotlin 中,類能夠擁有多個構造函數,這一點跟 Java 相似。但你也能夠有一個主構造函數。下面的例子是咱們從上面的例子裏衍生出來的,在函數頭裏添加了一個主構造函數:

在主構造函數裏,能夠直接用這些參數變量賦值給類的屬性,或者用構造代碼塊來實現初始化。

class Customer(firstName: String, lastName: String, email: String) {
  var firstName: String
  var lastName: String
  var email: String

  init {
    this.firstName = firstName
    this.lastName = lastName
    this.email = email
  }
}

固然,更好的方法是:直接在主構造函數裏定義這些屬性,定義的方法是在參數名前加上 var或者 val 關鍵字,val 是表明屬性是常量。

class Customer(
  var firstName: String,
  var lastName: String,
  var email: String)

單例 (35:53)

你可能常常會用到單例設計模式。好比一個 Logger 類,在 Java 裏,有多種實現單例的寫法。

在 Kotlin 裏,你只要在 package 級別建立一個 object 便可!不論你在什麼域裏,你均可以像單例同樣調用這個 object。

object Singleton

好比下面是一個 looger 的寫法:

object Logger {
  val tag = "TAG"
  fun d(message: String) {
    Log.d(tag, message)
  }
}

你能夠直接經過 Logger.D 的方法來調用 D 函數,它在任何地方都是可用的,並且始終只有一個實例。

Companion Objects (37:00)

Kotlin 移除了 static 的概念。一般用 companion object 來實現相似功能。你可能時常會看到一個 Activity 有一個 靜態類型的 string,名叫 tag,和一個啓動 Activity 的靜態方法。Java 中的實現以下:

class LaunchActivity extends AppCompatActivity { public static final String TAG = LaunchActivity.class.getName(); public static void start(Context context) { context.startActivity(new Intent(context, LaunchActivity.class)); } }

在 Kotlin 下的實現以下:

class LaunchActivity {
  companion object {
    val TAG: String = LaunchActivity::class.simpleName

    fun start(context: Context) {
      context.startActivity(Intent(context, LaunchActivity::class))
    }
  }
}

Timber.v("Starting activity ${LaunchActivity.TAG}")

LaunchActivity.start(context)

有了 companion object 後,就跟類多了一個單例的對象和方法同樣。

類委託 (37:58)

委託是一個你們都知道的設計模式,Kotlin 把委託視爲很重要的語言特性。下面是一個在 Java 中典型的委託寫法:

public class MyList<E> implements List<E> { private List<E> delegate; public MyList(List<E> delegate) { this.delegate = delegate; } // ... public E get(int location) { return delegate.get(location) } // ... }

咱們有一個本身的 lists 實現,經過構造函數將一個 list 存儲起來,存在內部的成員變量裏,而後在調用相關方法的時候再委託給這個內部變量。下面是在 Kotlin 裏的實現:

class MyList<E>(list: List<E>) : List<E> by list

用 by 關鍵字,咱們實現了一個存儲 E 類型的 list,在調用 List 相關的方法時,會自動委託到 list 上。
譯者注:參考 Kotlin 官方文檔瞭解更多。

聲明點變型(Declaration-Site Variance) (39:03)

這個多是一個比較容易讓人迷惑的主題。首先,咱們用一個協變數組來開始咱們的例子,下面的代碼可以很好的編譯:

String[] strings = { "hello", "world" }; Object[] objects = strings;

string 數組能夠正常的賦值給一個 object 數組。可是下面的不行:

List<String> strings = Arrays.asList("hello", "world"); List<Object> objects = strings;

你不能分配一個 string 類型的 list 給一個 object 類型的 list。由於 list 之間是沒有繼承關係的。若是你編譯這個代碼,會獲得一個類型不兼容的錯誤。想要修復這個錯誤,咱們得用到 Java 中的點變型(use-site variance)去聲明,所謂的點變型就是在聲明 list 可接受類型的時候,用extends 關鍵字給出參數類型的可接受類型範圍,好比相似以下的例子:

譯者注:點變型只是一個名字,不要太在乎爲何叫這個,簡單理解就是相似通配符原理,具體能夠查看這個維基頁面

public interface List<E> extends Collection<E> { public boolean addAll(Collection<? extends E> collection); public E get(int location); }

addAll 方法能夠接受一個參數,參數類型爲全部繼承自 E 的類型,這不是一個具體類型,而是一個類型範圍。每次調用 get 方法時,依然返回類型 E。在 Kotlin 中,你能夠用 out 關鍵字來實現相似的功能:

public interface List<out E> : Collection<E> {
  public fun get(index: Int): E
}

public interface MutableList<E> : List<E>, MutableCollection<E> {
  override fun addAll(c: Collection<E>): Boolean
}

上面的一系列被稱爲聲明點變型,即在聲明可接受參數的時候,就聲明爲它是可變的。好比上面例子:咱們聲明參數是能夠容許全部繼承自 E 類型的,返回類型也爲 E 的。

如今,咱們有了可變和不可變類型的列表。可變性(variance) 其實很簡單,就是取決於咱們在聲明的時候是動做。

譯者注:其實不論聲明點變型(Declaration-Site Variance) 仍是 點變型(Use-site variance) 都是爲了實現泛型的類型聲明,標註泛型類型可支持的範圍,釐清泛型類型上下繼承邊界。參考Generic Types

操做符重載 (41:26)

enum class Coin(val cents: Int) {
  PENNY(1),
  NICKEL(5),
  DIME(10),
  QUARTER(25),
}

class Purse(var amount: Float) {
  fun plusAssign(coin: Coin): Unit {
    amount += (coin.cents / 100f)
  }
}

var purse = Purse(1.50f)
purse += Coin.QUARTER // 1.75
purse += Coin.DIME // 1.85
purse += Coin.PENNY // 1.86

上面的代碼中,咱們建立了一個硬幣枚舉,每一個硬幣枚舉都表明一個特定數額的硬幣。咱們有一個 Purse(錢包) 類, 它擁有一個 amount 成員變量,表明錢包裏如今有多少錢。咱們建立了一個叫作 plusAssign 的函數,plusAssign 是一個保留關鍵字。這個函數會重載 += 操做符,也就是說當你在調用 += 符號的時候,就會調用這個函數。

隨後,建立一個 purse 實例,能夠直接用 += 操做來實現給錢包裏放錢進去。

讓你的項目支持 Kotlin (42:25)

讓你的項目支持 Kotlin 其實很是簡單,你須要讓你的項目裏啓用 gradle 插件,Kotlin Android 插件,拷貝代碼文件,引入標準庫便可。

開始 Kotlin 之路

Q&A (43:40)

Q: Kotlin 有沒有什麼缺點?

Michael: Kotlin 的少數特性可能有性能問題,好比 annotation 處理速度很慢。除此以外強烈推薦。

Q: 支持 Unit Test 麼?

Michael: 支持,咱們用Robolectric 和 Mockito 來作測試。

Q: Debug 的時候會很順利嗎?

Michael: Kotlin 生成的只是 JVM 字節碼而已,並且 Kotlin 和 IntelliJ 共存的很是好,畢竟都是一家作的。咱們以前用的 retrolambda,可是他生成的字節碼老是有些小問題。

Q: Kotlin 對 Jack and Jill 的支持如何?

Michael: Jack and Jill 和 Kotlin 不能一塊兒工做。

Q: Kotlin 上用反射怎麼樣?

Michael: Kotlin 上用反射的一大問題是回引入一個很大的庫…… 不過,這些你能夠用 Java 來作。

相關文章
相關標籤/搜索