本文介紹了Kotlin入門應(yīng)該知道一些基本語法概念。包括變量、常量、函數(shù)、空安全、類定義、類繼承、數(shù)據(jù)類、接口定義、冒號、可見性、擴展函數(shù)、Anko、對象表達式和聲明、Lambda表達式、when表達式、with函數(shù)、內(nèi)聯(lián)函數(shù)、Kotlin Android Extensions等。
本文首發(fā):http://yuweiguocn.github.io/
《送孟浩然之廣陵》
故人西辭黃鶴樓,煙花三月下?lián)P州。
孤帆遠影碧空盡,唯見長江天際流。
—唐,李白
本文所有用例基于Android Studio 3.0.1、Kotlin 1.2版本。
引入
在項目根目錄下 build.gradle
文件中添加 kotlin 插件依賴:
buildscript {
ext.gradle_plugin_version = '3.0.1'
ext.kotlin_version = '1.2.0'
repositories {
jcenter()
google()
}
dependencies {
classpath "com.android.tools.build:gradle:$gradle_plugin_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
在主 module 下 build.gradle
文件中添加 kotlin 依賴:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
...
...
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
如果開啟了 Data Binding,還需要添加如下依賴:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
dataBinding {
enabled = true
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
kapt "com.android.databinding:compiler:$gradle_plugin_version"
}
變量
在 kotlin 中一切皆為對象,沒有像 Java 中的原始基本類型。在 kotlin 中使用 var
修飾的為變量。例如我們定義一個 Int 類型的變量并賦值為1:
var a: Int = 1
a += 1
由于 kotlin 編譯器可以自動推斷出變量的類型,所以我們通常不需要指定變量的類型:
var s = "String" //類型為String
var a = 1 //類型為Int
在 kotlin 中分號不是必須的,不使用分號是一個不錯的實踐。
常量
在 kotlin 中使用 val
修飾的為常量。這和 java 中的 final
很相似。在 kotlin 中有一個重要的概念是:盡可能地使用 val
。
val s = "String" //類型為String
val ll = 22L //類型為Long
val d = 2.5 //類型為Double
val f = 5.5F //類型為Float
函數(shù)
定義一個函數(shù)接受兩個 Int 型參數(shù),返回值類型為 Int :
fun sum(a: Int, b: Int): Int {
return a + b
}
只有一個表達式作為函數(shù)體,以及自推導(dǎo)型的返回值:
fun sum(a: Int, b: Int) = a + b
函數(shù)的參數(shù)可以指定默認值:
fun sum(a: Int, b: Int = 10) = a + b
var c = sum(10) //調(diào)用
Unit 表示無返回值,對應(yīng) java 中 void:
fun printSum(a: Int, b: Int): Unit {
println("sum of $a and $b is ${a + b}")
}
Unit 的返回類型可以省略:
fun printSum(a: Int, b: Int) {
println("sum of $a and $b is ${a + b}")
}
空安全
在 kotlin 中,默認定義的變量不能為 null 的,這可以避免很多的 NullPointerException。
var a: String ="abc"
a = null //編譯錯誤
指定一個變量可null是通過在類型的最后增加一個問號:
var b: String? = "abc"
b = null
當(dāng)變量聲明為可空時,在調(diào)用它的屬性時無法通過編譯:
var b: String? = "abc"
val l = b.length //編譯錯誤
在這種情況下,我們可以使用安全操作符 ?.
:
var b: String? = "abc"
val l = b?.length
如果 b 不為空則返回長度,否則返回空,這個表達式的的類型是 Int?
。
我們還可以使用 ?:
操作符,當(dāng)前面的值不為空取前面的值,否則取后面的值,這和java中三目運算符類似。
val a:Int? = null
val myString = a?.toString() ?: ""
因為在Kotlin中 throw 和 return 都是表達式,他們可以用在Elvis operator操作符的右邊:
val myString = a?.toString() ?: return false
val myString = a?.toString() ?: throw IllegalStateException()
如果你確定該變量不為空,可以使用 !!
操作符:
var b: String? = "abc"
val l = b!!.length
使用 !!
操作符可以跳過限制檢查通過編譯,此時如果變量為空會拋出空指針異常。如果大量使用此操作符,顯然不是很好的處理。
類定義
使用 class 定義一個類。類的聲明包含類名,類頭(指定類型參數(shù),主構(gòu)造函數(shù)等等),以及類主體,用大括
號包裹。類頭和類體是可選的;如果沒有類體可以省略大括號。
class MainActivity{
}
在 Kotlin 中類可以有一個主構(gòu)造函數(shù)以及多個二級構(gòu)造函數(shù)。主構(gòu)造函數(shù)是類頭的一部分:跟在類名后面(可以有可選的類型參數(shù))。
class Person constructor(firstName: String) {
}
如果主構(gòu)造函數(shù)沒有注解或可見性說明,則 constructor
關(guān)鍵字是可以省略:
class Person(name: String, surname: String)
構(gòu)造函數(shù)的函數(shù)體可以寫在 init
塊中:
class Customer(name: String) {
init {
logger.info("Customer initialized with value ${name}")
}
}
注意主構(gòu)造函數(shù)的參數(shù)可以用在初始化塊內(nèi),也可以用在類的屬性初始化聲明處:
class Customer(name: String) {
val customerKry = name.toUpperCase()
}
事實上,聲明屬性并在主構(gòu)造函數(shù)中初始化,在 Kotlin 中有更簡單的語法:
class Person(val firstName: String, val lastName: String, var age: Int) {
}
就像普通的屬性,在主構(gòu)造函數(shù)中的屬性可以是可變的( var )或只讀的( val )。
類繼承
Kotlin 中所有的類都有共同的父類 Any
,它是一個沒有父類聲明的類的默認父類:
class Example // 隱式繼承于 Any
Any 不是 java.lang.Object ;事實上它除了 equals() , hashCode() 以及 toString() 外沒有任何成員了。
默認情況下,kotlin 中所有的類都是不可繼承 (final) 的,所以我們只能繼承那些明確聲明為 open
或 abstract
的類,當(dāng)我們只有單個構(gòu)造器時,我們需要在從父類繼承下來的構(gòu)造器中指定需要的參數(shù)。這是用來替換Java中的 super 調(diào)用的。
open class Example(name: String)
class MyExample(name: String, age: Int) : Example(name)
數(shù)據(jù)類
數(shù)據(jù)類是一種非常強大的類,它可以讓你避免創(chuàng)建Java中的用于保存狀態(tài)但又操作非常簡單的POJO的模版代碼。它們通常只提供了用于訪問它們屬性的簡單的getter和setter。定義一個新的數(shù)據(jù)類非常簡單:
data class Forecast(val date: Date, val temperature: Float, val details: String)
編譯器會自動根據(jù)主構(gòu)造函數(shù)中聲明的所有屬性添加如下方法:
- equals(): 它可以比較兩個對象的屬性來確保他們是相同的。
- hashCode(): 我們可以得到一個hash值,也是從屬性中計算出來的。
- toString(): 格式是 "User(name=john, age=42)"
- copy(): 你可以拷貝一個對象,可以根據(jù)你的需要去修改里面的屬性。
- componentN()函數(shù) 對應(yīng)按聲明順序出現(xiàn)的所有屬性
定義數(shù)據(jù)類需要注意的地方:
- 主構(gòu)造函數(shù)應(yīng)該至少有一個參數(shù)。
- 數(shù)據(jù)類的變量屬性只能是
var
或val
的。 - 數(shù)據(jù)類不能是 abstract,open,sealed,或者 inner 。
復(fù)制數(shù)據(jù)類并修改某一屬性值:
val f1 = Forecast(Date(), 27.5f, "Shiny day")
val f2 = f1.copy(temperature = 30f)
映射對象的每一個屬性到一個變量中,這個過程就是我們知道的多聲明。這就是為什么會有 componentN 函數(shù)被自動創(chuàng)建。使用上面的 Forecast 類舉個例子:
val f1 = Forecast(Date(), 27.5f, "Shiny day")
val (date, temperature, details) = f1
上面這個多聲明會被編譯成下面的代碼:
val date = f1.component1()
val temperature = f1.component2()
val details = f1.component3()
這個特性背后的邏輯是非常強大的,它可以在很多情況下幫助我們簡化代碼。舉個例子, Map 類含有一些擴展函數(shù)的實現(xiàn),允許它在迭代時使用key和value:
for ((key, value) in map) {
Log.d("map", "key:$key, value:$value")
}
接口定義
Kotlin 的接口很像 java 8。它們都可以包含抽象方法,以及方法的實現(xiàn)。和抽象類不同的是,接口不能保存狀態(tài)。可以有屬性但必須是抽象的,或者提供訪問器的實現(xiàn)。
接口用關(guān)鍵字 interface
來定義:
interface Bar {
fun bar()
fun foo() {
//函數(shù)體是可選的
}
}
冒號
在冒號區(qū)分類型和父類型中要有空格,在實例和類型之間是沒有空格的:
interface Foo<out T : Any> : Bar {
fun foo(a: Int): T
}
可見性
在 kotlin 中,默認修飾符為 public
。
修飾符 | 說明 |
---|---|
private | 當(dāng)前類可見 |
protected | 成員自己和繼承它的成員可見 |
internal | 當(dāng)前 module 可見 |
public | 所有地方可見 |
擴展函數(shù)
擴展函數(shù)數(shù)是指在一個類上增加一種新的行為,甚至我們沒有這個類代碼的訪問權(quán)限。這是一個在缺少有用函的類上擴展的方法。在Java中,通常會實現(xiàn)很多帶有static方法的工具類。Kotlin中擴展函數(shù)的一個優(yōu)勢是我們不需要在調(diào)用方法的時候把整個對象當(dāng)作參數(shù)傳入。擴展函數(shù)表現(xiàn)得就像是屬于這個類的一樣,而且我們可以使用 this 關(guān)鍵字和調(diào)用所有public方法。
舉個例子,我們可以創(chuàng)建一個toast函數(shù),這個函數(shù)不需要傳入任何context,它可以被任何Context或者它的子類調(diào)用,比如Activity或者Service:
fun Context.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, duration).show()
}
這個方法可以在Activity內(nèi)部直接調(diào)用:
toast("Hello world!")
toast("Hello world!", Toast.LENGTH_LONG)
擴展函數(shù)也可以是一個屬性。所以我們可以通過相似的方法來擴展屬性。下面的例子展示了使用他自己的getter/setter生成一個屬性的方式。Kotlin由于互操作性的特性已經(jīng)提供了這個屬性,但理解擴展屬性背后的思想是一個很不錯的練習(xí):
public var TextView.text: CharSequence
get() = getText()
set(v) = setText(v)
擴展函數(shù)并不是真正地修改了原來的類,它是以靜態(tài)導(dǎo)入的方式來實現(xiàn)的。擴展函數(shù)可以被聲明在任何文件中,因此有個通用的實踐是把一系列有關(guān)的函數(shù)放在一個新建的文件里。
Anko
Anko是JetBrains開發(fā)的一個強大的庫。它主要的目的是用來替代以前XML的方式來使用代碼生成UI布局。Anko包含了很多的非常有幫助的函數(shù)和屬性來避免讓你寫很多的模版代碼。通過查看Anko源碼學(xué)習(xí)kotlin語言是一種不錯的方法。Anko能幫助我們簡化代碼,比如,實例化Intent,Activity之間的跳轉(zhuǎn),F(xiàn)ragment的創(chuàng)建,數(shù)據(jù)庫的訪問,Alert的創(chuàng)建等等。
github地址:https://github.com/Kotlin/anko
添加Anko的依賴:
// 主工程目錄下build.gradle文件中聲明版本
buildscript {
ext.anko_version = '0.10.0'
}
// module的下build.gradle文件中添加依賴
dependencies {
compile "org.jetbrains.anko:anko:$anko_version"
}
例如執(zhí)行Activity的跳轉(zhuǎn):
startActivity<MainActivity>()
//傳遞Intent參數(shù)
startActivity<NewHomeActivity>("name1" to "value1","name2" to "value2")
在Activity中顯示Toast:
toast("Hello world!")
longToast(R.id.hello_world)
線程切換:
async {
val response = URL("http://yuweiguocn.github.io").readText()
uiThread {
textView.text = response
}
}
對象表達式和聲明
有時候我們想要創(chuàng)建一個對當(dāng)前類有一點小修改的對象,但不想重新聲明一個子類。java 用匿名內(nèi)部類的概念解決這個問題。Kotlin 用對象表達式和對象聲明巧妙的實現(xiàn)了這一概念。
window.addMouseListener(object: MouseAdapter () {
override fun mouseClicked(e: MouseEvent) {
//...
}
})
像 java 的匿名內(nèi)部類一樣,對象表達式可以訪問閉合范圍內(nèi)的變量 (和 java 不一樣的是,這些變量不用是 final 修飾的)
fun countClicks(window: JComponent) {
var clickCount = 0
var enterCount = 0
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++
}
override fun mouseEntered(e: MouseEvent){
enterCount++
}
})
}
單例模式是一種很有用的模式,Kotln 中聲明它很方便,其中init代碼塊對應(yīng)java中static代碼塊。
object DataProviderManager {
init { //對應(yīng)java中static代碼塊
}
fun registerDataProvider(provider: DataProvider) {
// ...
}
val allDataProviders: Collection<DataProvider>
get() = // ...
}
這叫做對象聲明,跟在 object 關(guān)鍵字后面是對象名。和變量聲明一樣,對象聲明并不是表達式,而且不能作為右值用在賦值語句。想要訪問這個類,直接通過名字來使用這個類:
// in kotlin
DataProviderManager.registerDataProvider(...)
// in java
DataProviderManager.INSTANCE.registerDataProvider(...)
注意:對象聲明不可以是局部的(比如不可以直接在函數(shù)內(nèi)部聲明),但可以在其它對象的聲明或非內(nèi)部類中進行內(nèi)嵌入。
我們需要一個類里面有一些靜態(tài)的屬性、常量或者函數(shù),我們可以使用伴隨對象。這個對象被這個類的所有對象所共享,就像java中的靜態(tài)屬性或者方法。在類聲明內(nèi)部可以用 companion
關(guān)鍵字標(biāo)記對象聲明:
class MyClass {
companion object Factory {
val URL = "http://yuweiguocn.github.io/"
fun create(): MyClass = MyClass()
}
}
伴隨對象的成員可以通過類名做限定詞直接使用:
// in kotlin
val instance = MyClass.create()
val url = MyClass.URL
// in java
MyClass c = MyClass.INSTANCE.create()
String url = MyClass.INSTANCE.getURL()
在使用了 companion 關(guān)鍵字時,伴隨對象的名字可以省略。
class MyClass {
companion object {
fun create(): MyClass = MyClass()
}
}
注意:在kotlin中沒有 new 關(guān)鍵字。
對象表達式和聲明的區(qū)別:
- 對象表達式在我們使用的地方立即初始化并執(zhí)行的
- 對象聲明是懶加載的,是在我們第一次訪問時初始化的。
- 伴隨對象是在對應(yīng)的類加載時初始化的,和 Java 的靜態(tài)初始是對應(yīng)的。
Lambda表達式
Lambda表達式是一種很簡單的方法,去定義一個匿名函數(shù)。Lambda是非常有用的,因為它們避免我們?nèi)懸恍┌四承┖瘮?shù)的抽象類或者接口,然后在類中去實現(xiàn)它們。在Kotlin,我們把一個函數(shù)作為另一個函數(shù)的參數(shù)。
我們用Android中非常典型的例子去解釋它是怎么工作的: View.setOnClickListener() 方法。如果我們想用Java的方式去增加點擊事件的回調(diào),我首先要編寫一個 OnClickListener 接口:
public interface OnClickListener {
void onClick(View v);
}
然后我們要編寫一個匿名內(nèi)部類去實現(xiàn)這個接口:
view.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
Toast.makeText(v.getContext(), "Click", Toast.LENGTH_SHORT).show();
}
});
我們將把上面的代碼轉(zhuǎn)換成Kotlin(使用了Anko的toast函數(shù)):
view.setOnClickListener(object : OnClickListener {
override fun onClick(v: View) {
toast("Click")
}
}
Kotlin允許Java庫的一些優(yōu)化,Interface中包含單個函數(shù)可以被替代為一個函數(shù)。如果我們這么去定義了,它會正常執(zhí)行:
fun setOnClickListener(listener: (View) -> Unit)
一個lambda表達式通過參數(shù)的形式被定義在箭頭的左邊(普通圓括號包圍),然后在箭頭的右邊返回結(jié)果值。當(dāng)我們定義了一個方法,我們必須使用大括號包圍。如果左邊的參數(shù)沒有用到,我們甚至可以省略左邊的參數(shù)。
view.setOnClickListener({ view -> toast("Click")})
view.setOnClickListener({ toast("Click") })
如果這個函數(shù)只接收一個參數(shù),我們可以使用it引用,而不用去指定左邊的參數(shù):
view.setOnClickListener({ toast("Click" + it.id)})
如果這個函數(shù)的最后一個參數(shù)是一個函數(shù),我們可以把這個函數(shù)移動到圓括號外面:
view.setOnClickListener() { toast("Click") }
并且,最后,如果這個函數(shù)只有一個參數(shù),我們可以省略這個圓括號:
view.setOnClickListener { toast("Click") }
When表達式
when 表達式與Java中的 switch/case 類似,但是要強大得多。這個表達式會去試圖匹配所有可能的分支直到找到滿意的一項。然后它會運行右邊的表達式。與Java的 switch/case 不同之處是參數(shù)可以是任何類型,并且分支也可以是一個條件。
對于默認的選項,我們可以增加一個 else 分支,它會在前面沒有任何條件匹配時再執(zhí)行。條件匹配成功后執(zhí)行的代碼也可以是代碼塊:
when (x){
1 -> print("x == 1")
2 -> print("x == 2")
else -> {
print("I'm a block")
print("x is neither 1 nor 2")
}
}
因為它是一個表達式,它也可以返回一個值。我們需要考慮什么時候作為一個表達式使用,它必須要覆蓋所有分支的可能性或者實現(xiàn) else 分支。否則它不會被編譯成功:
val result = when (x) {
0, 1 -> "binary"
else -> "error"
}
with函數(shù)
with是一個非常有用的函數(shù),包含在Kotlin的標(biāo)準(zhǔn)庫中。它接收一個對象和一個擴展函數(shù)作為它的參數(shù),然后使這個對象擴展這個函數(shù)。這表示所有我們在括號中編寫的代碼都是作為對象(第一個參數(shù))的一個擴展函數(shù),我們可以就像作為this一樣使用所有它的public方法和屬性。當(dāng)我們針對同一個對象做很多操作的時候這個非常有利于簡化代碼。
data class Person(val name: String, val age: Int)
val p = Person("growth",25)
with(p){
var info = “$name - $age”
}
內(nèi)聯(lián)函數(shù)
下面是with函數(shù)的定義:
inline fun <T> with(t: T, body: T.() -> Unit) { t.body() }
這個函數(shù)接收一個 T 類型的對象和一個被作為擴展函數(shù)的函數(shù)。它的實現(xiàn)僅僅是讓這個對象去執(zhí)行這個函數(shù)。因為第二個參數(shù)是一個函數(shù),所以我們可以把它放在圓括號外面,所以我們可以創(chuàng)建一個代碼塊,在這這個代碼塊中我們可以使用 this 和直接訪問所有的public的方法和屬性。
內(nèi)聯(lián)函數(shù)與普通的函數(shù)有點不同。一個內(nèi)聯(lián)函數(shù)會在編譯的時候被替換掉,而不是真正的方法調(diào)用。這在一些情況下可以減少內(nèi)存分配和運行時開銷。舉個例子,如果我們有一個函數(shù),只接收一個函數(shù)作為它的參數(shù)。如果是一個普通的函數(shù),內(nèi)部會創(chuàng)建一個含有那個函數(shù)的對象。另一方面,內(nèi)聯(lián)函數(shù)會把我們調(diào)用這個函數(shù)的地方替換掉,所以它不需要為此生成一個內(nèi)部的對象。
inline fun supportsLollipop(code: () -> Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
code()
}
}
它只是檢查版本,然后如果滿足條件則去執(zhí)行。現(xiàn)在我們可以這么做:
supportsLollipop {
window.setStatusBarColor(Color.BLACK)
}
Kotlin Android Extensions
Kotlin Android Extensions是另一個kotlin團隊研發(fā)的可以讓開發(fā)更簡單的插件。該插件依賴于 kotlin 標(biāo)準(zhǔn)庫,當(dāng)前僅僅包括了view的綁定,這可以讓我們省去findViewById操作。
使用該插件非常簡單,修改module的build.gradle文件:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
例如在布局文件中定義一了個id為tvTest的TextView,在Activity的setContentView之后就可以直接使用該TextView了:
class MainActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tvTest.text = "hello world"
}
}