【Kotlin】第7課-類別的設計(二)

在第6課,學到了如何設計自己的類別,包含定義建構式、屬性與方法,並建立物件來操作。本文會講解存取權限的設置,讓類別中的這些成員只能在允許的地方被存取。

接著介紹初始化函數,讓物件建立後還能進行額外的工作。最後提供一個情境,讓讀者練習類別的設計,並接觸更多第5課所學到的集合的方法。

一、存取權限

學習 Java 時曾經認識 public 與 private 等存取權限,它決定了類別、方法與屬性能夠被使用的範圍。

在 Kotlin 也有相同的關鍵字可以控制,其中 public、protected 與 private 的效果與 Java 是一樣的。不同的地方在於 Kotlin 沒有「package private」的概念,且預設的存取權限是 public。以下示範使用 private。

class Student constructor(val id: String,
firstName: String,
lastName: String) {

var fullName = "$firstName $lastName"
private set

private fun getDetail() = "Id: $id, Full name: $fullName"

fun printDetail() {
println(getDetail())
}
}

這邊將 fullName 的 set 方法設為 private,意義在於保護該屬性不被外部賦予值。此外,get 方法的權限固定與屬性相同,此處仍為 public。而 set 方法的存取權限不可大於 get 方法

至於 getDetail 方法已經設為 private,所以無法在主程式被呼叫。但同一類別中的 printDetail 方法則可以。

Kotlin 比 Java 多了一種存取權限,叫做「internal」,它的範圍涵蓋當前的模組。簡單的程式專案一般只有一個模組,因此我們看到的大多數範例中,internal 的效果常常與 public 相同,但實質範圍仍小於 public。


二、初始化函數

如果想要在主要建構式每次產生物件後,都能執行額外的程式,可以利用「初始化函數」。其實它就跟第6課第三節介紹的次要建構式,結束後能執行額外程式的道理相同。

class Course constructor(id: String,
name: String,
credit: Int) {
val id: String
var name: String
var credit: Int

init {
this.id = id
this.name = if (name.isEmpty()) "(Not provided)" else name
this.credit = if (credit < 0) 0 else credit
println("Main constructor finished.")
}

constructor(id: String, name: String)
: this(id, name, 0) {
println("Sub constructor finished.")
}
}

使用「init {}」語法可以宣告初始化函數,它會在主要建構式結束後執行。在一個類別中可以有多個,執行順序等同宣告順序。至於前面提到次要建構式的額外程式,則會等初始化函數結束後才執行。

三、綜合應用

結合先前所學,以下提供一個練習,讀者可以嘗試用程式解決問題。下面給定了可選修課程的 Map,key 為課程編號,value 為整個課程資料。請模擬出學生在某學年度某學期進行選課,並印出選課清單的過程。

fun initCources(): Map<String, Course> {
val cources = listOf(
Course("IT001", "計算機概論", 3),
Course("IT002", "Java程式設計", 3),
Course("IT003", "資料庫系統應用", 3),
Course("MG001", "人力資源管理", 3),
Course("MG002", "管理資訊系統", 3),
Course("FN001", "中等會計學I", 4),
Course("FN001", "財務報表分析", 3)
)

return cources.map { it.id to it }.toMap()
}

實作內容:

  1. 請設計一個「選課紀錄」的新類別,裡面能儲存學生與選課資料。也可視需要,練習重新設計課程(Course)與學生(Student)的類別。
  2. 在選課紀錄類別中撰寫能產生該學期選課清單文字的方法,內容不拘。

讀者可參考下方筆者的做法。

class Course constructor(val id: String,
name: String,
credit: Int) {
var name = if (name.isEmpty()) "(Not provided)" else name
var credit = if (credit < 0) 0 else credit
}

class Student constructor(val id: String,
firstName: String,
lastName: String) {
var fullName = "$firstName $lastName"
}

課程類別的屬性包含編號、名稱與學分數。在建立物件時,若名稱給予空字串,則會修正為「(Not provided)」文字。若學分數給予小於0的數值,則會修正為0。學生類別的屬性包含編號與完整名字。在建立物件時,會將建構式參數中的姓與名組合成完整名字。

class CourceSelection(var year: Int,
var semester: Int,
var student: Student) {
var courses: MutableSet<Course> = mutableSetOf()

fun addCourse(vararg selectedCourse: Course?) {
val courses = selectedCourse.filterNotNull()
this.courses.addAll(courses.toList())
}

fun generateDetail(): String {
var detail = ""
var totalCredit = 0

detail += "${year}學年度第${semester}學期選課清單\n"
detail += "學生:${student.id} ${student.fullName}\n"

detail += "課程編號\t\t課程名稱\t\t學分數\n"
for (c in courses) {
detail += "${c.id}\t\t${c.name}\t\t${c.credit}\n"
totalCredit += c.credit
}
detail += "總學分:$totalCredit\n"

return detail
}
}

選課紀錄類別的屬性包含年度、學期、學生資料與課程清單。課程清單使用 Set 的原因是不應該有課程重複的情形,而 Set 正好具備元素不重複的特色。

選課紀錄提供 addCourse 方法讓外部傳入課程資料,它是可變長度參數。接著使用 filterNotNull 方法篩選出非 null 的元素。再使用 toList 方法將陣列轉為 List,存入課程清單。

該類別還提供 generateDetail 方法來產生選課清單的文字。輸出的內容包含大標題、學生資料、選課清單的標頭與內容。

以上三個類別準備好,就能回到 main 主程式實際使用。

fun main(args: Array<String>) {
// 取得可選修課程
val cources = initCources()

// 學生 Anna 進行選課
val student = Student("1056001", "Anna", "Su")
val selection = CourceSelection(107, 2, student)
selection.addCourse(
cources["IT003"], cources["MG001"], cources["MG002"],
cources["FN001"], cources["FN002"]
)

// 印出選課清單
println(selection.generateDetail())
}
首先取得題目給定的可選修課程 Map,再建立選課紀錄的物件。

接著用 Map 的 key 可以取出課程物件,再將課程添加到選課紀錄。由於 Map 在資料不存在時會回傳 null,因此 addCourse 方法才設計成允許接收 null 值再另行排除的做法,避免了 NullPointerException 例外。

選課完成後,便能呼叫方法,產生選課清單的文字並印出來。

 


上一篇:【Kotlin】第6課-類別的設計(一)

下一篇:【Kotlin】第8課-類別繼承與抽象類別

留言