在前幾節中,我們學習了 Scala 的基礎語法和流程控制。現在,我們將深入探索 Scala 作為一門純粹的面向對象語言的核心。在 Scala 中,萬物皆對象,沒有像 Java 那樣的原始類型和靜態成員的區分。本節將重點介紹如何定義對象的藍圖,以及如何使用 Scala 獨特的單例對象和伴生機制。
思維導圖
一、類和對象
類 是創建對象的模板或藍圖。它定義了一類事物共同的屬性 (成員變量) 和行為 (成員方法)。
對象,也稱為實例,是根據類這個藍圖創建出來的具體實體。
基本語法:
class ClassName {// 成員變量 (字段)// 成員方法
}// 使用 new 關鍵字創建類的實例 (對象)
val objectName = new ClassName()
二、成員變量與成員方法
1. 定義和訪問成員變量
在類中定義的變量或常量,稱為成員變量或字段。
class Person {// 定義一個可變的成員變量 namevar name: String = "Unknown"// 定義一個不可變的成員變量 (常量) ageval age: Int = 0
}// 創建 Person 類的對象
val person1 = new Person()// 訪問和修改成員變量
println(person1.name) // 輸出: Unknown
person1.name = "Alice"
println(person1.name) // 輸出: Alice
// person1.age = 25 // 這行會編譯錯誤,因為 age 是 val (常量)
2. 使用下劃線 _
初始化成員變量
在 Scala 中,var
類型的成員變量必須被初始化。如果你暫時不想給它一個有意義的初始值,可以使用下劃線 _
作為占位符,Scala 會為其賦予該類型的默認零值。
類型 | 默認零值 |
---|---|
數值類型 (Int, Double, etc.) | 0 |
Boolean | false |
Char | \u0000 |
所有引用類型 (AnyRef ) | null |
代碼案例:
class Student {var name: String = _ // 初始化為 nullvar age: Int = _ // 初始化為 0var isMale: Boolean = _ // 初始化為 false
}val student1 = new Student()
println(s"Name: ${student1.name}, Age: ${student1.age}, Is Male: ${student1.isMale}")
// 輸出: Name: null, Age: 0, Is Male: false
注意: 這種下劃線初始化的方式在現代 Scala 編程中使用得越來越少,因為它容易引入 NullPointerException
。更推薦的做法是提供一個有意義的初始值,或者使用Option
類型來表示可能缺失的值。
3. 定義和訪問成員方法
成員方法定義了對象的行為。
class Circle {val radius: Double = 5.0// 定義一個計算面積的方法def getArea(): Double = {Math.PI * radius * radius}
}val c1 = new Circle()
// 調用方法
val area = c1.getArea()
println(s"The area of the circle is: $area")
三、訪問權限修飾符
Scala 通過訪問權限修飾符來控制成員的可見性,以實現封裝。
修飾符 | 描述 |
---|---|
(無修飾符) | 默認為 public ,在任何地方都可以訪問。 |
private | 私有成員,只能在定義該成員的類或其伴生對象內部訪問。 |
protected | 受保護成員,只能在定義該成員的類及其子類中訪問。 |
private[this] | 對象私有,比 private 更嚴格。只能在當前對象實例中訪問,即使是同一個類的其他對象也不能訪問。 |
private[包名] | 包私有,成員的可見性被限定在指定的包及其子包中。 |
代碼案例:
class Animal {private var privateName = "Secret"protected var protectedAge = 2def printInfo(): Unit = {println(s"This is a private name: $privateName") // 類內部可以訪問 private}
}class Dog extends Animal {def getAge(): Int = {// println(privateName) // 錯誤:子類不能訪問父類的 private 成員protectedAge // 正確:子類可以訪問父類的 protected 成員}
}val animal = new Animal()
// println(animal.privateName) // 錯誤:外部不能訪問 private 成員
// println(animal.protectedAge) // 錯誤:外部不能訪問 protected 成員
animal.printInfo()val dog = new Dog()
println(s"Dog's age: ${dog.getAge()}")
四、類的構造器
構造器是在創建對象時自動調用的特殊方法,用于初始化對象。
1. 主構造器
主構造器直接定義在類名之后的參數列表中。
主構造器會執行類定義中所有的語句。
如果主構造器的參數沒有用val
或var
聲明,它將是一個私有的不可變字段,僅在類內部可見。
如果用val
或var
聲明,該參數會成為一個公共的成員變量。
代碼案例:
// name 和 age 是主構造器的參數,并成為公共的不可變/可變成員變量
class Employee(val name: String, var age: Int) {// 這部分代碼是主構造器的一部分,在 new Employee(...) 時執行println(s"New employee created: $name, age $age")// 一個普通的成員方法def work(): Unit = println(s"$name is working.")
}val emp1 = new Employee("Alice", 30)
println(emp1.name) // 可以訪問
emp1.age = 31 // 可以修改
2. 輔助構造器
一個類可以有多個輔助構造器。
輔助構造器的名稱必須是this
。
關鍵規則:每個輔助構造器的第一行必須直接或間接地調用主構造器 (或另一個已定義的輔助構造器)。
代碼案例:
class Car(val brand: String, val year: Int) {var color: String = "White"// 輔助構造器一:提供品牌、年份和顏色def this(brand: String, year: Int, color: String) {this(brand, year) // 必須先調用主構造器this.color = color}// 輔助構造器二:只提供品牌def this(brand: String) {this(brand, 2024) // 調用主構造器,年份默認為 2024}
}val car1 = new Car("Toyota", 2023)
val car2 = new Car("BMW", 2024, "Black")
val car3 = new Car("Ford")
println(s"${car3.brand} color is ${car3.color} and year is ${car3.year}")
五、單例對象、main方法與伴生對象
1. 單例對象
在 Scala 中,使用 object
關鍵字定義的不是類,而是一個單例對象——它是一個全局唯一的實例。
單例對象不能被 new
,它的所有成員都類似于 Java 中的靜態成員。
代碼案例:
object Logger {var level: String = "INFO"def log(message: String): Unit = {println(s"[$level] $message")}
}// 直接通過對象名訪問成員
Logger.level = "DEBUG"
Logger.log("This is a debug message.")
2. main 方法
Scala 應用程序的入口點是一個名為 main
的方法,它必須定義在一個單例對象中。
兩種實現方式:
- 標準
main
方法:
object MyApp {def main(args: Array[String]): Unit = {println("Hello from the main method!")}
}
- 繼承
App
特質 (更簡潔):
object MyApp extends App {// 這里的代碼會直接作為 main 方法體執行println("Hello from the App trait!")// 命令行參數可以通過 args 變量訪問if (args.length > 0) {println(s"First argument: ${args(0)}")}
}
3. 伴生對象
當一個單例對象與一個類具有相同的名稱,并且它們定義在同一個源文件中時,這個對象被稱為該類的伴生對象,該類被稱為該對象的伴生類。
核心特性:
伴生類和伴生對象可以互相訪問對方的私有 (private
) 成員。
常見用途:
在伴生對象中放置類似于 Java 靜態方法的工具方法。
在伴生對象中定義工廠方法 (特別是名為apply
的方法),用于創建伴生類的實例,隱藏new
關鍵字。
代碼案例:
// 伴生類
class User private (val id: Int, val name: String) { // 主構造器設為 privateprivate def secretMethod(): String = s"User $name has a secret."def greet(): Unit = {// 訪問伴生對象的私有成員println(User.defaultGreeting + ", " + name)}
}// 伴生對象
object User {private val defaultGreeting = "Welcome"// 工廠方法,可以訪問 User 類的私有構造器def apply(name: String): User = {val newId = scala.util.Random.nextInt(1000)new User(newId, name)}def printSecret(user: User): Unit = {// 訪問 User 實例的私有方法println(user.secretMethod())}
}// 使用伴生對象的 apply 工廠方法創建實例 (無需 new)
val user1 = User("Bob")
user1.greet()
User.printSecret(user1)// val user2 = new User(10, "Charlie") // 錯誤:構造器是私有的
六、綜合案例
在 Scala 中,工具類 (包含純粹的功能方法,不維護狀態) 通常被實現為單例對象。
代碼案例:一個簡單的字符串工具對象
object StringUtils {/*** 判斷字符串是否為空 (null 或 "")* @param s 待檢查的字符串* @return 如果為空則返回 true,否則返回 false*/def isEmpty(s: String): Boolean = {s == null || s.trim.isEmpty}/*** 將字符串首字母大寫* @param s 待轉換的字符串* @return 轉換后的字符串*/def capitalize(s: String): String = {if (isEmpty(s)) s else s.substring(0, 1).toUpperCase + s.substring(1)}
}// 在另一個對象中(例如主程序)使用工具類
object MainApp extends App {val str1 = "hello scala"val str2 = " "val str3: String = nullprintln(s"'${str1}' is empty? ${StringUtils.isEmpty(str1)}")println(s"'${str2}' is empty? ${StringUtils.isEmpty(str2)}")println(s"Capitalized '${str1}': ${StringUtils.capitalize(str1)}")
}
練習題
題目一:簡單類定義
定義一個 Book
類,包含兩個不可變的成員變量:title
(String) 和 author
(String)。
題目二:創建和訪問對象
創建 Book
類的一個實例,title
為 “Programming in Scala”,author
為 “Martin Odersky”。然后打印出這本書的標題。
題目三:成員方法
為 Book
類添加一個名為 getInfo
的方法,該方法返回一個格式為 "Title by Author"
的字符串。
題目四:下劃線初始化
定義一個 Movie
類,包含一個可變的成員變量 director
(String),使用下劃線 _
進行默認初始化。創建實例后打印出 director
的初始值。
題目五:主構造器
定義一個 Laptop
類,其主構造器接收 brand
(String, 不可變) 和 ramInGB
(Int, 可變) 兩個參數,并將它們直接定義為公共成員變量。
題目六:輔助構造器
為 Laptop
類添加一個輔助構造器,該構造器只接收 brand
參數,并默認將 ramInGB
設置為 8。
題目七:訪問修飾符
定義一個 BankAccount
類,其中 balance
(Double) 是私有的。提供一個公共的 deposit(amount: Double)
方法和一個公共的 getBalance()
方法來訪問余額。
題目八:單例對象
創建一個名為 MathConstants
的單例對象,在其中定義兩個常量:PI
(值為 3.14159) 和 E
(值為 2.71828)。
題目九:main
方法
創建一個名為 EntryPoint
的單例對象,并繼承 App
特質,在其中打印 “Scala application started!”。
題目十:伴生對象與私有成員
定義一個 Circle
類,其主構造器接收一個私有的 radius
(Double) 參數。然后,為其創建一個伴生對象,該對象有一個 calculateArea(c: Circle)
方法,可以計算并返回給定 Circle
實例的面積 (面積 = PI * r * r)。
題目十一:apply
工廠方法
在 Circle
的伴生對象中添加一個 apply
方法,該方法接收一個 radius
參數,并返回一個新的 Circle
實例。這樣就可以使用 Circle(5.0)
來創建對象。
題目十二:工具類方法
在之前的 StringUtils
單例對象中,添加一個名為 reverse
的方法,接收一個字符串并返回其反轉后的結果。
題目十三:主構造器代碼塊
修改 Employee
類的定義,在其主構造器代碼塊中添加一條邏輯:檢查傳入的 age
是否小于18,如果是,則打印一條警告信息 “Warning: Employee age is below 18.”。
題目十四:對象私有成員 private[this]
定義一個 Point
類,包含 x
和 y
兩個坐標。再定義一個 isSameAs(other: Point)
方法,比較當前點是否與另一個點相同。然后,修改 x
和 y
為 private[this]
,并觀察 isSameAs
方法是否還能正常編譯。如果不能,解釋原因。
題目十五:綜合案例
創建一個 Counter
類,它有一個私有的、可變的 count
變量,初始值為0。類中提供 increment()
方法 (每次將count加1) 和 current()
方法 (返回當前count值)。為其創建一個伴生對象,提供一個 apply
方法,允許通過 Counter()
創建新實例。
答案與解析
答案一:
class Book(val title: String, val author: String)
解析: 在主構造器參數前使用
val
是將參數直接定義為公共不可變成員變量的簡潔語法。
答案二:
val myBook = new Book("Programming in Scala", "Martin Odersky")
println(myBook.title)
解析: 使用
new
關鍵字和類名來創建對象,通過.
操作符訪問其成員。
答案三:
class Book(val title: String, val author: String) {def getInfo(): String = {s"$title by $author"}
}
val myBook = new Book("A Brief History of Time", "Stephen Hawking")
println(myBook.getInfo())
解析:
def
用于在類中定義方法。s""
字符串插值器用于方便地格式化字符串。
答案四:
class Movie {var director: String = _
}
val m = new Movie()
println(m.director) // 輸出: null
解析:
String
是引用類型 (AnyRef),其默認零值是null
。
答案五:
class Laptop(val brand: String, var ramInGB: Int)
解析:
val
使brand
成為不可變成員,var
使ramInGB
成為可變成員。
答案六:
class Laptop(val brand: String, var ramInGB: Int) {// 輔助構造器def this(brand: String) {this(brand, 8) // 調用主構造器}
}
val defaultLaptop = new Laptop("Dell")
println(s"${defaultLaptop.brand} has ${defaultLaptop.ramInGB}GB RAM")
解析: 輔助構造器
def this(...)
必須在其第一行調用另一個構造器。
答案七:
class BankAccount {private var balance: Double = 0.0def deposit(amount: Double): Unit = {if (amount > 0) balance += amount}def getBalance(): Double = {balance}
}
解析:
private
關鍵字將balance
的訪問權限限制在類內部,外部只能通過公共的deposit
和getBalance
方法進行交互,實現了封裝。
答案八:
object MathConstants {val PI = 3.14159val E = 2.71828
}
println(MathConstants.PI)
解析:
object
關鍵字創建了一個全局唯一的單例對象。
答案九:
object EntryPoint extends App {println("Scala application started!")
}
解析: 繼承
App
特質是創建可執行應用程序的最簡潔方式,對象體內的代碼會自動成為main
方法的內容。
答案十:
class Circle private (val radius: Double)object Circle {def calculateArea(c: Circle): Double = {// 可以訪問 Circle 的私有成員 radiusMath.PI * c.radius * c.radius}
}
解析: 伴生對象
Circle
可以訪問伴生類Circle
的private
成員radius
。
答案十一:
class Circle private (val radius: Double)object Circle {def apply(radius: Double): Circle = {new Circle(radius)}// ... calculateArea 方法 ...
}val myCircle = Circle(5.0) // 無需 new,直接調用 apply 方法
解析:
apply
方法是一個特殊的語法糖,允許你像調用函數一樣創建對象。
答案十二:
object StringUtils {// ... isEmpty, capitalize 方法 ...def reverse(s: String): String = {if (s == null) s else s.reverse}
}
println(StringUtils.reverse("scala"))
```* **解析:** `String` 類型自帶 `.reverse` 方法,可以直接使用。**答案十三:**
```scala
class Employee(val name: String, var age: Int) {if (age < 18) {println(s"Warning: Employee $name's age is below 18.")}def work(): Unit = println(s"$name is working.")
}
val youngEmployee = new Employee("Tom", 17)
解析: 類定義體中、成員方法之外的代碼都屬于主構造器的一部分,會在對象創建時執行。
答案十四:
class Point(private[this] val x: Int, private[this] val y: Int) {def isSameAs(other: Point): Boolean = {// this.x == other.x // 這行代碼會編譯錯誤false // 僅為使代碼完整}
}
解析: 代碼無法正常編譯。因為
private[this]
是對象私有的,意味著只有當前對象 (this
) 才能訪問x
和y
。在isSameAs
方法中,other.x
嘗試訪問另一個Point
對象的x
字段,這是不被允許的。如果使用private
,則可以訪問,因為private
允許同一類的不同實例之間互相訪問私有成員。
答案十五:
class Counter private {private var count: Int = 0def increment(): Unit = {count += 1}def current(): Int = {count}
}object Counter {def apply(): Counter = new Counter()
}val c1 = Counter()
c1.increment()
c1.increment()
println(c1.current()) // 輸出: 2
解析: 這個例子結合了私有構造器、私有成員、公共方法和伴生對象的
apply
工廠方法,是一個典型的Scala封裝模式。將構造器設為私有,強制用戶通過伴生對象的工廠方法來創建實例。
日期:2025年9月14日
專欄:Scala教程