這篇文章會列出我認(rèn)為入門需要掌握的特性,如果要想應(yīng)用到項目中去的話可以先去GitHub上找一些優(yōu)秀的Kotlin項目學(xué)習(xí)一下Kotlin的編程思想。
Kotlin中文站:里面有一些參考資料以及一些推薦書籍。
如何在IDE中查看Kotlin編譯過后的Java代碼
很多時候直接看下編譯后的Java代碼比看別人的解釋容易得多。
以Android Studio為例:Tools -> Kotlin -> Show Kotlin Bytecode -> 會出現(xiàn)一個小窗口,點擊Decompile
基本語法
基本語法可以看這個java-to-kotlin(這個很重要,一定要先看一下,內(nèi)容并不多),里面是關(guān)于Java與Kotlin語法上的不同,當(dāng)然只看這些是不夠的
Lambda 表達(dá)式
在Kotlin中大家基本上都會用lambda表達(dá)式去寫代碼,這里不建議直接去看Kotlin lambda表達(dá)式的語法,自己剛開始寫Demo的時候編譯器會給出一些警告,指出某些代碼可以被轉(zhuǎn)換成lambda表達(dá)式,然后讓編譯器去幫你轉(zhuǎn)換,有了這些基本印象后再去看語法會感覺比較友好,具體的語法這里就不說了,內(nèi)容還是比較多的,可以自行查找。
訪問修飾符
private :類內(nèi)部可見
protected:類內(nèi)部及其子類可見
internal:Module內(nèi)可見
public :全局可見
變量
-
var表示可變變量(variable),val表示不可變變量(value),也就是后者是被final修飾的 - 變量類型不是必須的,編譯器可以自動識別,比如
val name = "xiaoming"編譯器會自動識別為String類型 - Kotlin是空安全的,對象默認(rèn)是不能為
null的,如果想要賦值為null在聲明對象類型時需要加上?,且可空類型不能直接調(diào)用其方法,需要使用?.或!!,如果為null前者會返回null后者會拋出空指針異常,具體如下:
var str1: String = "abc"
str1 = null // 編譯器會報錯
var str2: String? = "abc"
str2 = null // 沒問題
str1.length // 沒問題
str2.length // 編譯器會報錯
val out = str2?.length //沒問題
println(out) // 這里輸出null
str2!!.length // 這里運行時拋出空指針
if (str2 != null ) {
str2.length // 沒問題
}
-
var和val都是private的(就算顯式聲明了public也會被編譯成private),編譯器會自動生成getter/setter方法(val只有getter),在Kotlin中可以直接使用.來調(diào)用,編譯器會自己去調(diào)用它的getter/setter方法,如user.id = 1會去調(diào)用user對象的setId(1)
如果不想讓成員變量被外部訪問,可以顯示聲明變量為private,如private val id = 1,這樣編譯器就不會生成getter/setter
當(dāng)然getter/setter方法是可以被修改的,操作如下:
class User{
var id = 0
get() = field - 1
set(value) {
field = value + 1
}
}
其中field被稱為”幕后字段“指的就是當(dāng)前變量,像下面這種寫法是錯誤的,因為id = value + 1會繼續(xù)調(diào)用id的setId(value + 1)方法,就會產(chǎn)生死循環(huán)。
class User{
var id = 0
set(value) {
id = value + 1
}
}
字符串模板
在Kotlin中不需要使用+來拼接字符串,一個字符串中可以通過$來獲取變量值,如下
val name = "xiaoming"
println("name is $name") // 獲取變量值
println("length is ${name.length}") // 使用表達(dá)式
println("${'$'}29.18") // 打印 $ 符號
上面的代碼分別會輸出
name is xiaoming
length is 8
$29.18
函數(shù)
- 所有函數(shù)默認(rèn)是
public的 - 所有函數(shù)默認(rèn)是
final的(抽象函數(shù)和接口函數(shù)除外),想要不final需要加上open關(guān)鍵字修飾
// 可以被重寫
open fun canOverride() {}
// 無法被重寫
fun cannotOverride() {}
- 重寫父類函數(shù)需要在函數(shù)聲明前加上
overrider關(guān)鍵字
override fun toString() = "kotlin"
- 如果函數(shù)只有一個語句可以直接用賦值的形式
fun plus(x: Int,y: Int) : Int = x + y // 此處返回類型可以省略
fun sout(s: String) = print(s)
- Kotlin中函數(shù)的參數(shù)是可以有默認(rèn)值的,這樣就不用寫重載函數(shù)了
fun userInfo(id: Int, name: String = "xiaoming", sex: String = "male") {
println("id: $id, name: $name, sex: $sex")
}
fun test() {
userInfo(1) // 輸出:id: 1, name: xiaoming, sex: male
userInfo(2, "xiaohong") // 輸出:id: 2, name: xiaohong, sex: male
userInfo(1, sex = "female") // 輸出:id: 1, name: xiaoming, sex: female
}
如果想要提供給Java使用可以加上@JvmOverloads,這樣編譯成Java代碼時就會生成對應(yīng)重載函數(shù)
@JvmOverloads
fun userInfo(id: Int, name: String = "xiaoming", sex: String = "male") {
println("id: $id, name: $name, sex: $sex")
}
- 無返回類型的函數(shù)會返回一個
Unit對象,Unit可以理解成Java的Void,可以作為泛型對象,如果將Unit類型的函數(shù)賦值給一個變量,編譯器會給變量賦值一個Unit單例
val a = test()
編譯為Java之后就是這樣的
Unit a = Unit.INSTANCE;
Unit直接繼承于Any類(即Java中的Object類),只重寫了toString方法
override fun toString() = "kotlin.Unit"
嵌套函數(shù)
在Kotlin中函數(shù)是可以嵌套的,內(nèi)部函數(shù)可以訪問到外部函數(shù)的局部變量,且其本身只能被外部函數(shù)訪問。
fun outFun(name: String) {
fun nestFun() {
print(name)
}
nestFun()
}
擴(kuò)展函數(shù)
這是Kotlin的一個非常好用的特性,它可以為一個類添加函數(shù),在這個函數(shù)中可以訪問到對象的公有屬性和方法,聲明完擴(kuò)展函數(shù)之后,該類及其子類的對象就可以直接通過.來調(diào)用這個函數(shù),比如為CharSequence類添加一個toList方法,將字符逐個添加到一個列表中然后返回
fun CharSequence.toList(): List<Char> {
val list = ArrayList<Char>()
for (char in this) {
list.add(char)
}
return list
}
聲明完上面這段代碼之后,就可以直接調(diào)用了
val s = "asdf"
val list = s.toList()
除了擴(kuò)展函數(shù)之外,Kotlin還支持?jǐn)U展屬性,但是擴(kuò)展屬性不能直接賦值,只能設(shè)置它的getter/setter方法,實際上編譯成Java代碼后還是擴(kuò)展了兩個方法
var StringBuilder.lastChar: Char
get() = get(length - 1)
set(value) = setCharAt(length - 1, value)
高階函數(shù)
在Kotlin中函數(shù)可以作為參數(shù)傳入到另一個函數(shù)中
fun run(block: () -> Unit) {
block()
}
上面就是一個簡單的高階函數(shù),可以這樣使用,運行之后就會輸出run,這里用到了Lambda 表達(dá)式,不懂的可以再去看下
run {
print("run")
}
通過::來獲取某個對象的方法來傳入(::也可以用來獲取變量)
val runnable = Runnable {
print("run")
}
run(runnable::run)
內(nèi)聯(lián)函數(shù)
通過inline修飾的函數(shù)為內(nèi)聯(lián)函數(shù)
當(dāng)我們使用高階函數(shù)時,傳入的函數(shù)對象會被編譯成一個對象,然后再調(diào)用該對象的方法,這樣會增加內(nèi)存和性能的開銷,而如果使用內(nèi)聯(lián)函數(shù)的話就可以將方法的調(diào)用轉(zhuǎn)換為語句的調(diào)用
fun run(block: () -> Unit) {
block()
}
fun testRun() {
run {
print("run")
}
}
上面的代碼編譯成Java代碼大致是這樣的,可以看到在testRun方法中,直接被編譯成了語句調(diào)用
public final void run(@NotNull Function0 block) {
block.invoke();
}
public final void testRun() {
String var = "run";
System.out.print(var);
}
使用內(nèi)聯(lián)函數(shù)可以避免產(chǎn)生多余的對象,但是會增加編譯后代碼量,所以要避免內(nèi)聯(lián)代碼塊過大的函數(shù),如果一個函數(shù)中有包含多個函數(shù)參數(shù),可以通過noinline關(guān)鍵字來避免內(nèi)聯(lián)
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {}
類
- 所有類都繼承于
Any類,相當(dāng)于Java中的Object - 類與函數(shù)一樣默認(rèn)都是
public final的,想要類可以被繼承同樣需要用open關(guān)鍵字來修飾 - 類的繼承與接口的實現(xiàn)都是通過
:來表示的,中間由,隔開且沒有順序要求
open class A()
interface B
class C : A(), B
- 類的構(gòu)造函數(shù)在
init代碼塊中實現(xiàn),如果不需要對構(gòu)造函數(shù)添加訪問修飾符或者注解,那么constructor關(guān)鍵字可以省略
class Test constructor(a:Int) {
init {
println("Here is the constructor.")
println("value a= $a")
}
}
如果只想通過構(gòu)造函數(shù)給成員變量賦值的話可以直接這樣
class Test(val a:Int){
...
}
如果想讓構(gòu)造函數(shù)私有可以這樣
class Test private constructor() {
...
}
- 如果需要多個構(gòu)造函數(shù)可以在類的內(nèi)部使用
constructor關(guān)鍵字來聲明,這在Kotlin中叫做次構(gòu)造函數(shù),而聲明在類名上的叫做主構(gòu)造函數(shù),次構(gòu)造函數(shù)需要直接或間接的繼承主構(gòu)造函數(shù),具體可以看這里
class Test() {
init {
...
}
constructor(a:Int): this() {
...
}
}
- Kotlin中有兩種
class類,一種就是Java中Class,另外一種是Kotlin自由的KClass,它們的獲取方式也不一樣
val kClazz = Person::class
val clazz = Person::class.java
嵌套類
嵌套類編譯成Java代碼之后就是靜態(tài)內(nèi)部類
class Out {
...
class Nest {
...
}
}
val nestClass = Out.Nest()
內(nèi)部類
Kotlin中內(nèi)部類需要使用inner關(guān)鍵字修飾
class Out {
...
inner class Inner {
...
}
}
val innerClass = Out().Inner()
object關(guān)鍵字
object關(guān)鍵字可以簡單地創(chuàng)建一個單例類,其中的變量和方法都可以直接通過類名來調(diào)用
fun main(args: Array<String>) {
val string = Singleton.str;
Singleton.printMessage(string)
}
object Singleton {
val str = "Singleton"
fun printMessage(message: String) = println("$str $message")
}
可以用來寫工具類
object StringUtils {
fun isEmpty(str: String?) = str == null || str == ""
}
伴生對象
Kotlin中并沒有static關(guān)鍵字,如果需要靜態(tài)變量或是靜態(tài)方法的話就要使用伴生對象,使用companion object來聲明,伴生對象的命名可以省略,編譯器會使用默認(rèn)的命名Companion,一個類只能擁有一個伴生對象
fun main(args: Array<String>) {
val test = Test.create()
println(Test.TAG)
}
class Test {
companion object {
val TAG = "KotlinTest"
fun create() = Test()
}
}
上面的代碼會將TAG編譯成Test類的靜態(tài)成員變量,而create()方法其實不是一個真正的靜態(tài)方法,它是屬于伴生對象類的一個普通的public成員方法,伴生對象實際上是外部類的一個靜態(tài)單例內(nèi)部類,雖然可以直接通過Test類來調(diào)用create(),但其實編譯過后是這樣的
// kotlin code
val test = Test.create()
// java code
Test test = Test.Companion.create();
如果想要生成一個真正的靜態(tài)方法,可以使用@JvmStatic注解來實現(xiàn)
下面使用object和companion object寫一個延時加載的單例類
class Singleton private constructor() {
companion object {
fun getInstance() = Holder.instance
}
object Holder {
val instance = Singleton()
}
}
數(shù)據(jù)類
數(shù)據(jù)類類似于lombok的@Data注解,可以自動生成toString(),equals(),hashcode(),copy()等方法,具體可以去看一下編譯成的Java代碼
data class User(val id:Int, val name:String, val age:Int)
除了常用的getter/setter之外還可以這樣用
val user1 = User(1, "Ben", 25)
val user2 = user1.copy(id= 2, age = 23)
val (id, name, age) = user1
第二行代碼將user1拷貝給了user2并修改了id和age,第三行代碼將user1的三個數(shù)據(jù)分別賦值給了id,name,age三個變量,這個被稱為解構(gòu),之所以可以這樣寫是因為數(shù)據(jù)類還實現(xiàn)了componentN(),后面在運算符重載會講到
密封類
密封類本身是個抽象類,主要特點是它的構(gòu)造方法是私有的,直接繼承它的子類只能定義在密封類所在的文件中,無法定義在別的文件中,也就是說它限制了外部繼承,直接子類就是確定的那幾個,所以密封類也可以理解為功能更多的枚舉類,因為它可以有更多的屬性以及方法
sealed class Person(val name: String, var age: Int) {
class Male(name: String, age: Int) : Person(name, age)
class Female(name: String, age: Int) : Person(name, age)
}
Kotlin也對密封類使用when語句也做了優(yōu)化,可以不寫else,因為子類是確定的那幾個
fun test(person: Person) {
when (person) {
is Person.Male -> println("male")
is Person.Female -> println("female")
}
}
操作符重載
Kotlin中的各種操作符都對應(yīng)著一種方法,比如+,-,*,/分別對應(yīng)著plus,minus,times,div,這些方法都是可以被重載的,編寫時需要在方法前面加上operate來修飾
fun main(args: Array<String>) {
val a = Point(1, 4)
val b = Point(2, 3)
val c = a + b
c.printPoint()
}
class Point(val x: Int, val y: Int) {
operator fun plus(another: Point): Point {
return Point(x + another.x, y + another.y)
}
fun printPoint() {
println("x= $x, y= $y")
}
}
上面的代碼運行之后會輸出"x= 3, y= 7",此外還有很多操作符,這里就不列舉了,前面說到的componentN()其實也是操作符對應(yīng)的方法,val (id, name, age) = user編譯之后就是
int id = user.component1();
String name = user.component2();
int age = user.component3();
常量
使用const val來聲明一個常量,常量只能在object,companion object或是類的外部聲明,且只能是基本數(shù)據(jù)類型或是String類型,因為常量要求在編譯期就能確定它的值
const val pi = 3.14
object A {
const val pi = 3.14
}
class B {
companion object {
const val pi = 3.14
}
}
前面說過val是不可變變量,它和常量的區(qū)別是const val編譯之后是public的而val是pirvate的,訪問val只能通過它的getter方法,而getter方法又是可以修改的(如下),對于開發(fā)者來說,常量應(yīng)該是一個確定的值,所以val不是常量
object A {
val pi = 3.14
get() = field + Math.random()
}
其他
下面的這些還是屬于Kotlin入門的范疇,并不是不重要,只是內(nèi)容較多,就不詳細(xì)講了,網(wǎng)上相關(guān)的文章也有很多
-
Kotlin標(biāo)準(zhǔn)庫
Kotlin標(biāo)準(zhǔn)庫提供了一些好用的函數(shù),可以看下那些函數(shù)的實現(xiàn),對學(xué)習(xí)Kotlin也有很大幫助 -
委托/委托屬性
Kotlin語言是原生支持委托的,其中還包括了委托屬性 -
泛型
Kotlin的泛型和Java的泛型大致上是相同的,但是寫法上還是有點區(qū)別的,而且Kotlin可以使用inline和reified來支持真泛型 -
協(xié)程
Kotlin是有協(xié)程庫來支持協(xié)程的,Kotlin的協(xié)程可以理解為和線程池類似的線程框架,但是使用起來更加方便 -
集合操作符
Kotlin為集合提供了一系列操作符(實際上是集合的擴(kuò)展函數(shù)),類似于RxJava,可以鏈?zhǔn)秸{(diào)用 -
與Java交互
一般使用Kotlin開發(fā)避免不了與Java交互,對于調(diào)用Java代碼或是讓Java調(diào)用Kotlin代碼都可能會存在一些問題 -
Anko、ktx
Anko和ktx是為android設(shè)計的一個kotlin代碼庫,對于android開發(fā)可以了解一下