入門學習_Kotlin

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修飾的屬性需要滿足:

  1. top-level或者在object或companion object中定義
  2. 初始化的值爲基本類型或者String類型
  3. 不可以自定義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()等函數。但是,定義數據類需要滿足一下條件:

  1. 主構造函數必須包含至少一個參數
  2. 主構造函數的參數必須聲明爲val或var
  3. data修飾的類不能被abstract, open, sealed等關鍵字修飾,且不能爲內部類
  4. 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在高階函數中用於循環調用。

Reference

kotlin官方文檔
kotin語言中文站

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章