首發于公眾號: DSGtalk1989
5.Kotlin 類和對象
-
構造器
kotlin中一個類只能有一個主構造器和一個或多個次構造器。主構造器可以直接跟在
class
定義的類名后面但是沒有方法體,如下:class Person constructor(s : String) { } //也可以寫成這樣,記得,沒有空格 class Person(s : String){ } //一旦構造函數存在修飾符或者是注解的情況下,我們就不能省去constructor的方法名 class Child public constructor(s : String){ }
以上的都是主構造函數,次構造函數定義在類中,并且kotlin強制規定,次構造函數必須要調用主構造函數,如下:
class Person constructor(s : String) { constructor(i : Int) : this("123"){ } } //也可以沒有主構造器,如下的方式就是直接起的次構造器 //只有這種情況下,次構造器不需要再調用主構造器,所以一般如下這種方式跟我們的java習慣比較像 class Person{ constructor(){ } }
-
private,public,protected,internal
前面三個大家比較熟悉了,在java中都有,但是
internal
是kotlin中才引入的,叫做模塊內可見,即同一個module
中可見。我們分別來看下,用這四個修飾符來描述屬性所帶來的編譯區別。
//kt private var a = "a" public var b = "b" protected var c = "c" internal var d = "d" //decompiled private String a; @NotNull private String b; @NotNull private String c; @NotNull private String d; @NotNull public final String getB() { return this.b; } public final void setB(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.b = var1; } @NotNull protected final String getC() { return this.c; } protected final void setC(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.c = var1; } @NotNull public final String getD$app_debug() { return this.d; } public final void setD$app_debug(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.d = var1; }
總結一下,就是不管是哪一個修飾符,最終經過編譯之后生成的在java中的參數描述都是
private
的,修飾符真正造成的區別是在編譯了自后的get
和set
的方法不同。private
的話,就不會生成get
和set
方法,因為對于這個參數來說,是外部不可訪問的。public
和protected
就是相應的set
和get
。而internal
則是public
的setD$app_debug
和getD$app_debug
方法。我們可以認為這兩個方法,在model
中都是可以被訪問的。 -
init關鍵字
上面說到主構造器直接寫在類名之后是沒有方法體的,因此一旦我們想要在構造函數中做一些初始化的操作,就需要挪到init中實現了。
class Person constructor(firstName: String) { init { println("FirstName is $firstName") } }
-
getter和setter
跟java差的有點多,首先屬性定義前面說過了,kotlin中getter和setter直接定義在屬性下方,由于kotlin的本身屬性的直接訪問性,只要你創建的是public的屬性,都可以直接獲取到屬性值即get方法和修改屬性值即set方法。
所以免去了為了
fastjson
而專門去寫的setter
和getter
,但是一旦你需要在getter
和setter
時做一些其他的操作,我們就需要去顯示的寫出get
和set
了var sex = "boy" get() { return "girl" } set(value) { when { value.contains("girl") -> field = "boy" } } var age = 16 get() = field + 10 private set(value) = action(value) fun action(int: Int) { }
get
和set
可以直接包含方法體,也可以直接通過等號的方式鏈到單行表達式或者方法。
即我們認為,一旦觸發取值和賦值的時候會做相應的操作。其中field
就是指的他自己。>>注:field的重要性<<
在kotlin中,我們定義了一個參數
name
然后,只要去調用他,比如parent.name
或者說去對他進行賦值name = "Tony"
最終都會被編譯成使用了get
和set
方法,如下//Observer.kt fun main(args : Array<String>){ var observer = Observer() observer.name = "Tony" var newName = observer.name } //Observer.decompiled.java public static final void main(@NotNull String[] args) { Intrinsics.checkParameterIsNotNull(args, "args"); Observer observer = new Observer(); //調用了set observer.setName("Tony"); //調用了get String newName = observer.getName(); }
所以我們在類中定義屬性的
setter
和getter
的時候,如果直接操作屬性本身,就會出現死循環。這就是field
的用途//kt var no: Int = 100 get() = no set(value) { if (value < 10) { // 如果傳入的值小于 10 返回該值 no = value } else { no = -1 // 如果傳入的值大于等于 10 返回 -1 } } //decompiled int no = 100; public int getNo() { return getNo();// Kotlin中的get() = no語句中出來了變量no,直接被編譯器理解成“調用getter方法” } public void setNo(int value) { if (value < 10) { setNo(value);// Kotlin中出現“no =”這樣的字樣,直接被編譯器理解成“這里要調用setter方法” } else { setNo(-1);// 在setter方法中調用setter方法,這是不正確的 } }
很顯然,造成了死循環。
-
lateinit關鍵字
我們都知道kotlin中,在方法中定義屬性時,我們必須進行初始化的操作。而類中本身我們一開始并不知道他到底是什么,所以會有希望晚一點再初始化的需求,這里就可以使用
lateinit
關鍵字來描述,那我們就可以不用給出具體的初始化值,但是kotlin會要求你必須給出屬性的類型。lateinit var game : String
那么這個
game
我們可以之后再對其賦值。從kotlin 1.2開始已經支持全局和局部變量都是用lateinit, 并且我們可以通過isInitialized來判斷是否已經初始化過
-
抽象類
我們默認定義的
class
都是final
的,無法被繼承的。所以一旦需要這個class
能夠被繼承,我們需要加上open
關鍵字。如果這是個抽象類,那我們需要添加abstract
關鍵字。一旦被abstract
描述,就無需再加上open
了。open class Person(){ } abstract class Parent{ }
緊接著,另外幾種場景
-
方法是否可以被重寫
默認方法都是
final
的,如果需要讓方法可以被重寫,需要在方法前再加上open
所有我們平時在java中寫的一個單純的類實際上轉換成kotlin是如下這個樣子的:
open class Person constructor(s: String) { open fun getName(){} }
同樣的
abstract
也是這個意思,加載class
前面只是形容類,跟方法和屬性什么的一點關系都沒有 -
抽象屬性
這是一個比較新的東西,因為java不支持抽象屬性。就是說,你要是繼承我,你就必須要初始化我所要求初始化的屬性。
abstract class Parent(ame : String){ abstract var ame : String } //兩種集成方式,一種是直接在構造函數中對抽象屬性進行復寫 //由于父類構造需要傳一個字符串,所以在繼承時也需要直接傳入,此處傳入的是Child1自己的構造參數s class Child1 constructor(s: String, override var ame: String) : Parent(s) { } //一種是在類中對屬性進行復寫 class Child2 constructor(s: String) : Parent(s) { override lateinit var ame: String } //如果子類沒有主構造函數,也可以通過次構造函數調用`super`方法實現 class Child: Parent { constructor() : super("s") override lateinit var ame: String }
-
-
嵌套類
直接在
class
內部定義class
,基本和java差不多class Outer { // 外部類 private val bar: Int = 1 class Nested { // 嵌套類 fun foo() = 2 } } fun main(args: Array<String>) { val demo = Outer.Nested().foo() // 調用格式:外部類.嵌套類.嵌套類方法/屬性 println(demo) // == 2 }
-
內部類
在剛才嵌套類的基礎上加上
inner
的關鍵字申明。class Outer { private val bar: Int = 1 var v = "成員屬性" /**嵌套內部類**/ inner class Inner { fun foo() = bar // 訪問外部類成員 fun innerTest() { var o = this@Outer //獲取外部類的成員變量 println("內部類可以引用外部類的成員,例如:" + o.v) } } }
唯一的區別在于內部類持有了外部類的引用,可以通過
@外部類名
的方式,來訪問外部類的成員變量。 -
內部類和嵌套類的區別
我們來看如下兩個嵌套類和內部類的以及讓門各自編譯成class文件的例子
//Out.kt class Out{ class Inner{ } } //Out.decompiled.java public final class Out { public static final class Inner { } } //Out.kt class Out{ inner class Inner{ } } //Out.decompiled.java public final class Out { public final class Inner { } }
已經很明顯了,
inner
之所以持有外部的引用,是因為他不是static
的。也就是說kotlin的class
默認就是static final
的。調用嵌套類的方式與調用內部類的方式差別也只是一個括號而已
fun main(args : Array<String>){ //內部類調用 Out().Inner().method() //嵌套類的調用 Out1.Inner().method() }
其實比較容易理解,嵌套類是
static
的直接可以通過類名來進行訪問嵌套類。 -
匿名內部類
一般用在接口層面的很多,我們通常知道的是傳參是個接口,方法中調用了接口方法的形式,如下:
class Observer{ fun getIt(listener: Listener){ listener.onClick() } } interface Listener{ fun onClick() } fun main(args : Array<String>){ var observer = Observer() //注意,此處的object是kotlin獨有的關鍵字 //不是隨便寫寫的,匿名內部類必須通過這個關鍵字來申明 observer.getIt(object : Listener{ override fun onClick() { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } }) }
通常我們也可以直接使用
接口名
+lambda表達式
的方式來生成匿名內部類,但條件是這個接口必須是函數式java接口,即只有一個抽象方法的java文件中定義的接口。比如我們基本碰到的所有的什么
OnclickListener
等等tv_case_id.setOnClickListener { View.OnClickListener{ } }
不過kotlin中定義的接口,我們就必須通過
object
的方式去實現了同時匿名內部類我們可以單獨的拿出來進行定義,實際上我們可以把
object :
理解成一個匿名的內部類實現了一個接口,也就是說我們還可以實現多個接口,比如:open class A(x: Int) { public open val y: Int = x } interface B { …… } val ab: A = object : A(1), B { override val y = 15 }
通常我們在java中是無法做到匿名內部類實現多個接口的,因為我們只能
new
一個接口出來。
更甚者說,我們很多時候甚至不需要這個object
去實現或者是繼承什么,我們可以直接搞一個object
出來
fun foo() {
val adHoc = object {
var x: Int = 0
var y: Int = 0
}
print(adHoc.x + adHoc.y)
}
-
匿名對象最為函數的返回類型
我們上面是將匿名對象賦值給了對象,我們還可以吧匿名對象直接賦值給方法,比如下面這個樣子。
fun publicFoo() = object { val x: String = "x" }
這里涉及到公有還是私有的問題。
匿名對象我們一般只能用在私有域和本地。白話的說就是一旦變成了公有,那就說誰都可以去調用,由于匿名對象只在生命的本地和私有域起作用,導致公有調用拿到的對象只能是匿名對象的超類(即父類,比如上面的
object : Listener
就是Listener
,如果沒有顯式的定義超類就是Any
)那么這樣一來,就會導致匿名內部類中定義的屬性是拿不到的,比如上面的x
,因為上面的object
并沒有顯式的定義超類,所以他返回的是Any
,而Any
是沒有x
屬性的. -
匿名對象訪問變量
在java中匿名內部類想要訪問相應的屬性變量必須要
final
才行,但是在kotlin中,我們直接可以訪問包含匿名對象作用域中的所有變量。fun countClicks(window: JComponent) { var clickCount = 0 var enterCount = 0 window.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { clickCount++ } override fun mouseEntered(e: MouseEvent) { enterCount++ } }) }
Kotlin學習筆記之 13 基礎操作符run、with、let、also、apply