【Kotlin】簡介&變量&類&接口
【Kotlin】數字&字符串&數組&集合
【Kotlin】高階函數&Lambda&內聯函數
【Kotlin】表達式&關鍵字
文章目錄
- Kotlin_簡介&變量&類&接口
- `Kotlin`的特性
- `Kotlin`優勢
- 創建`Kotlin`項目
- 變量
- 變量保存了指向對象的引用
- 優先使用val來避免副作用
- 編譯期常量
- 后端變量`Backing Fields`
- 后端屬性(Blocking Property)
- 延遲初始化
- @操作符
- 類的定義:使用`class`關鍵字
- 創建類的實例
- 創建對象的執行過程
- 構造函數
- 主構造函數
- 次構造函數
- 構造方法默認參數
- init語句塊
- 數據類:使用`data class`定義
- 數據類定義了componentN方法
- 獲取類中成員函數的對象
- 構造函數引用
- 繼承
- Any
- Any:非空類型的跟類型
- Any?:所有類型的根類型
- 覆蓋
- 方法覆蓋
- 屬性覆蓋
- 抽象類
- 注釋
- 接口:使用`interface`關鍵字
- 函數:通過`fun`關鍵字定義
- `Unit`:讓函數調用皆為表達式
- 表達式函數體
- 無參主函數
- 可變長參數函數:使用`vararg`關鍵字
- 命名風格
- 類布局
- 接口實現布局
- 重載布局
- 冒號
- 類頭格式化
Kotlin_簡介&變量&類&接口
在5月18
日谷歌在I/O
開發者大會上宣布,將Kotlin
語言作為安卓開發的一級編程語言。并且會在Android Studio 3.0
版本全面支持Kotlin
。
Kotlin
是一個基于JVM
的新的編程語言,由JetBrains開發。JetBrains
作為目前廣受歡迎的
Java IDE IntelliJ
的提供商,在Apache
許可下已經開源其Kotlin
編程語言。Kotlin
可以編譯成Java
字節碼,也可以編譯成JavaScript
,方便在沒有JVM
的設備上運行。Kotlin
已正式成為Android
官方開發語言。
Kotlin官網
JetBrains
這家公司非常牛逼,開發了很多著名的軟件,他們在使用Java
的過程中發現java
比較笨重不方便,所以就開發了kotlin
,kotlin
是
一種全棧的開發語言,可以用它進行開發web
、web
后端、Android
等。
但是JetBrains團隊設計Kotlin所要面臨的第一個問題就是必須兼容他們所擁有的數百萬行Java代碼庫,這也代表了Kotlin基于整個Java社區所承載的使命之一,
即需要與現有的Java代碼完全兼容。這個背景也決定了Kotlin的核心目標–為Java程序員提供一門更好的編程語言。
很多開發者都說Google
學什么不好,非要學蘋果,出個android
的swift
版本,一定會搞不起來沒人用,所以不用浪費時間去學習。在這里想引用馬云
的一句話:
擁抱變化
Google
做事,向來言出必行,之前在推行Android Studio
時也是一片罵聲,吐槽各種不好用,各種慢。但是現在Android Studio
基本都已經普及了。
我相信Kotlin
也不會例外。所以我們不僅要學,還要要認真的學。
Kotlin
的特性
- 它更加易表現:這是它最重要的優點之一。你可以編寫少得多的代碼。
Kotlin
是一種兼容Java
的語言Kotlin
比Java
更安全,能夠靜態檢測常見的陷阱。如:引用空指針Kotlin
比Java
更簡潔,通過支持variable type inference,higher-order functions (closures),extension functions,mixins and first-class delegation
等實現Kotlin
可與Java
語言無縫通信。這意味著我們可以在Kotlin
代碼中使用任何已有的Java
庫;同樣的Kotlin
代碼還可以為Java
代碼所用Kotlin
在代碼中很少需要在代碼中指定類型,因為編譯器可以在絕大多數情況下推斷出變量或是函數返回值的類型。這樣就能獲得兩個好處:簡潔與安全Kotlin
是一種靜態類型的語言。這意味著,類型將在編譯時解析且從不改變
Kotlin
優勢
- 全面支持
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
上面說簡潔簡潔,到底簡潔在哪里?這里先用一個例子開始,在Java
開發過程中經常會寫一些Bean
類:
package com.charon.kotlinstudydemo;public class Person {private int age;private String name;private float height;private float weight;public int getAge() {return age;}public void setAge(int age) {this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public float getHeight() {return height;}public void setHeight(float height) {this.height = height;}public float getWeight() {return weight;}public void setWeight(float weight) {this.weight = weight;}@Overridepublic String toString() {return "Person name is : " + name + " age is : " + age + " height is :"+ height + " weight is :" + weight;}
}
使用Kotlin
:
package com.charon.kotlinstudydemodata class Person(var name: String,var age: Int,var height: Float,var weight: Float)
這個數據類,它會自動生成所有屬性和它們的訪問器,以及一些有用的方法,比如toString()
方法。
這里插一嘴,從上面的例子中我們可以看到對于包的聲明基本是一樣的,唯一不同的是kotlin
中后面結束不用分號。
創建Kotlin
項目
Google
宣布在Android Studio 3.0
版本會全面支持Kotlin
,
直接通過New Project
創建就可以,與創建普通Java
項目唯一不同的是要勾選Include Kotlin support
的選項。
創建完成后我們看一下MainActivity
的代碼:
// 定義包
package com.charon.kotlinstudydemo// 導入
import android.support.v7.app.AppCompatActivity
import android.os.Bundle// 定義類,繼承AppCompatActivity
class MainActivity : AppCompatActivity() {// 重寫方法用overide,函數名用fun聲明 參數是a: 類型的形式 ?是啥?它是指明該對象可能為null,// 如果有了?那在調用該方法的時候參數可以傳遞null進入,如果沒有?傳遞null就會報錯override fun onCreate(savedInstanceState: Bundle?) {// super super.onCreate(savedInstanceState)// 調用方法setContentView(R.layout.activity_main)}
}
變量
變量可以很簡單地定義成可變var
(可讀可寫)和不可變val
(只讀)的變量。如果var代表了variable(變量),那么val可看成value(值)的縮寫,
但是也有人覺得這樣并不直觀或準確,而是把val解釋成variable+final,即通過val聲明的變量具有Java中的final關鍵字的
效果(我們通過查看對val語法反編譯后轉化的java代碼,從中可以很清楚的發現它是用final實現的),也就是引用不可變。
因此,val聲明的變量是只讀變量,它的引用不可更改,但并不代表其引用對象也不可變。事實上,我們依然可以修改引用對象的可變成員。
聲明:
var age: Int = 18
val name: String = "charon"val book = Book("Thinking in Java") // 用val聲明的book對象的引用不可變
book.name = "Diving into Kotlin"
book.printName() // Diving into Kotlin
再提示一下:kotlin
中每行代碼結束不需要分號了,不要和java
是的每行都帶分號
字面上可以寫明具體的類型。這個不是必須的,但是一個通用的Kotlin
實踐是省略變量的類型我們可以讓編譯器自己去推斷出具體的類型,
Kotlin擁有比Java更加強大的類型推導功能,這避免了靜態類型語言在編碼時需要書寫大量類型的弊端:
var age = 18 // int
val name = "charon" // string
var height = 180.5f // flat
var weight = 70.5 // double
在Kotlin
中,一切都是對象。沒有像Java
中那樣的原始基本類型。
當然,像Integer
,Float
或者Boolean
等類型仍然存在,但是它們全部都會作為對象存在的。基本類型的名字和它們工作方式都是與Java
非常相似的,
但是有一些不同之處你可能需要考慮到:
-
數字類型中不會自動轉型。舉個例子,你不能給
Double
變量分配一個Int
。必須要做一個明確的類型轉換,可以使用眾多的函數之一:private var age = 18 private var weight = age.toFloat()
-
字符(
Char
)不能直接作為一個數字來處理。在需要時我們需要把他們轉換為一個數字:val c: Char='c' val i: Int = c.toInt()
-
位運算也有一點不同。在
Android
中,我們經常在flags
中使用或
:// Java int bitwiseOr = FLAG1 | FLAG2; int bitwiseAnd = FLAG1 & FLAG2;
// Kotlin val bitwiseOr = FLAG1 or FLAG2 val bitwiseAnd = FLAG1 and FLAG2
-
一個
String
可以像數組那樣訪問,并且也可以被迭代:var s = "charon" var c = s[2]for (a in s) {Log.e("@@@", a + ""); }
變量保存了指向對象的引用
當該對象被賦值給變量時,這個對象本身并不會被直接賦值給當前的變量。相反,該對象的引用會被賦值給該變量。
因為當前的變量存儲的是對象的引用,因此它可以訪問該對象。
如果你使用val來聲明一個變量,那么該變量所存儲的對象的引用將不可修改。然而如果你使用var聲明了一個變量,你可以對該變量重新賦值。
例如,如果我們使用代碼: x = 6
,將x的值賦為6,此時會創建一個值為6的新Int對象,并且x會存放該對象的引用。下面新的引用會替代原有的引用值被存放在x中:
注意: 在Java中,數字類型是原生類型,所以變量存儲的是實際數值。但是在Kotlin中的數字也是對象,而變量僅僅存儲該數字對象的引用,并非對象本身。
優先使用val來避免副作用
在很多Kotlin的學習資料中,都會傳遞一個原則:優先使用val來聲明變量。這相當正確,但更好的理解可以是:盡可能采用val、不可變對象及純函數來設計程序。
關于純函數的概念,其實就是沒有副作用的函數,具備引用透明性。
簡單來說,副作用就是修改了某處的某些東西,比如說:
- 修改了外部變量的值
- IO操作,如寫數據到磁盤
- UI操作,如修改了一個按鈕的可操作狀態
來看一個實際的例子:先用var來聲明一個變量a,然后在count函數內部對其進行自增操作:
var a = 1
fun count(x: Int) {a = a + 1println(x + a)
}
如果執行兩次count(1)函數,第一次的執行結果是3、第二次的執行結果是4。這顯然是受到了外部變量a的影響,這個就是典型的副作用。
編譯期常量
已知值的屬性可以使用const
修飾符標記為編譯期常量(類似java
中的public static final
)。
const
只能修復val
不能修復var
,這些屬性需要滿足以下要求:
- 位于頂層或者是
object
的一個成員 - 用
String
或原生類型值初始化 - 沒有自定義
getter
// Const val are only allowed on top level or in objects
const val NAME: String = "charon"class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)}
}
后端變量Backing Fields
Kotlin
會默認創建set get
方法,我們也可以自定義get set
方法:
在kotlin
的getter
和setter
是不允許調用本身的局部變量的,因為屬性的調用也是對get
的調用,因此會產生遞歸,造成內存溢出。
例如:
var count = 1
var size: Int = 2set(value) {Log.e("text", "count : ${count++}")size = if (value > 10) 15 else 0}
這個例子中就會內存溢出。
kotlin
為此提供了一種我們要說的后端變量,也就是field
。編譯器會檢查函數體,如果使用到了它,就會生成一個后端變量,否則就不會生成。
我們在使用的時候,用field
代替屬性本身進行操作。按照慣例set
參數的名稱是value
,但是如果你喜歡你可以選擇一個不同的名稱。
setter通過field標識更新變量屬性值。field指的是屬性的支持字段,你可以將其視為對屬性的底層值的引用。在getter和setter中使用field代替屬性名稱
很重要,因為這樣可以阻止你陷入無限循環中。
class A {var count = 1var size: Int = 2set(value) {field = if (value > 10) 15 else 0}get() {return if (field == 15) 1 else 0}
}
fun main() {val a = A()a.size = 11println("${a.size}")
}
//
1
如果我們不手動寫getter和setter方法,編譯器會在編譯代碼時添加以下代碼段:
var myProperty: Stringget() = fieldset(value) {field = value}
自定義set和get的重點在field,field指代當前參數,類似于java的this關鍵字。
這意味著無論何時當你使用點操作符來獲取或設置屬性值時,實際上你總是調用了屬性的getter或是setter。那么,為什么編譯器要這么做呢?
為屬性添加getter和setter意味著有訪問該屬性的標準方法。getter處理獲取值的所有請求,而setter處理所有屬性值設置的請求。
因此,如果你想要改變處理這些請求的方式,你可以在不破壞任何人代碼的前提下進行。通過將其包裝在getter和setter中來輸出對屬性的直接訪問稱為數據隱藏。
在某些情況下,無參的函數與只讀屬性可互換通用。雖然語義相似,但在以下情況中,更多的是選擇使用屬性而不是方法。
- 不會拋出任何異常。
- 具有O(1)的復雜度。
- 容易計算(或者運行一次之后緩存結果)。
- 每次調用返回同樣的結果。
后端屬性(Blocking Property)
它實際上是一個隱含的對屬性值的初始化聲明。能有效避免空指針問題的產生。
var size: Int = 2;
private var _table: Map<toString, Int>? = nullval table: Map<String, Int> get() {if (_table == null) {_table = HashMap()}return _table ?: throw AssertionError("Set to null by another thread")}
在Java中,訪問private成員變量需要通過getter和setter來實現,此處通過table來獲取_table變量,優化了Java中函數調用帶來的開銷。
延遲初始化
在類內聲明的屬性必須初始化,如果設置非null
的屬性,應該將此屬性在構造器內進行初始化。
假如想在類內聲明一個null
屬性,在需要時再進行初始化(最典型的就是懶漢式單例模式),這就與Kotlin
的規則是相背的,此時我們可以聲明一個屬性并
延遲其初始化,此屬性用lateinit
修飾符修飾。
class MainActivity : AppCompatActivity() {lateinit var name : Stringoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)var test = MainActivity()// 要先調用方法讓其初始化test.init()// 再使用其屬性Log.e("@@@", test.name)}fun init() {// 延遲初始化name = "charon"}
}
需要注意的是,我們在使用的時候,一定要確保屬性是被初始化過的,通常先調用初始化方法,否則會有異常。
如果只是用lateinit
聲明了,但是還沒有調用初始化方法就使用,哪怕你判斷了該變量是否為null
也是會crash
的:
private lateinit var test: Stringprivate fun switchFragment(position: Int) {if (test == null) {LogUtil.e("@@@", "test is null")} else {LogUtil.e("@@@", "test is not null")check(test)}
}
會報kotlin.UninitializedPropertyAccessException: lateinit property test has not been initialized
We’ve added a new reflection API allowing you to check whether a lateinit variable has been initialized:
這里想要判斷是否初始化了,需要用isInitialized來判斷:
class MyService{fun performAction(): String = "foo"
}class Test{private lateinit var myService: MyServicefun main(){// 如果 myService 對象還未初始化,則進行初始化if(!::myService.isInitialized){println("hha")myService = MyService()}}
}
注意: ::myService.isInitialized可用于判斷adapter變量是否已經初始化。雖然語法看上去有點奇怪,但這是固定的寫法。::
前綴不能省。::是一個引用運算符,一般用于反射相關的操作中,可以引用屬性或者函數。
這里可以寫成::myService.isInitialized
或this::myService.isInitialized
。
如果在listener或者內部類中,可以這樣寫this@OuterClassName::myService.isInitialized
那lateinit有什么用呢? 每次使用還要判斷isInitialized。
The primary use-case for lateinit is when you can’t initialize a property in the constructor but can guarantee that it’s initialized “early enough” in some sense that most uses won’t need an isInitialized check. E.g. because some framework calls a method initializing it immediately after construction.
Lateinit Initialization is used when you want to initialize a variable at a later stage, especially when it’s non-null and must be initialized before use.
It’s commonly used in dependency injection and testing.
除了使用lateinit
外還可以使用by lazy {}
效果是一樣的:
private val test by lazy { "haha" }private fun switchFragment(position: Int) {if (test == null) {LogUtil.e("@@@", "test is null")} else {LogUtil.e("@@@", "test is not null ${test}")check(test)}
}
執行結果:
test is not null haha
那lateinit
和by lazy
有什么區別呢?
by lazy{}
只能用在val
類型而lateinit
只能用在var
類型lateinit
不能用在可空的屬性上和java
的基本類型上,否則會報lateinit
錯誤- lateinit在分配之前不會初始化變量,而by lazy在第一次訪問時初始化它。
- 如果在初始化之前訪問,lateinit會拋出異常,而lazy則可以確保已初始化。
lazy的背后是接收一個lambda并返回一個Lazy實例的函數,第一次訪問該屬性時,會執行lazy對應的Lambda表達式并記錄結果,后續訪問該屬性時只是返回記錄的結果。
另外系統會給lazy屬性默認加上同步鎖,也就是LazyThreadSafetyMode.SYNCHRONIZED,它在同一時刻只允許一個線程對lazy屬性進行初始化,所以它是線程安全的。
但若你能確認該屬性可以并行執行,沒有線程安全問題,那么可以給lazy傳遞LazyThreadSafetyMode.PUBLICATION參數。
你還可以給lazy傳遞LazyThreadSafetyMode.NONE參數,這將不會有任何線程方面的開銷,當然也不會有任何線程安全的保證。例如:
val sex: String by lazy(LazyThreadSafetyMode.PUBLICATION) {// 并行模式if (color == "yellow") "male" else "female"
}val sex: String by lazy(LazyThreadSafetyMode.NONE) {// 不做任何線程保證也不會有任何線程開銷if (color == "yellow") "male" else "female"
}
- 盡量不要使用lateinit來定義不可空類型的變量,可能會在使用時出現null的情況
- 只讀變量(val修飾)可以使用by lazy { }實現懶加載,可變變量(var修飾)使用改寫get方法的形式實現懶加載
// 只讀變量
private val lazyImmutableValue: String by lazy {"Hello"
}// 可變變量
private var lazyValue: Fragment? = nullget() {if (field == null) {field = Fragment()}return field}
當您稍后需要在代碼中初始化var時,請選擇lateinit,它將被重新分配。當您想要初始化一個val值一次時,特別是當初始化的計算量很大時,請選擇by lazy。
val name: String by lazy {getName()}
這樣,當第一次使用name引用時,getName()函數只會被調用一次。此外,還可以使用函數引用代替lambda表達式:
val name: String by lazy(::getName)fun getName() : String {println("computing name")return "Mockey"
}
@操作符
在Kotlin中,@操作符主要有兩個作用:
- 限定this的對象類型
class User {inner class State {fun getUser(): User {return this@User // 返回User}fun getState(): State {return this@State // 返回State}}
}
- 作為標簽使用
當把@操作符作為標簽使用時,可以跳出雙層for循環和forEach函數。
例如:
val listA = listOf(1, 2, 3, 4, 5, 6)
val listB = listOf(2, 3, 4, 5, 6, 7 )loop@ for(itemA in listA) {var i: Int = 0for (itemB in listB) {i++if (item > 2) {break @loop // 當itemB > 2時,跳出循環}println("itemB: $itemB")}
}
類的定義:使用class
關鍵字
當你在定義類的時候,需要想想該類所創建的對象需要什么:
-
每個對象自身的特點
對象自身的特點稱為屬性(properties)。它們代表了對象自身的狀態(數據),并且該類中的每一個對象都有自己獨特的數值。
例如,一個狗(Dog)類可能有名字(name)、體重(weight)和品種(breed)屬性。一個歌曲(Song)類可能有標題(title)和演唱者(artist)屬性。 -
每個對象的行為
對象的行為是它們的函數(functions)。它們決定了對象的行為,并且可能會使用對象的屬性。例如,上面提到的Dog類,可能具有吠叫(bark)函數;
Song這個類可能會有播放(play)函數。
類可以包含:
- 構造函數和初始化塊
- 函數
- 屬性
- 嵌套類和內部類
- 對象聲明
你可以將類想象成一個對象的模板,因為它告訴編譯器如何創建該特定類的對象。它還將告訴編譯器每個對象應該具有哪些屬性,并且從該類生成的每個對象都可以
擁有自己獨有的屬性值。例如,每個Dog對象都有自己的名稱、重量和品種屬性,每個Dog的屬性值都可以是不同的。
class Dog(val name: String, var weight: Int, val breed: String){fun bark() {}
}
如果有參數的話你只需要在類名后面寫上它的參數,如果這個類沒有任何內容可以省略大括號:
class Dog(val name: String, var weight: Int, val breed: String)
創建類的實例
val myDog = Dog("Fido", 70, "Mixed" )
上面的類有一個默認的構造函數。
注意:創建類的實例不用new
了啊。
類中所定義的函數又稱為成員函數,有時也被稱為方法。
創建對象的執行過程
var myDog = Dog("Fido", 70, "Mixed")
- 系統會為每個傳入Dog構造函數的參數創建一個對象。它會創建一個值為“Fido”的String,一個值為70的Int,以及一個值為“Mixed”的String。
- 系統會為一個新的Dog對象分配空間,并且Dog構造函數會被調用。
- Dog構造函數定義了三個屬性:名稱、重量以及品種。在這個現象背后,每一個屬性實際上是一個變量。對于構造函數中定義的每個屬性,都會有一個相應類型的變量被創建。
- 相應的變量的引用將會被賦值給Dog的屬性。例如,值為“Fido”的String將會被賦值給name屬性。
- 最后,這個新的Dog對象的引用將會被賦值給名為myDog的Dog變量。
構造函數
構造函數包含了初始化對象所需的代碼。它在對象被分配給引用之前運行,這意味著你有機會對對象進行一些內部操作以便其被使用。大多人使用構造函數來定義對
象的屬性,并且給這些屬性賦值。每當你創建一個新的對象,該對象所屬的類的構造函數將會被調用。構造函數在你初始化對象時被調用。它通常被用于定義對象的
屬性,并且對屬性賦值。
在Kotlin
中的一個類可以有一個主構造函數和一個或多個次構造函數。
主構造函數
主構造函數是類頭的一部分:它跟在類名(和可選的類型參數)后:
class Person constructor(name: String, surname: String) {
}
如果主構造函數沒有任何注解或者可見性修飾符,可以省略constructor
關鍵字:
class Person(name: String, surname: String) {
}
主構造函數不能包含任何的代碼。初始化的代碼可以放到以init
關鍵字作為前綴的初始化塊中:
class Person constructor(name: String, surname: String) {init {print("name is $name and surname is $surname")}
}
如果構造函數有注解或可見性修飾符,那么constructor
關鍵字是必需的,并且這些修飾符在它前面:
class Person private @Inject constructor(name: String, surname: String) {init {print("name is $name and surname is $surname")}
}
次構造函數
類也可以聲明前綴有constructor
的次構造函數:
class Person{constructor(name: String) {print("name is $name")}
}
如果類有一個主構造函數,每個次構造函數都需要委托給主構造函數(不然會報錯), 可以直接委托或者通過別的次構造函數間接委托。
委托到同一個類的另一個構造函數用this
關鍵字即可:
class Person constructor(name: String) {constructor(name: String, surName: String) : this(name) {Log.d("@@@", "name is : $name surName is : $surName")}
}
使用該對象:
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)Person("charon", "chui")}
}
就會在logcat
上打印:
09-20 16:51:19.738 6010-6010/com.charon.kotlinstudydemo D/@@@: name is : charon surName is : chui
如果一個非抽象類沒有聲明任何(主或次)構造函數,它會有一個生成的不帶參數的主構造函數。構造函數的可見性是public
。
如果你不希望你的類有一個公有構造函數,你需要聲明一個帶有非默認可見性的空的主構造函數:
class Person private constructor(name: String) {
}
open class Base(p: Int)
class Example(p: Int) : Base(p)
如果該類有一個主構造函數,那么其基類型可以用主構造函數的參數進行初始化。
如果該類沒有主構造函數,那么每個次構造函數必須使用super關鍵字初始化其類型,或者委托給另一個構造函數初始化。如:
class Example : View {constructor(ctx: Context) : super(ctx)constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
構造方法默認參數
class Bird(val weight: Double = 0.00, val age: Int = 0, val color: String = "blue")val bird1 = Bird(color = "black")
val bird2 = Bird(weight = 1000.00, color = "black")
上面在Bird類中使用了val或者var來聲明構造方法的參數。這一方面代表了參數的引用可變性,另一方面也使得我們在構造類的語法上得到了簡化。事實上,
構造方法的參數名前當然可以沒有val和var。然而帶上它們之后就等價于在Bird類內部聲明了一個同名的屬性,我們可以用this來進行調用。
比如,上面定義的Bird類就類似于一下實現:
// 構造方法參數名前沒有val
class Bird (weight: Double = 0.00, age: Int = 0, color: String = "blue"){val weight: Doubleval age: Intval color: Stringinit {this.weight = weight // 構造方法參數可以在init語句中被調用this.age = agethis.color = color}
}
init語句塊
Kotlin引入了一種叫作init語句塊的語法,它屬于上述構造方法的一部分,兩者在表現形式上卻是分離的。Bird類的構造方法在類的外部,它只能對參數進行賦值。
如果我們需要在初始化時進行其他的額外操作,那么我們就可以使用init語句塊來執行。比如:
class Bird(weight: Double, age: Int, color: String) {init {println("the weight is ${weight}")}
}
當沒有val或者var的時候,構造函數的參數可以在init語句塊被直接調用。除此之外,不能在其他地方使用。以下是一個錯誤的用法:
class Bird(weight: Double, age: Int, color: String) {fun printWeight() {print(weight) // Unresolved reference: weight}
}
事實上,我們的構造方法還可以擁有多個init,他們會在對象被創建時按照類中從上到下的順序先后執行。例如:
class Bird(weight: Double, aget: Int, color: String) {val weight: Doubleval age: Intval color: Stringinit {this.weight = weightthis.age = age}init {this.color = color}
}
可以發現,多個init語句塊有利于進一步對初始化的操作進行職能分離,這在復雜的業務開發中顯得特別有用。
數據類:使用data class
定義
數據類通常需要重寫equals()、hashCode()、toString()這幾個方法。其中,equals()方法用于判斷兩個數據類是否相等。
hashCode()方法作為equals()的配套方法,也需要一起重寫,否則會導致HashMap、HashSet等hash相關的系統類無法正常工作。
toString()方法用于提供更清晰的輸入日志,否則一個數據類默認打印出來的就是一行內存地址。所以我們在Java中創建一個數據類時要寫很多代碼,
但是在Kotlin中你只需要一行代碼。
數據類是一種非常強大的類:
public class Artist {private long id;private String name;private String url;private String mbid;public long getId() {return id;}public void setId(long id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getUrl() {return url;}public void setUrl(String url) {this.url = url;}public String getMbid() {return mbid;}public void setMbid(String mbid) {this.mbid = mbid;}@Override public String toString() {return "Artist{" +"id=" + id +", name='" + name + '\'' +", url='" + url + '\'' +", mbid='" + mbid + '\'' +'}';}
}
使用Kotlin
:
data class Artist(var id: Long,var name: String,var url: String,var mbid: String)
數據類自動覆蓋它們的equals方法以改變操作符的行為,由此通過檢查對象的每個屬性值來判斷是否相等。
例如,假設你創建了兩個屬性值完全相同的Artist對象,
使用操作符對它們進行比較將返回true,因為它們存放了相同的數據:除了提供從Any父類繼承的equals方法的新實現,數據類還覆蓋了hashCode和toString方法。
通過數據類,會自動提供以下函數:
- 所有屬性的
get() set()
方法 equals()
hashCode()
copy()
toString()
componentN()
如果我們使用不可修改的對象,就像我們之前講過的,假如我們需要修改這個對象狀態,必須要創建一個新的或者多個屬性被修改的實例。
這個任務是非常重復且不簡潔的。
舉個例子,如果要修改Person
類中charon
的age
:
data class Person(val name: String,val age: Int)
val charon = Person("charon", 18)
val charon2 = charon.copy(age = 19)
如上,我們拷貝了charon
對象然后只修改了age
的屬性而沒有修改這個對象的其它狀態。
如果你要在Kotlin聲明一個數據類,必須滿足以下幾點條件:
- 數據類必須擁有一個構造方法,該方法至少包含一個參數,一個沒有數據的數據類是沒有任何用處的。
- 與普通的類不同,數據類構造方法的參數強制使用var或者val進行聲明
- data class之前不能用abstract、open、sealed或者inner進行修飾
- 在Kotlin 1.1版本前數據類只允許實現接口,之后的版本既可以實現接口也可以繼承類,
更多可看Feedback Request: Limitations on Data Classes
與任何其他類一樣,你可以向數據類添加屬性和方法,只需要將它們包含在類主體中。但是有一個大問題,就是在編譯器生成數據類的方法實現時,
比如覆蓋equals方法和創建copy方法,它僅包含在主構造函數中定義的屬性。因此如果你在數據類主體中定義添加的屬性,則它們不會被包含到任何編譯器生成的方法中。
數據類定義了componentN方法
定義數據類時,編譯器會自動向該類添加一組方法,你可以將其作為訪問對象屬性值的替代方法。它們被稱為componentN方法,其中N表示被訪問屬性的編號(按聲明排序)。
多聲明,也可以理解為變量映射,這就是編譯器自動生成的componentN()
方法。
var personD = PersonData("PersonData", 20, "male")
var (name, age) = personDLog.d("test", "name = $name, age = $age")//輸出
// name = PersonData, age = 20
上面的多聲明,大概可以翻譯成這樣:
var name = f1.component1()
var age = f1.component2()
數據類的缺點,數據類雖然使用的時候很簡單,但是因為它會默認幫我們自動生成很多代碼,里面的有些代碼其實在某些情況下我們并不需要,例如copy、component等,
如果在項目中使用了大量的數據類,那就會引起包大小增加的問題。
具體可見Data classes in Kotlin: how do they impact application size
雖然對于release包,使用了R8,ProGuard,DexGuard等優化器。這些可以刪除未使用的方法,這意味著它們可以優化數據類。
像componentN()、copy()如果沒有使用的話,會默認給刪除。但是對于toString()、equals()、hashCode()方法則不會刪除。
對于有些不需要toString()、equals()、hashCode()方法的類如果使用數據類就會導致多生成這些代碼,所以在使用數據類的時候不要去為了簡單而亂用,
也要去想想是否需要這些方法?是否需要設計成數據類。
獲取類中成員函數的對象
fun main(args: Array<String>) {var user = User::special// 調用invoke函數執行user.invoke(User("Jack", 30))// 利用反射機制獲取指定方法的內容var method = User::class.java.getMethod("special")method.invoke(User("Tom", 20))
}class User(val name: String, val age: Int) {fun special() {println("name:${name") age:${age}");}
}
在上面的實例中,User類還有一個special函數,使用User::special可以獲取成員函數的對象,然后使用invoke函數調用special函數,以獲取該函數的內容。
構造函數引用
構造函數的引用和屬性、方法類似,構造函數可以作用于任何函數類型的對象,該函數的對象與構造函數的參數相同,可以使用::操作符加類名的方式來引用構造函數。
class Foofun function(factory: () -> Foo) {val x: Foo = factory()
}// 使用::Foo方式調用類Foo的無參數構造函數
fun main() {function(::Foo)
}
繼承
在Kotlin
中所有類都有一個共同的超類Any
,這對于沒有超類型聲明的類是默認超類:
class Person // 從 Any 隱式繼承
Any
不是java.lang.Object
。它除了equals()
、hashCode()
和toString()
外沒有任何成員。
在Java中,類默認是可以被繼承的,除非你主動加final修飾符。而在Kotlin中恰好相反,默認是不可被繼承的,除非你主動加可以繼承的修飾符,那便是open,
如果不加open,那它在轉化為Java代碼時就是final的:
class Bird {val weight: Double = 500.0val color: String = "blue"val age: Int = 1fun fly() {}
}
將Bird類編譯后轉換為Java的代碼:
public final class Bird {private final double weight = 500.0;private final String color = "blue";private final int age = 1;public final double getWeight() {return this.weight;}public final String getColor() {return this.color;}public final int getAge() {return this.age;}public final void fly() {}
}
所以Kotlin中所有的類默認都是不可繼承的(final
),為什么要這樣設計呢?引用Effective Java
書中的第17條:要么為繼承而設計,并提供文檔說明,
要么就禁止繼承。所以我們只能繼承那些明確聲明open
或者abstract
的類:要聲明一個顯式的超類型,我們把類型放到類頭的冒號之后:
open class Person(num: Int)
// 繼承
class SuperPerson(num: Int) : Person(num)
冒號后面的Person(num)會調用Person類的構造函數,以確保所有的初始化代碼(例如給屬性賦值)能夠被執行。
調用父類構造函數是強制性的:如果父類有主構造函數,你必須在子類頭中調用它,否則代碼將無法通過編譯。
請記住,即使你沒有在父類中顯式地添加構造函數,編譯器也會在編譯代碼的時候自動創建一個空構造函數。
假如我們不想為Person類添加構造函數,因此編譯器在編譯代碼的時候創建了一個空構造函數。該構造函數通過使用Person()被調用。
注意: 上面在說到繼承的時候class SuperPerson(num: Int) : Person(num)
在父類后面必須加上括號,這是為了能夠調用到父類的主構造函數。
Kotlin中規定,當一個類既有主構造函數又有次構造函數時,所有的次構造函數都必須調用主構造函數(包括間接調用)。
但是如果類沒有主構造函數,那么每個次構造函數必須使用super
關鍵字初始化其基類型,或委托給另一個構造函數做到這一點。 這里很特殊,在Kotlin
中是允許類中只有次構造函數,沒有主構造函數的。當一個類沒有顯式的定義主構造函數且定義了次構造函數時,它就是沒有主構造函數的。
如果該類有一個主構造函數,其基類必須用基類型的主構造函數參數就地初始化。
如果類沒有主構造函數,那么每個次構造函數必須使用super
關鍵字初始化其基類型,或委托給另一個構造函數做到這一點。
注意,在這種情況下,不同的次構造函數可以調用基類型的不同的構造函數:
class MyView : View {constructor(ctx: Context) : super(ctx)constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
也就是MyView類的后面沒有顯式的定義主構造函數,同時又定義了次構造函數。所以現在MyView類是沒有主構造函數的。
那么既然沒有主構造函數,繼承View類的時候也就不需要再在View類后加上括號了。
其實原因就是這么簡單,只是很多人在剛開始學習Kotlin的時候沒能理解這對括號的意義和規則,因此總感覺繼承的
寫法有時候要加上括號,有時候又不要加,搞得暈頭轉向的,而在你真正理解了規則之后,就會發現其實還是很好懂的。
另外,由于沒有主構造函數,次構造函數只能直接調用父類的構造函數,上述代碼也是將this關鍵字換成了super關鍵字,這部分就很好理解了。
Any
我們都知道,Java并不能在真正意義上被稱為一門"純面向對象"語言,因為它的原始類型(如int)的值與函數等并不能被視作對象。
但是Kotlin不同,在Kotlin的類型系統中,并不區分原始類型(基本數據類型)和包裝類型,我們使用的始終是同一個類型。雖然從嚴格意義上,我們不能說
Kotlin是一門純面向對象的語言,但它顯然比Java有更純的設計。
Any:非空類型的跟類型
與Object作為Java類層級結構的頂層類似,Any類型是Kotlin中所有非空類型(如String、Int)的超類,如:
與Java不同的是,Kotlin不區分"原始類型"(primitive type)和其他的類型,他們都是同一類型層級結構的一部分。 如果定義了一個沒有指定父類型的類型,
則該類型將是Any的直接子類型。如:
class Animal(val weight: Double)
Any?:所有類型的根類型
如果說Any是所有非空類型的根類型,那么Any?才是所有類型(可空和非空類型)的根類型。這也就是說?Any?是?Any的父類型。
覆蓋
方法覆蓋
只能重寫顯示標注可覆蓋的方法:
open class Person(num: Int) {open fun changeName(name: String) {}fun changeAge(age: Int) {}
}class SuperPerson(num: Int) : Person(num) {override fun changeName(name: String) {// 通過super關鍵字調用超類實現super.changeName(name)}
}
SuperPerson.changeName()
方法前面必須加上override
標注,不然編譯器將會報錯。如果像上面Person.changeAge()
方法沒有標注open
,
則子類中不能定義相同的方法:
class SuperPerson(num: Int) : Person(num) {override fun changeName(name: String) {super.changeName(name)}// 編譯器報錯fun changeAge(age: Int) {}// 重載是可以的fun changeAge(name: String) {}// 重載是可以的fun changeAge(age: Int, name: String) {}
}
標記為override
的成員本身是開放的,也就是說,它可以在子類中覆蓋。如果你想禁止再次覆蓋,可以使用final
關鍵字:
open class SuperPerson(num: Int) : Person(num) {final override fun changeName(name: String) {super.changeName(name)}
}
屬性覆蓋
屬性覆蓋與方法覆蓋類似,只能覆蓋顯式標明open
的屬性,并且要用override
開頭:
open class Person(num: Int) {open val name: String = ""open fun changeName(name: String) {}fun changeAge(age: Int) {}
}open class SuperPerson(num: Int) : Person(num) {override val name: Stringget() = super.namefinal override fun changeName(name: String) {super.changeName(name)}}
每個聲明的屬性可以由具有初始化器的屬性或者具有get
方法的屬性覆蓋,如果某個屬性在父類中被定義為val,你可以在子類中使用var屬性覆蓋它。
只需要覆蓋該屬性并將其聲明為var即可。請注意,這只適用于這一種方式。如果嘗試使用val覆蓋var屬性,編譯器將會感到沮喪并拒絕編譯你的代碼。
抽象類
類和其中的某些成員可以聲明為abstract
。抽象成員在本類中可以不用實現。需要注意的是,我們并不需要用open
標注一個抽象類或者函數——因為這不言而喻。
我們可以用一個抽象成員覆蓋一個非抽象的開放成員:
open class Base {open fun f() {}
}abstract class Derived : Base() {override abstract fun f()
}
注釋
和Java
差不多
// 這是一個行注釋/* 這是一個多行的塊注釋。 */
接口:使用interface
關鍵字
接口可以讓你在父類層次結構之外定義共同的行為,接口用于為共同行為定義協議,使你可以不依賴嚴格的繼承結構卻又可以利用多態。與抽象類類似,接口不能被
實例化且可以定義抽象或具體的方法和屬性,但兩者有一個關鍵的不同點:類可以實現多個接口,但是只能繼承于一個直接父類。所以接口不僅擁有抽象類的優點,
而且使用起來更加靈活。
interface FlyingAnimal {fun fly()
}
雖然Kotlin接口支持屬性聲明,然而它在Java源碼中是通過一個get方法來實現的。在接口的屬性并不能像Java接口那樣,被直接賦值一個常量。如以下這樣是錯誤的:
interface Flyer {val height = 1000 // error Property initializers are not allowed in interfacesval speed: Int// 可以支持默認實現方法,反編譯可以看到是通過靜態內部類來提供fly方法的默認實現的,Java8也開始支持了接口方法的默認實現fun fly() {println("I can fly")}
}
Kotlin提供了另外一種方式來實現這種效果:
interface Flyer {val height get() = 1000
}
一個類實現接口時:
class Bird() : Flyer {// ...
}
接口的后面不用加上括號,因為它沒有構造函數可以去調用。
函數:通過fun
關鍵字定義
fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)
}
如果你沒有指定它的返回值,它就會返回Unit
,Unit
與Java
中的void
類似,但是Unit
是一個類型,而void只是一個關鍵字。Unit
可以省略。
你當然也可以指定任何其它的返回類型:
fun maxOf(a: Int, b: Int): Int {if (a > b) {return a} else {return b}
}
Unit
:讓函數調用皆為表達式
如果函數返回Unit
類型,該返回類型應該省略:
fun foo() { // 省略了 ": Unit"}
之所以不能說Java中的函數調用皆是表達式,是因為存在特例void。眾所周知,在Java中如果聲明的函數沒有返回值,那么它就需要用void來修飾,如:
void foo() {System.out.println("return nothing")
}
所以foo()就不具有值和類型信息,它就不能算作一個表達式。在Kotlin中,函數在所有的情況下都具有返回類型,所以他們引入了Unit來替代Java中的void關鍵字。
Unit與Int一樣,都是一種類型,然而它不代表任何信息,用面向對象的術語來描述就是一個單例,它的實例只有一個,可寫為()。
表達式函數體
如果一個函數的返回的結果可以使用一個表達式計算出來,你可以不使用括號而是使用等號:
fun add(x: Int,y: Int) : Int = x + y // 省略了{}
Kotlin支持這種單行表達式與等號的語法來定義函數,叫做表達式函數體,作為區分,普通的函數聲明則可以叫做代碼塊函數體。
如你所見,在使用表達式函數體的情況下我們可以不聲明返回值類型,這進一步簡化了語法。
我們可以給參數指定一個默認值使的它們變的可選,這是非常有幫助的。這里有一個例子,在Activity
中創建了一個函數用來Toast
一段信息:
fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {Toast.makeText(this, message, length).show()
}
上面代碼中第二個參數length
指定了一個默認值。這意味著你調用的時候可以傳入第二個值或者不傳,這樣可以避免你需要的重載函數:
toast("Hello")
toast("Hello", Toast.LENGTH_LONG)
無參主函數
如果你使用的是Kotlin1.2或更早的版本,若想正常運行程序,你的主函數必須寫成如下形式:
fun main(args: Array<String>) {// ...
}
從Kotlin1.3版本起,你可以忽略main函數的參數,寫成如下形式:
fun main() {// ...
}
可變長參數函數:使用vararg
關鍵字
fun vars(vararg v: Int){for(vt in v){print(vt)}
}// 測試
fun main(args: Array<String>) {vars(1,2,3,4,5) // 輸出12345
}
如果你有一個現有的值數組,則可以通過在數組名前加上*
來將這些值傳遞給該函數。星號(*)
被稱為擴展運算符,以下是它的一些使用示例:
val myArray = arrayOf(1, 2, 3, 4, 5)
val mList = vars(*myArray)
val mList2 = vars(0, *myArray, 6, 7)
在Kotlin中,方法調用也被定義為二元操作運算符,而這些方法往往可以轉化為invoke函數。
例如 a(i)方法的對應轉換方法為 a.invoke(i)
命名風格
如果拿不準的時候,默認使用Java
的編碼規范,比如:
- 使用駝峰法命名(并避免命名含有下劃線)
- 類型名以大寫字母開頭
- 方法和屬性以小寫字母開頭
- 使用4個空格縮進
- 公有函數應撰寫函數文檔,這樣這些文檔才會出現在
Kotlin Doc
中
類布局
通常,一個類的內容按以下順序排列:
- 屬性聲明與初始化塊
- 次構造函數
- 方法聲明
- 伴生對象
不要按字母順序或者可見性對方法聲明排序,也不要將常規方法與擴展方法分開。而是要把相關的東西放在一起,這樣從上到下閱讀類的人就能夠跟進所發生事情的
邏輯。選擇一個順序(高級別優先,或者相反)并堅持下去。
將嵌套類放在緊挨使用這些類的代碼之后。如果打算在外部使用嵌套類,而且類中并沒有引用這些類,那么把它們放到末尾,在伴生對象之后。
接口實現布局
在實現一個接口時,實現成員的順序應該與該接口的成員順序相同(如果需要,還要插入用于實現的額外的私有方法)
重載布局
在類中總是將重載放在一起。
冒號
類型和超類型之間的冒號前要有一個空格,而實例和類型之間的冒號前不要有空格:
interface Foo<out T : Any> : Bar {fun foo(a: Int): T
}
類頭格式化
有少數幾個參數的類可以寫成一行:
class Person(id: Int, name: String)
具有較長類頭的類應該格式化,以使每個主構造函數參數位于帶有縮進的單獨一行中。此外,右括號應該另起一行。如果我們使用繼承,
那么超類構造函數調用或者實現接口列表應位于與括號相同的行上:
class Person(id: Int, name: String,surname: String
) : Human(id, name) {// ……
}
對于多個接口,應首先放置超類構造函數調用,然后每個接口應位于不同的行中:
class Person(id: Int, name: String,surname: String
) : Human(id, name),KotlinMaker {// ……
}