Overview
由於kotlin和Java之間的高兼容性,使得kotlin適用於服務端、客戶端、前端以及數據科學等多個領域。同時,從Java轉向kotlin的學習曲線也更加平滑。究其本質,kotlin和Java一樣都是將源代碼編譯成字節碼,從而可以運行在虛擬機之上。此外,對於服務端開發而言,kotlin支持協程,相比Java中的線程,協程更加輕量級。因此,在硬件資源相等的情況下,使用協程可以大大地提高服務端應用的伸縮性。
kotlin基於內聯函數提供Lambda,使得基於kotlin編寫的應用程序運行速度更快。
kotlin在語言級別上支持協程。因此,基於kotlin提供的協程編寫的應用程序用戶體驗更加流暢,且更具伸縮性。
基礎入門
基礎語法
- 包的定義與導入
相比Java,區別在於不需要以分號結尾,同時目錄無需和包一一匹配。 - 變量和常量的定義
需要注意的是,kotlin中存在top-level的變量和常量,在整個kotlin文件中的任何地方都可訪問和使用。
var variant_name : variant_type //局部變量必須初始化,全局變量無須初始化
val variant_name : variant_type //局部常量必須初始化,全局變量無須初始化
- 方法定義
fun function_name(var_name: var_type, var_name1: var_type1, ...): return_type {
//funciton_body
}
- 方法參數可以設置默認值
fun function1(a: Int = 1, b: String = "default value") {
// method body
}
fun function2() {
function1() //不傳參數,則默認使用方法定義中指定的參數值
}
- 可空以及判空
kotlin中的類型默認是不可空的,除非顯式地在類型後面緊接一個問號,表示該類型可以爲null。
fun toString(): Int? {
//funciton body
}
//第一個參數不能爲null,第二個參數可以爲null
fun test(param1: String, param2: Int?) {
//function body
}
//Java代碼
test(null, 1) //這裏運行時會拋出NPE
注意:通過Java調用kotlin的方法時,如果傳入的參數爲null,但是kotlin方法的參數類型沒有顯式地聲明爲Nullable,則運行時會出現空指針異常(NPE)。因此,建議如果kotlin中的方法如果是提供給其他開發人員使用的,則統一把方法參數聲明爲Nullable,方法體內部作不可爲null的處理,避免調用方調用出現運行時異常!
- 類型判斷以及類型轉換
相比Java的instanceof關鍵字,kotlin中使用關鍵字is來判斷某個實例是否爲某一指定類型。
fun typeCheckAndCast(obj: Any) {
if(obj is String) {
println("obj is a String and its length is ${obj.length}")//當判斷條件成立時,代碼塊中已經自動將obj轉換爲String類型,因此可以直接調用String的方法
return
}
var len = obj.length // 這句代碼編譯時會報錯,因爲經過上述的判斷,這裏的obj並沒有具體的類型,因此不可以調用String裏的方法
}
fun typeCheckAndCast_1(obj: Any) {
if(obj !is String) {
println("obj is not a String")
return
}
var len1 = obj.length // 這句代碼可以正常編譯並執行,因爲經過上面的判斷,這裏的obj已經自動轉換爲String,因此可以直接調用String的方法
}
fun typeCheckAndCast_2(obj: Any) {
if(obj is String && obj.length > 0) { //由於&&是短路運算符,因此當左邊成立的時候,右邊可以直接將obj默認爲String類型的實例進行使用
println("obj is not a String")
}
}
- 字符串模板
kotlin中的字符串拼接更爲高級,kotlin中的 ‘’ 等同於Java中的 “” ,kotlin中的 “” 可以通過$來拼接想要的值,kotlin中的 ‘’’ ‘’’ 可以保留字符串的格式,比如換行。
var a = 1
str = "the value of variant a is $a" //str is "the value of variant a is 1"
a = 3
str = "${str.replace("is", "was")} , but now is $a" // str is "the value of variant a was 1, but now is 3"
- kotlin中的循環
for循環
val fruits = listOf("apple", "banana", "kiwifruit")// kotlin中的listOf返回的是一個不可變的list,即不可以向其中添加、刪除、修改元素
//類似於Java中的for-each
for (fruit in fruits) {
println(fruit)
}
for (index in fruits.indices) {
println("the index is $index , the element is ${fruits[index]}")
}
while循環
val fruits = listOf("apple", "banana", "kiwifruit")
var index = 0
while(index < fruits.size) {
println("the index is $index , the element is ${fruits[index]}")
index++
}
- when語句(類似於Java中的swith語句,但是使用起來更爲靈活)
fun testhen(obj: Any) : String?=
when(obj) {
1 -> "one"
"Hello" -> "hello"
is String -> "string"
is Long -> "long"
else -> null
}
- range(a…b)
fun testRange() {
var a = 1
var items = listOf(1, 2, 3)
if(a in 1..2) {
println("$a is in range of [1,2]")
}
if(a !in 1 until 2) {
println("$a is not in range of [1,2)")
}
if(a !in items.indices) {
println("$a is not in range of 0 to ${items.length - 1}")
}
for(index in 1..10) {
println("the element is $index")
}
}
- kotlin中的集合操作(鏈式調用結合Lambda)
// 集合提供的操作
fun testCollectionOperation() {
val fruits = listOf("apple", "banana", "apricot", "avocado", "kiwifruit")
fruit.filter(it.startsWith("a"))
.sortedBy(it)
.map(it.toUpperCase())
.forEach(println(it))
}
// 檢查指定元素是否在集合中
fun checkValueOfCollection() {
val fruits = listOf("apple", "banana", "apricot", "avocado", "kiwifruit") // 只讀的list
var firstFruit = fruit.firstOrNull() ?: "unknown fruit"
if("apple" in fruits) {
println("apple is in fruits")
}
if("pear" !in fruits) {
println("pear is not in fruits")
}
}
// 遍歷map中的鍵值對
fun iterateMap() {
val fruits = mapOf("apple" to 1, "banana" to 2, "apricot" to 3) //只讀的map
for((name, priority) in fruits) {
println("$name -> $priority")
}
}
- 應用程序的入口函數:相比Java而言,區別在於main函數沒有參數
fun main() {
//function_body
}
基本類型
對於開發人員來說,kotlin中不存在基本類型,任何聲明的變量都引用了一個對象,可以通過變量調用方法以及訪問屬性。也就是說,對於Java中的基本類型,比如數值類型、字符類型、字符串類型以及數組等,在kotlin中都是對應一個常規的類型。
- kotlin通過內置的類型來表示number類型,如Byte、Short、Int、Long、Float、Double等。在kotlin中對一個變量賦值一個整數,kotlin會根據整數的大小所處的範圍,將該變量指向對應的整數類型。
var i = 2 //Int
var l = 3000000000 //Long
var l1 = 1L //Long
var s: Short = 3 //Short
var b: Byte = 2 //Byte
var f = 1.0f //Float
var d = 1.0 //Double
- kotlin中無法對數值類型的變量自動進行類型轉換
fun printDouble(num: Double) {
println(num)
}
var i = 1
var f = 1.0f
var l = 1L
printDouble(1.0)
// 以下調用運行都會報錯,類型匹配異常
// printDouble(i)
// printDouble(f)
// printDouble(l)
- kotlin無法自動將向上轉型,如Byte轉爲Int。但是可以通過調用轉換類型的方法,如toInt()等方法進行轉換
var b: Byte = 1
// var i: Int = b //這是編譯不通過的
var i: Int = b.toInt()
- Array數組類型
var arr = arrayOf(1, 2, 3) //生成數組[1, 2, 3]
var arr1 = arrayOfNulls(2) //生成數組[null, null]
val asc = Array(3) { i -> (i * i).toString() } //生成數組["0", "1", "4"]
val intArr = IntArray(5) // 生成數組[0, 0, 0, 0, 0]
val intArr1 = IntArray(5) { 42 } // 生成數組[42, 42, 42, 42, 42]
val intArr2 = IntArray(5) { it * 1} // 生成數組[0, 1, 2, 3, 4]
- 字符串類型
字符串常量分爲兩種:通過\n實現換行 和 通過""" “”"來實現換行
val s = "Hello, world!\n" // \n實現換行
// 三引號實現換行
val text = """
for (c in "foo")
print(c)
"""
代碼組織結構
- 包管理:
kotlin基於包管理文件,文件中包含top-level的變量、常量、方法以及類的聲明。不同包下使用其他包下的類或者方法等,需要進行導入。如果出現類名或者方法名衝突,可以在導包的時候進行別名處理,保證本文件內的名稱不發生衝突。
import org.example.Message
import org.test.Message as testMessage // testMessage stands for 'org.test.Message'
- 跳轉關鍵字:
return:函數級別
break:循環體級別
continue:循環級別
label@:標籤,搭配return、break或continue使用
loop@ for (i in 1..100) {
for (j in 1..100) {
if (...) break@loop //跳出指定的循環體,繼續執行循環體外的代碼
}
}
//循環體外的代碼
fun foo() {
listOf(1, 2, 3, 4, 5).forEach lit@{
if (it == 3) return@lit // 從foreach退出,繼續執行1處的代碼
print(it)
}
print(" done with explicit label") // 1
}
類與實例
- 類的聲明
成員字段:
字段的定義和初始化和方法中的變量定義和初始化基本一致,都是通過var定義變量,val定義常量。不同的地方在於,類的屬性定義會生成對應的getter和setter方法,其中,對於val字段,沒有setter方法。可以對setter或者getter方法進行權限修飾符的添加,如下代碼所示:
class FieldsDemo {
var str: String? = null
private get
private set
val i: Int = 1
private get
// private set // val變量沒有setter
var size: Int = 3
set(value) {
isEmpty = value
if(value) {
size = 0
}
}
// 通過get的返回值類型推測屬性的類型
val inEmpty get() = {
this.size == 0
}
}
編譯期常量
通過const關鍵字修飾的屬性,其值在編譯期就是已知的。const修飾的屬性需要滿足:
- top-level或者在object或companion object中定義
- 初始化的值爲基本類型或者String類型
- 不可以自定義getter
延遲初始化屬性和變量
如果變量或屬性在聲明時,沒有特殊說明,默認是不可爲null的。這時就需要在構造函數中對其進行初始化。但是這樣並不便於開發,因此,可以採用延遲初始化來提高開發的靈活性。通過lateinit關鍵字修飾某個成員變量,即可實現延遲初始化的效果。注意,當該變量還沒初始化的時候,使用該變量將會拋出使用前未初始化的異常。
class LateInitDemo {
lateinit var str: String
@setup fun setup() {
str = "late inited"
}
}
- 類的構造函數
構造函數包含主要構造函數和次要構造函數。主要構造函數只有一個(相當於Java中的無參構造函數),次要構造函數有多個。主要構造函數中的參數必須在init代碼塊中進行初始化,次要構造函數必須調用主要構造函數,即如下示例:
class Person(val name: String) {// 不加權限修飾符,默認是public
var children: MutableList<Person> = mutableListOf<Person>();
init{
println("the init block is executed")
}
constructor(name: String, parent: Person) : this(name) {
parent.children.add(this)
}
}
- kotlin中的單例模式(非線程安全)
class Singleton private constructor() {
private singleton: Singleton?
init{
println("the init bolck is executed")
}
companion object{
@JvmStatic fun getInstance(): Singleton{
if(singleton == null) {
singleton = Singleton()
}
return singleton
}
}
}
- 繼承
kotlin中,如果某個類需要作爲基類,必須以關鍵字open修飾該類。同樣地,方法如果可以被重寫,也需要以關鍵字open進行修飾。繼承父類的子類,其構造函數必須調用父類對應的構造函數,如下代碼所示:
open class Base {
constructor(a: int)
open fun method1() {
println("this is the method of Base")
}
}
class Sub : Base{
constructor(a: int): super(a){
}
override fun method1() {
println("this is the method of Sub")
}
}
class MyView : View {
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
- 屬性重寫:類似方法的重寫,可以被重寫的屬性需要關鍵字open來修飾
open class Base {
open val filed: Int = 1
open val filed1: Int = 2
open var filed2: Int = 3
}
class SubClass : Base {
override val filed: Int = 0
override var filed1: Int = 1
override var filed2: Int = 2
//override val filed2: Int = 2 //重寫的屬性不可以由var變爲val,原因是屬性的方法只能增加,不能減少。val重寫爲var,相當於增加了set方法
}
- 子類的方法中調用父類的方法:可以通過super關鍵字進行調用,但是,內部類想要調用外部類的父類的方法則需要基於super結合@outer_class來實現:
open class Rectangle {
val borderColor: String get() = "black"
open fun draw() {
println("Drawing a rectangle")
}
}
class FilledRectangle: Rectangle() {
override fun draw() { /* ... */ }
val borderColor: String get() = "white"
inner class Filler {
fun fill() { /* ... */ }
fun drawAndFill() {
super@FilledRectangle.draw() // Calls Rectangle's implementation of draw()
fill()
println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // Uses Rectangle's implementation of borderColor's get()
}
}
}
接口與抽象類
接口與抽象類的區別在於,接口中不能存儲狀態,但是可以定義變量,以及爲變量重寫getter方法。除此以外,接口與抽象類都以定義抽象方法以及方法的實現。
interface Demo {
val str: String // 缺省修飾符abstract
val prop: String
get() = "demo"
fun method1()
fun method2() {
// body
}
}
class DemoImpl : Demo {
override val str: String = "override filed"
override fun method1() {
println("this is the implemention")
}
}
可見性修飾符
koltin中一共有四種可見性修飾符,分別是private、protected、internal和public。缺省的可見性修飾符爲public。
- top-level的可見性修飾符:屬性、方法、類、單例、接口等都可以定義爲包級別,也就是在一個包下可以直接定義這些成員。針對top-level的成員,可見性修飾符只能使用private、internal以及public。public修飾的top-level成員,可以在同一程序的任意代碼中使用;private修飾的top-level成員,只能在其定義所處的文件中使用;internal修飾的則可以在同一module中使用。
- 類和接口內的可見性修飾符:private、protected、internal和public都可以使用,其中private、protected以及public的可見性與Java一致,internal則對在同一模塊中的client可見。其中,kotlin中外部類無法訪問內部類中的私有成員。
- 局部變量、函數以及類不可以使用可見性修飾符
- kotlin中的module:module是指一同被編譯的多個kotlin文件
擴展函數
- kotlin中可以通過擴展來爲一個已經定義好的類添加新的函數。類似地,kotlin也允許通過擴展來爲一個已有類添加新的屬性。但是,這種擴展是並不是通過在已實現的類對應的文件中添加代碼來實現,只是對實例對象擴展了能力。
package my.extension
//擴展函數需要在函數名前加上前綴
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val temp = this[index1]
this[index1] = this[index2]
this[index2] = temp
}
val mutableList = mutableListOf(1, 2, 3)
mutableList.swap(0, 1)
println(mutableList) // 2, 1, 3
//針對元素爲任意類型的MutableList進行擴展
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val temp = this[index1]
this[index1] = this[index2]
this[index2] = temp
}
- 擴展函數的調用是編譯期決定的,與Java中的多態性不同。
open class Fruit
class Apple : Fruit()
fun Fruit.printName() = "Fruit"
fun Apple.printName() = "Apple"
//無論傳入是Fruit還是Apple的實例,最終輸出的都是 Fruit
fun printFruitName(fruit: Fruit) {
println(fruit.printName())
}
printFruitName(Apple()) // 這裏打印到控制檯的內容是 Fruit
- 當一個類的成員函數和擴展函數的方法簽名相同時,調用的實際函數永遠都是成員函數。而擴展函數如果是重載了一個成員函數,則不會出現這樣的問題。
- 利用擴展函數,可以實現可空類型實例的某些方法的安全調用,避免出現NPE。
fun Any?.toString(): String {
if(this == null) {
return "null"
}
return toString()
}
- 同樣地,kotlin中可以擴展屬性,但是擴展的屬性只能通過顯式地定義getter和setter方法來進行訪問和賦值
- 伴生對象也可以擴展函數和擴展屬性
class CompanionClass {
companion object { }
}
fun CompanionClass.Companion.extendMethod() {
println("this is the companion pbject extend function")
}
fun main() {
CompanionClass.extendMethod()
}
- 擴展函數的作用域:通常定義擴展函數都是top-level的,因此,在同一包下可以直接使用,但是,在其他包內調用擴展函數需要先導入這個函數,才能夠使用
package example.extension.scope
fun <T> List<T>.swap(index1: Int, index2: Int) {
val temp = this[index1]
this[index1] = this[index2]
this[index2] = temp
}
package example.extension.client
import example.extension.scope.swap
fun main() {
val list = listOf(1, 2, 3)
list.swap(0, 2)
}
- 類中定義其他類的擴展函數:在類B中定義目標類A的擴展函數,該擴展函數既可以調用目標類A的成員函數,還可以調用類B的成員函數。此外,定義類B的成員函數可以通過類A的實例來調用擴展函數
class A(val name: String) {
fun printName() { print(name) }
}
class B(val a: A, val num: Int) {
fun printNum() { print(num) }
fun A.printString() {
printName() // calls A.printName()
print(":")
printNum() // calls B.printPort()
toString() // calls A.toString()
this@B.toString() // calls B.toString()
}
fun printNumForA() {
a.printString() // calls the extension function
}
}
fun main() {
B(A("kotlin"), 443).printNumForA()
}
數據類
kotlin中通過data關鍵字聲明數據類,data聲明的數據類,kotlin會爲其自動生成一些函數,包括equals()/hashCode()、toString()、copy()以及componentN()等函數。但是,定義數據類需要滿足一下條件:
- 主構造函數必須包含至少一個參數
- 主構造函數的參數必須聲明爲val或var
- data修飾的類不能被abstract, open, sealed等關鍵字修飾,且不能爲內部類
- 1.1之前,data修飾的類只能實現接口
需要注意的是,kotlin爲data生成的函數只針對主構造函數中定義的參數。因此,可以通過在類的主體中定義屬性來避免屬性被kotlin用於生成對應的函數。
嵌套類與內部類
kotlin中的內部類是基於嵌套類的,kotlin中將定義在一個類(如A)中的另一個類(如B)成爲嵌套類,嵌套類的聲明與普通類一樣,只需要class關鍵字修飾即可。但是,如果使用inner關鍵字修飾類B,則類B就成爲內部類。內部類與嵌套類的區別在於:內部類可以訪問外部類的成員變量和成員函數,而嵌套類不可以。這是因爲,內部類的實例保存了指向外部類實例的引用。基於內部類,還可以定義匿名內部類,如果匿名內部類的父接口只包含一個方法,此時可以使用lambda來進行編寫。
package my.inner.class
class OuterClass {
private var str = "outer class member"
//內嵌類
class NestedClass {
fun getStr(): String {
return "nested class member"
}
}
//內部類
inner class InnerClass {
fun getStr(): String {
return str
}
}
//參數可以傳入匿名內部類
fun addListener(view: View, listener: OnclickListener) {
view.addListener(listener)
}
fun main() {
addListener(object: OnclickListener {
override fun onClick(){
//body
}
})
}
}
枚舉類
kotlin中的枚舉類支持定義成員變量、抽象方法(枚舉類定義,枚舉常量實現)以及實現接口等功能。
package my.enum
enum class MyEnum(var value: Int) {//定義成員變量
SMALL(1) {
override fun nextGrade() = MIDDLE //實現抽象方法
}
MIDDLE(2) {
override fun nextGrade() = LARGE
}
LARGE(3) {
override fun nextGrade() = SMALL
}
abstract fun nextGrade(): MyEnum
}
class Main() {
fun main() {
val enums = MyEnum.values()
for(en: MyEnum in enums) {
println(en)
}
println(MyEnum.valueOf(1)) // print SMALL
}
}
object關鍵字
object既可以用於聲明匿名內部類,又可以用於定義單例,並且object實現的單例是線程安全的。通過object實現的單例,可以直接通過類名訪問單例中定義的成員。此外,object還用於定義伴生對象,伴生對象中可以通過@JvmStatic來定義靜態方法。
package my.object
object SingleTon { //延遲初始化,只有當第一次使用的使用纔會進行初始化
fun printStr() {
// body
}
}
class Outer() {
companion object Inner() { //當Outer被加載到JVM時,會被初始化,相當於Outer的靜態成員
fun method1() = "method1"
}
fun main() {
println(Outer.method1()) // 類似於Java中的靜態成員,但是運行時卻不是以靜態成員的身份執行,而是以一個實例的方法進行執行
}
}
類的別名(typealias)
通過關鍵字typealias來爲一個類定義別名,可以幫助開發人員簡化代碼的編寫
typealias FunctionHandler = (Int, String) -> Unit
class Outer {
inner class Inner
}
typealias OI = Outer.Inner
typealias Predicate<T> = (T) -> Boolean
fun foo(p: Predicate<Int>) = p(42)
fun main() {
val f: (Int) -> Boolean = { it > 0 }
println(foo(f)) // prints "true"
val p: Predicate<Int> = { it > 0 }
println(listOf(1, -2).filter(p)) // prints "[1]"
}
委託機制以及委託屬性
-
委託機制
-
委託屬性
函數與Lambda
- 函數定義
相比Java中的函數定義,kotlin中的函數通過fun關鍵字來定義,參數聲明方式爲變量名: 變量類型,此外最大的不同在於可以在函數定義時,爲函數的參數設置默認值,設置默認值之後,函數的調用可以不用傳遞具有默認值的參數,但是需要注意的是,可以省略的參數一定是在不帶默認值的參數之後,這樣纔不會引發歧義。此外,調用函數的時候可以用鍵值對的形式傳遞參數值,這樣更具可讀性。函數的返回值類型必須在定義函數時顯式指定,除非返回的類型爲Unit,或者函數體直接以表達式的形式來定義函數體。
fun method1(var1: Int = 3, var2: String, var3: Int = 1): Unit {
println("var1 : $var1, var2: $var2, var3: $var3")
}
fun method2(var1: Int = 1, var2: () -> Unit) {
var()
}
fun <T> method3(var1: Boolean = true, vararg vars: T): List<T> {
val result = ArrayList<T>()
for (item in vars) { //vars is implemented by Array
result.add(item)
}
return result
}
class Main {
fun main() {
method1(var2 = "var2")
method1(var1 = 1, var2 = "var2", var3 = 3)
method2(var2 = {
println("the variant var2 is executed")
})
}
}
- 本地函數
kotlin與Java中一樣,允許定義成員函數,此外,kotlin還允許在函數中定義其他函數,定義top-level函數,定義擴展函數等。其中,本地函數可以使用在外部函數定義的本地變量。 - 高階函數和Lambda
在kotlin中,高階函數是指某個函數的參數爲函數或者返回的類型爲函數,這樣的函數稱之爲高階函數。使用Lambda的代價是造成內存開銷,因爲每個Lambda都對應一個實例對象,因此,如果能夠搭配inline內聯函數來使用高階函數,這樣就會降低Lambda使用帶來的內存開銷,而使用內聯函數也會導致編譯之後的代碼量增加,因此不適合對函數體大的Lambda進行內聯,除非這個Lambda在高階函數中用於循環調用。