物件導向是一門很難懂的理論,而kotlin的類別又比 C++/C#/Java更為複雜,所以更花更多時間慢慢理解。
自訂資料型態
類別就是自訂資料型態。如下代碼新增一個Student的類別,裏面有三個物件變數,分別為name,chinese, eng。
在main主程式裏,只要調用 var s1=Student() 就可產生一名學生物件s1。s1包含了name, chinese及eng三個變數,使用 “.” 可進行存取,如 s1.name。
fun main(args:Array<String>){
var s1=Student()
s1.name="Thomas"
s1.chinese=100
s1.eng=95
println("s1 姓名:${s1.name}, 國文:${s1.chinese}, 英文:${s1.eng}")
}
class Student{
var name:String=""
var chinese:Int=0
var eng:Int=10
}
類別與物件
類別是以class開頭定義,如
class Student{}
而物件是以如下代碼產生
var s1=Student()
var s2=Student()
上述只有一個Student類別,但卻可以產生s1, s2二個物件。底下程式,可以產生多達上千萬個物件
fun main(args:Array<String>){
var student= arrayOfNulls<Student>(1000)
for (i in student.indices){
student[i]=Student()
}
for(i in student.indices){
var s=student[i]
println("s[${i}], 姓名=${s?.name}, 國文=${s?.chinese}, 英文=${s?.eng}")
}
}
class Student{
var name:String="abcd"
var chinese:Int=0
var eng:Int=10
}
物件變數
物件變數,是附著於物件上的變數。
新增一個 Pokemon 的類別,然後在主程式裏產生一隻Pokemon物件
fun main(args:Array<String>){
var p1=Pokemon()
print("p1的等級為:${p1.level}, 速度:${p1.speed}")
}
class Pokemon{
var level:Int
var speed:Double
init{
this.level=1
this.speed=100.0
println("神奇寶貝出生了 - init")
}
}
結果 :
神奇寶貝出生了 - init
p1的等級為 : 1, 速度 : 100.0
上述class中的var level:Int稱為物件變數。物件變數一定要手動初始化,如果不初始化,就要在init{}裏面初始化。這跟Java會自動初始化物件變數的觀念完全不一樣。
學過Java的人大都會認為init{}就是Java的建構子。但ini{}並不是建構子,它只有一個特性~~產生物件並執行過主建構子後,才會自動執行init
主建構子
主建構子的目的,只有在主程式產生物件時,作二件事
1. 接收區域變數
2. 產生物件變數
先看一下如下代碼,等會再來說明
fun main(args:Array<String>){
var p1=Pokemon(1, 2, 3)
println("p1 沒有 a這個物件變數")
print("p1只有二個物件變數, p1.b=${p1.b}, p1.c=${p1.c}")
}
class Pokemon constructor(a:Int/*區域變數*/, var b:Int/*物件變數*/, val c:Int/*物件變數*/){
init{
println("a 是區域變數 $a")
println("b 是物件變數 $b")
println("c 是物件變數 $c")
}
}
在class Pokemon後面,緊接著 constructor(a:Int, var b:Int, val c:Int),稱為主建構子。是產生物件時,第一個會被執行的地方。其中有加 var 或 val 的變數,就是物件變數,而沒有加修飾子的變數 a,就是區域變數。所以在main中,只有p1.b及p1.b, 但沒有p1.a
每次宣告類別都要加上constructor()實在是很麻煩,可以將constructor拿掉,精簡成如下
class Pokemon(a:Int, var b:Int, var c:Int){
}
初始化函數
主建構子接收參數並產生物件變數後,接下來就會自動執行初始化函數 init{}。老實說 init{}稱為函數還真的不習慣,因為函數不是應該為 init(){}嗎,而事實上確少了 “()”。沒辦法啊,因為init不接受任何參數,所以乾脆把 “()” 給省了。
次建構子
次建構子是寫於類別之內的一個比較正常的方法。為什麼叫 “比較正常” 呢? 因為它是一個方法,但不能有返回值,裏面也不能有return。請注意如下二點
1. 如果只有次建構子,但沒有主建構子,還是會先執行init初始化函數,再執行次建構子。
fun main(args:Array<String>){
var p1=Pokemon()
}
open class Pokemon{
var level:Int
init{
println("Initial")
level=1
}
constructor(){
println("神奇寶貝出生了")
}
}
結果:
Initial
神奇寶貝出生了
2. 如果主建構子與次建構子同時存在的話,那次建構子一定要接 :this() 。產生物件時若由次建構子進入,因為接 :this(),所以會先去執行主建構子,再執行init, 然後才執行次建構子區塊裏面的程式碼
fun main(args:Array<String>){
println("p1 沒有 a這個物件變數")
print("p1只有二個物件變數, p1.b=${p1.b}, p1.c=${p1.c}")
}
class Pokemon constructor(a:Int, var b:Int, val c:Int){
init{
println("a 是區域變數 $a")
println("b 是物件變數 $b")
println("c 是物件變數 $c")
}
constructor(a:Int, b:Int):this(a, b, 100){
println("\n現在執行次建構子\n")
}
}
結果
a 是區域變數 1
b 是物件變數 2
c 是物件變數 100
現在執行次建構子
p1 沒有 a這個物件變數
p1只有二個物件變數, p1.b=2, p1.c=100
Process finished with exit code 0
執行順序
產生物件的整個流程為
1. 由次建構子進入 : 主建構子=>init=>次建構子
2. 由主建構子進入 : 主建構子=>init。此時當然就沒次構子表現的機會了
無參數之主建構子
主建構子的目的,是在建立物件變數或接收區域變數。所以如果主建構子無需任何參數的話,就不要加主建構子,因為這是多此一舉的。
fun main(args:Array<String>){
var p1=Pokemon()
}
class Pokemonconstructor(){
init{
println("初始化")
}
}
次建構子重載(Overload)
次建構子可以重載, 也可以在某一個次構子加入 :this() 去調用其他次建構子。但不論如何,都會先執行init,然後才會執行次建構子。
fun main(args:Array<String>){
var p1=Pokemon(10)
}
class Pokemon{
var level:Int
init{
println("初始化")
level=1
}
constructor(){
println("次建構子之預設建構子")
}
constructor(a:Int):this(){
println("次建構子之自訂建構子")
}
}
結果:
初始化
次建構子之預設建構子
次建構子之自訂建構子
類別內物件變數
物件變數可以在主建構子內宣告,也可以在類別內宣告。物件變數不會自動初始化,所以要在宣告後就初始化,或是在init{}內初始化。
class Pokemon constructor(a:Int, var b:Int, val c:Int){
var level:Int
var speed:Double
init{
println("a 是區域變數 $a")
println("b 是物件變數 $b")
println("c 是物件變數 $c")
level=1
speed=100.0
}
constructor(a:Int, b:Int):this(a, b, 100){
println("\n現在執行次建構子\n")
}
}
產生物件
產生物件時, 在Java中需使用new 關鍵字,但kotlin跟Python一樣 , 直接呼叫類別名稱即可。
var p1=Pokemon()