Kotlin 的類和接口在概念上跟 Java 是同樣的,可是用法存在一些差異,好比繼承的寫法、構造函數和可見性修飾符的不一樣等,此外還有一些 Java 中沒有的概念,如數據類、密封類、委託和 object 關鍵字等。下面從類和接口的定義開始,感覺一下 Kotlin 的非凡之處吧!程序員
跟 Java 同樣,Kotlin 使用 class 關鍵字來定義一個類。express
class Animal {
fun eat() {
...
}
fun move() {
...
}
}
複製代碼
在 Java 中,一個類除了被手動加上 final 關鍵字,它都能被任意一個類繼承並重寫它的非 final 方法,這就可能會致使某些子類出現不符合其父類的設計初衷,特別是在多人協做的開發環境下。編程
這類問題被 Kotlin 語言設計者注意到了並切引發了他們的重視,所以,在 Kotlin 中的類和方法默認都是 final 的,若是要繼承或者重寫一個類和方法,必須將他們顯式地聲明爲 open。安全
open class Animal {
fun eat() {
...
}
open fun move() {
...
}
}
複製代碼
繼承該類的時候,須要在類名後面加上冒號後再寫被繼承的類名,在 Kotlin 中使用冒號代替了 Java 中的 extend
關鍵字。編程語言
class Dog : Animal {
override fun move() {
...
}
}
複製代碼
一樣,Kotlin 中可使用 abstract 關鍵字將一個類聲明稱抽象類,但它不能被實例化。抽象方法也能夠覆蓋父類的 open 方法,抽象方法始終是 open 的且必須被子類實現。ide
abstract class Bird : Animal {
abstract fun song()
override abstract fun move()
}
複製代碼
Kotlin 中的接口一樣使用 interface 關鍵字來定義,能夠在方法中加入默認的方法體。函數
interface Machine {
fun component()
fun control() {
...
}
}
複製代碼
與類的繼承相似,實現/繼承接口方法是在類名/接口名後面加上冒號再寫被實現/繼承的接口名。ui
實現接口:this
class Electric : Machine {
override fun component() {
...
}
override fun control() {
...
}
}
複製代碼
繼承接口:spa
interface Computer : Machine {
fun IODevice()
}
複製代碼
可見性修飾符用於聲明一個類或者接口的可見範圍,相似於 Java,Kotlin 中使用 public、private 和 protected 關鍵字做爲可見性修飾符。跟 Java 不一樣的是,Kotlin 默認的可見性是 public 的,而且沒有 「包私有」 的概念,同時新增 internal 關鍵字用來表示 「模塊內部可見「。
public 是 kotlin 默認的可見性修飾符,表示任何地方可見。
使用 internal 修飾符表示只在模塊內部可見。
一個模塊就是一組一塊兒編譯的 Kotlin 文件。這有多是一個 Intellij IDEA 模塊、一個 Eclipse 項目、一個 Maven 或 Gradle 項目或者一組使用調用 Ant 任務進行編譯的文件。
Kotlin 將構造方法分爲了主構造方法和次構造方法,主構造方法做爲類頭在類體外部聲明,次構造方法在類體內部聲明。
主構造方法做爲類頭的一部分存在,它跟在類名(與可選的類型參數)後。
class Animal constructor(name: String) {
...
}
複製代碼
若是主構造方法沒有可見性修飾符或者註解,則能夠省略 constructor 關鍵字。
class Animal(name: String) {
...
}
複製代碼
主構造方法中不能包含其它任何代碼,所以,若是須要初始化代碼,則須要把這一部分代碼放在以 init 關鍵字做爲前綴的**初始化塊(initializer blocks)**中。
class Animal(name: String) {
init {
val outPutName = "It's the Animal call: $name"
}
}
複製代碼
若是類中的某個屬性使用主構造方法的參數來進行初始化,能夠經過使用 val 關鍵字對主構造函數的參數進行修飾,以簡化代碼。
class Animal(val name: String) {
init {
val outPutName = "It's the Animal call: $name"
}
fun showName() {
println(name)
}
}
複製代碼
次構造方法在類體內使用 constructor 關鍵字進行定義,若是類有一個主構造方法,每一個次構造方法須要委託給主構造方法, 能夠直接委託或者經過其它次構造方法間接委託。委託到同一個類的另外一個構造方法用 this 關鍵字便可。
class Animal(val name: String) {
init {
val outPutName = "It's the Animal call: $name"
}
// 直接委託給主構造方法
constructor(name: String, age: Int): this(name) {
...
}
// 經過上面的構造方法間接委託給主構造方法
constructor(name: String, age: Int, type: Int): this(name, age) {
...
}
}
複製代碼
初始化塊中的代碼實際上會成爲主構造函數的一部分。委託給主構造函數會做爲次構造函數的第一條語句,所以全部初始化塊中的代碼都會在次構造函數體以前執行。即便該類沒有主構造函數,這種委託仍會隱式發生,而且仍會執行初始化塊。
在一個類內部定義的另一個類默認爲嵌套類。
class OuterClz {
var num = 1
class NestedClz {
fun show() {
// 編譯報錯
println(num)
}
}
}
複製代碼
嵌套類不持有它所在外部類的引用。
在 class 關鍵字前面加上 inner 關鍵字則定義了一個內部類。
class OuterClz {
var num = 1
inner class InnerClz {
fun show() {
// 編譯經過
println(num)
}
}
}
複製代碼
內部類持有了它所在外部類的引用,所以能夠訪問外部類的成員。
在 Java 中靜態內部類是不會持有外部類引用的,至關於 Kotlin 的嵌套類;而非靜態內部類則持有外部類的引用,至關於 Kotlin 的內部類。
有時候爲了類型安全,須要將某個屬性全部可能的值枚舉出來,開發者只能使用該枚舉類中定義的枚舉常量。
定義一個枚舉類須要用到 enum 和 class 關鍵字。
enum class Color {
RED, GREEN, BLUE
}
複製代碼
與 Java 相同,枚舉類中能夠聲明屬性和方法。
enum class Color(val r: Int, val g: Int, val b: Int) {
RED(255, 0, 0),
GREEN(0, 255, 0),
BLUE(0, 0, 255);
fun rgb () = Integer.toHexString((r * 256 + g) * 256 + b)
}
複製代碼
當聲明枚舉常量的時候,須要提供該常量所需的屬性值,而且須要在聲明完成後加上分號。
Kotlin 中的 when 可使用任何對象,所以,可使用 when 表達式來判斷枚舉類型。
var myColor = Color.RED
when (myColor) {
Color.RED -> println("It's a red color")
Color.GREEN -> println("It's a green color")
Color.BLUE -> println("It's a blue color")
}
複製代碼
須要注意的是,若是在 when 表達式中沒有 case 到全部的枚舉常量,編譯器並不會報錯。
var myColor = Color.RED
when (myColor) {
Color.RED -> println("It's a red color")
Color.GREEN -> println("It's a green color")
}
複製代碼
可是會建議你處理全部可能的狀況(添加 「BLUE」 分支或者 「else」 分支)。
'when' expression on enum is recommended to be exhaustive, add 'BLUE' branch or 'else' branch instead
若是一個父類 Animal 只有兩個分別是 Bird 和 Dog 的子類,在 when 表達式中處理全部狀況的時候若是代碼寫成以下:
open class Animal {
fun doSomething() {
}
}
class Bird(val name: String) : Animal()
class Dog(val name: String) : Animal()
fun main() {
fun showName(animal: Animal) =
when (animal) {
is Dog -> println("It's the dog name ${animal.name}")
is Bird -> println("It's the bird name ${animal.name}")
}
}
複製代碼
這時編譯器會提示錯誤:必須加上 else
分組。
這時候,若是想寫出簡潔的代碼,密封類就派上用場了。
密封類用來表示受限的類繼承結構:當一個值爲有限集中的類型、而不能有任何其餘類型時。在某種意義上,他們是枚舉類的擴展:枚舉類型的值集合也是受限的,但每一個枚舉常量只存在一個實例,而密封類的一個子類能夠有可包含狀態的多個實例。
密封類的定義須要在類名前面加上 sealed 關鍵字。
sealed class Animal
複製代碼
使用密封類的時候須要注意幾點:
使父類變成密封類以後,意味着對可能建立的子類作出了限制,when 表達式中處理了全部 Animal 類的子類的狀況,所以不須要 「else」 分支。
sealed class Animal {
fun doSomething() {
}
}
class Bird(val name: String) : Animal()
class Dog(val name: String) : Animal()
fun main() {
fun showName(animal: Animal) =
when (animal) {
is Dog -> println("It's the dog name ${animal.name}")
is Bird -> println("It's the bird name ${animal.name}")
}
}
複製代碼
當你爲 Animal 類添加一個新的子類且沒有修改 when 表達式內容的時候的時候,IDE 會編譯報錯提醒你沒有覆蓋全部狀況。
使用 Java 的時候,難免會須要一些 Entity 類來承載數據,有時候還須要重寫 toString、equals 或者 hashCode 方法,而這些方法的寫法千遍一概,有些 IDE 還可以自動生成這些方法。可是 Kotlin 中的數據類可以很好地避免這些狀況,使代碼看起來更加簡潔。
數據類的定義須要在類名前面加上 data 關鍵字。
data class User(val name: String, val gender: Int, val age: Int)
複製代碼
數據類的定義必須保證如下條件:
數據類定義完成以後,編譯器自動從主構造函數中聲明的全部屬性導出如下成員:
equals() 和 hashCode() 方法:
一般用於對象實例之間的比較。
toString() 方法:
fun main() {
var user = User("guanpj", 1, 18)
println(user.toString())
}
複製代碼
輸出結果爲:User(name=guanpj, gender=1, age=18)
componentN 函數:
componentN 稱爲解構函數,簡單來講就是把一個對象解構成多個變量以便使用。在 data 類中若是有 N 個變量,則編譯器會生成 N 個解構函數(component一、component2 ... componentN)按順序對應這 N 個變量。使用方法以下:
fun main() {
var user = User("guanpj", 1, 18)
var (name, gender, age) = user
println("My name is $name and I'm $age years old.")
}
複製代碼
輸出結果:My name is guanpj and I'm 18 years old.
須要注意的是,data 類中的這些 componentN 函數不容許提供顯式實現。
copy() 函數:
使用 copy() 函數可以生成一個與該對象具備相同屬性的對象,而且能夠修改部分屬性。
fun main() {
var user = User("guanpj", 1, 18)
var newUser = user.copy("gpj")
println("My name is ${newUser.name} and I'm ${newUser.age} years old.")
}
複製代碼
輸出結果爲:My name is gpj and I'm 18 years old.
一樣,copy() 函數也不容許提供顯式實現。
雖然 」委託「 這個設計思想在各個編程語言中都或多或少地有所體現,可是 Kotlin 直接在語法上對 委託模式作了支持,Kotlin 支持類層面的委託和屬性的委託,下面分別從這個兩個方面講解委託模式在 Kotlin 中的使用。
前面已經提到過,Kotlin 在設計之初就考慮到因繼承帶來的 「脆弱的基類」 問題,所以把類默認視做 final 類型的,當須要擴展某些類的時候,手動將它們標記成 open 而且在擴展的過程當中注意兼容性。
假設你有一個需求,須要統計一個集合添加元素的次數,你使用一個 CountingSet 來實現 MutableCollection 接口,並擴展了 add() 和 addAll() 方法,其餘方法直接交給成員變量 innerSet 來處理。
class CountingSet<T>() : MutableCollection<T> {
val innerSet: MutableCollection<T> = HashSet<T>()
var objectsAdded = 0
override fun add(element: T) : Boolean {
objectsAdded++
return innerSet.add(element)
}
override fun addAll(c: Collection<T>): Boolean {
objectsAdded += c.size
return innerSet.addAll(c)
}
override val size: Int
get() = innerSet.size
override fun contains(element: T): Boolean = innerSet.contains(element)
override fun containsAll(elements: Collection<T>): Boolean = innerSet.containsAll(elements)
override fun isEmpty(): Boolean = innerSet.isEmpty()
override fun clear() = innerSet.clear()
override fun iterator(): MutableIterator<T> = innerSet.iterator()
override fun remove(element: T): Boolean = innerSet.remove(element)
override fun removeAll(elements: Collection<T>): Boolean = innerSet.removeAll(elements)
override fun retainAll(elements: Collection<T>): Boolean = innerSet.retainAll(elements)
}
複製代碼
能夠看到,除了 add() 和 addAll() 方法,其餘全部的方法都須要重寫並交給 innerSet 去實現,這樣勢必會產生比較多的模板代碼,使用類委託即可以很好地解決這個問題。
class CountingSet<T>(val innerSet: MutableCollection<T> = HashSet<T>())
: MutableCollection<T> by innerSet {
var objectsAdded = 0
override fun add(element: T) : Boolean {
objectsAdded++
return innerSet.add(element)
}
override fun addAll(c: Collection<T>): Boolean {
objectsAdded += c.size
return innerSet.addAll(c)
}
}
複製代碼
經過在超類型列表中使用 by 關鍵字進行委託,編譯器將會生成轉發給 innerSet 的全部 MutableCollection 中的方法,若是有覆蓋方法,編譯器將使用覆蓋的方法而不是委託對象中的方法。
一樣,一個屬性也能夠將它的訪問器邏輯委託給一個輔助對象,屬性委託的寫法爲:
val/var <屬性名>: <類型> by <表達式>
代碼表示以下:
class MyClz {
var p: String by Delegate("abc")
}
複製代碼
對於 val 和 var 類型的屬性來講,它的 get()(和 set())方法將會被委託給 Delegate 類的 getter()(和 setter)方法。
class Delegate<T>(default: T) {
private var value = default
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
}
複製代碼
使用屬性委託須要注意:
KProperty<*>
或其超類型。另外,Kotlin 提供了 ReadOnlyProperty 和 ReadWriteProperty 接口以方便實現 val 和 var 屬性的委託,只需實現這兩個接口並重寫它的方法便可。
class Delegate<T>(default: T) : ReadWriteProperty<Any?, T> {
private var value = default
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
}
複製代碼
Kotlin 中的 object 被視爲對象,可是跟面向對象中的 」對象「 卻不太同樣,它的功能很是強大,它能夠定義一個單例、實現相似 Java 中的靜態方法的功能以及建立匿名內部類,Kotlin 中的 object 是擁有某個具體狀態的實例
,出事化後不會再改變。
應該不少人跟我同樣,剛從 Java 轉過 Kotlin 的時候都會遇到一個疑惑:Kotlin 中是怎樣定義單例的?事實上,經過對象聲明,在 Kotlin 中定義單例簡直易如反掌,經過 object 關鍵字,能夠定義一個對象聲明。
object DataManager {
val data: Set<Person> = setOf()
fun doSomething() {
for (person in data) {
...
}
}
}
複製代碼
與變量同樣,對象聲明容許你使用對象名加.字符的方式來調用方法和訪問屬性:
fun main() {
DataManager.data
DataManager.doSomething()
}
複製代碼
對象聲明具備如下特色:
基於以上幾點,對象聲明徹底符合單例模式的要求。
一樣的,從其餘語言轉到 Kotlin 的程序員可能會遇到另一個問題 —— Kotlin 中怎樣在一個類中定義一個靜態方法?在 Kotlin 中,若是想要直接經過容器類名稱來訪問這個對象的方法和屬性的能力,再也不須要顯式地指明對象的名稱,就須要使用到伴生對象的概念了,伴生對象的定義以下:
class MyClz {
companion object {
var myVariable = "My variable"
fun doSomething() {
...
}
}
}
複製代碼
如今就能夠像 Java 中調用靜態變量和靜態方法的方式同樣調用 myVariable 變量和 doSomething() 方法了!
fun main() {
MyClz.myVariable
MyClz.doSomething()
}
複製代碼
與對象聲明 同樣,伴生對象也能夠實現接口。
interface MyInterface {
fun doSomething()
}
class MyClz {
companion object : MyInterface {
override fun doSomething() {
...
}
}
}
fun main() {
// MyClz 類的名字能夠被看成 MyInterface 實例
var myInstant: MyInterface = MyClz
myInstant.doSomething()
}
複製代碼
object 關鍵字不只僅能用來聲明單例模式的對象,還能用來聲明匿名對象,與 Java 匿名內部類只能擴展一個類或實現一個接口不一樣 , Kotlin 的匿名對象能夠實現多個接口或者不實現接口,咱們稱之爲對象表達式。
interface MyInterface {
fun doSomething()
fun doOtherthing()
}
val myInstant = object : MyInterface {
override fun doSomething() {
...
}
override fun doOtherthing() {
...
}
}
複製代碼
另一個跟 Java 不一樣的點就是,對象表達式中能夠直接訪問建立它的函數中的非 final 變量。
class MyClz {
var mVariable = "This is my variable."
val myInstant = object : MyInterface {
override fun doSomething() {
println(mVariable)
}
override fun doOtherthing() {
}
}
}
複製代碼
使用對象聲明能夠定義一個單例類;
使用伴生對象能夠實現相似 Java 中調用靜態變量和靜態方法的功能;
對象聲明和伴生對象均可以實現接口;
對象表達式能夠做爲 Java 中匿名內部類的替代品,而且使用起來更加方便。
對象聲明是在第一次被訪問到時延遲初始化的,以後訪問不會被初始化、對象表達式是在每次使用到的時候當即初始化並執行的,每次都會建立一個新的對象、伴生對象是在相應的類被加載(解析)的時候初始化的。