七、Scala 包、样例类与样例对象
随着我们构建的系统越来越复杂,如何清晰地组织代码和高效地对数据进行建模,变得至关重要。本节,我们将深入学习 Scala 提供的两大利器:用于构建命名空间和模块化的包,以及为数据而生、并为模式匹配提供强大支持的样例类 和 样例对象
思维导图
一、包
1.1 包简介
包是 Scala 组织代码的基本单位。它提供了一个命名空间,可以有效避免不同代码模块间的命名冲突,并帮助我们构建清晰的项目结构。
1.2 包的定义格式
Scala 支持两种定义包的方式:
方式一:文件顶部声明
这是最常见的方式,与 Java 类似。
package com.mycompany.project.moduleAclass MyClass {// ...
}
方式二:嵌套风格
允许在同一个文件中定义属于不同包的内容,并共享父包的作用域。
package com.mycompany.project {// 这里的代码属于 com.mycompany.project 包package moduleB {// 这里的代码属于 com.mycompany.project.moduleB 包class AnotherClass {// ...}}
}
1.3 作用域
使用嵌套风格时,子包可以直接访问父包中的成员,无需导入。
package parent {class ParentClasspackage child {class ChildClass {val p = new ParentClass //可以直接访问}}
}
1.4 包的引入
使用 import
关键字可以引入其他包中的类、对象或成员,以便在当前文件中直接使用。
引入方式 | 语法 | 示例与说明 |
---|---|---|
引入单个成员 | import path.to.Member | import java.util.Date |
引入多个成员 | import path.to.{Member1, Member2} | import scala.collection.mutable.{Map, Set} |
通配符引入 | import path.to._ | import scala.collection.mutable._ (引入 mutable 包下所有成员) |
重命名引入 | import path.to.{Member => NewName} | import java.util.{Date => JDate} (避免与自定义的Date 类冲突) |
隐藏成员引入 | import path.to.{Member => _, _} | import java.util.{Date => _, _} (引入 util 包下除 Date 外的所有成员) |
1.5 包对象
有时,我们希望在整个包的范围内共享一些常量或工具方法。包对象就是为此而生。每个包最多只能有一个包对象。
语法:
在与包同级的目录下,创建一个名为 package.scala
的文件。
// 文件名:com/mycompany/project/package.scala
package com.mycompanypackage object project {val PI = 3.14159def sayPackageHello(): Unit = println("Hello from the project package!")
}
使用:
// 在 com.mycompany.project 包下的任何文件中
package com.mycompany.projectclass MyClass {def calculate(radius: Double): Double = {sayPackageHello() // 直接调用2 * PI * radius // 直接使用}
}
1.6 包的可见性
可以使用 private[包名]
修饰符来扩大私有成员的可见范围,使其在指定的包内可见。
二、样例类
样例类是一种特殊的类,它专门为存储不可变 (immutable) 的数据而优化。当你定义一个类为 case class
时,Scala 编译器会自动为你生成一系列实用方法。
2.1 格式与特性
特性 (Feature) | 编译器自动生成的内容 | 说明 |
---|---|---|
不可变性 | 主构造器参数默认为 val 。 | 鼓励使用不可变的数据模型,更安全。 |
工厂方法 | 伴生对象中自动创建 apply 方法。 | 创建实例时无需使用 new 关键字,代码更简洁。 |
模式匹配支持 | 伴生对象中自动创建 unapply 方法。 | 这是样例类最重要的特性,使其能无缝用于模式匹配。 |
自动方法实现 | 实现了合理的 toString , equals , hashCode 方法。 | 便于打印、比较和在集合中使用。 |
copy 方法 | 自动创建 copy 方法。 | 可以方便地创建一个新实例,并修改其中部分字段。 |
2.2 示例
// 定义一个样例类
case class Person(name: String, age: Int)// 1. 无需 new 关键字创建实例 (apply 方法)
val alice = Person("Alice", 30)
val bob = Person("Bob", 32)
val anotherAlice = Person("Alice", 30)// 2. 友好的 toString 输出
println(alice) // 输出: Person(Alice,30)// 3. 基于值的 equals 和 hashCode
println(alice == bob) // 输出: false
println(alice == anotherAlice) // 输出: true// 4. 使用 copy 方法创建新实例
val olderAlice = alice.copy(age = 31)
println(olderAlice) // 输出: Person(Alice,31)
2.3 样例类中的默认方法
如上表所示,apply
, unapply
, toString
, equals
, hashCode
, copy
等都是编译器为样例类自动生成的“默认方法”。
三、样例对象
3.1 格式
case object
是一个同时具备样例类特性和单例对象特性的特殊对象。
3.2 示例
样例对象非常适合用于表示枚举或固定状态的集合,通常与 sealed trait
(密封特质) 配合使用。
// 密封特质,表示其所有子类必须在同一个文件中定义
sealed trait DayOfWeekcase object Monday extends DayOfWeek
case object Tuesday extends DayOfWeek
// ... 其他天def activity(day: DayOfWeek): String = day match {case Monday => "Work hard"case _ => "Relax" // _ 是通配符
}println(activity(Monday)) // 输出: Work hard
四、综合案例
这个案例将综合运用本节的知识,特别是样例类和样例对象,来构建一个灵活的消息模型。
8.1 需求
我们需要一个系统来表示不同类型的消息,例如文本消息、图片消息,以及一个特殊的断开连接信号。
8.2 目的
使用 sealed trait
, case class
和 case object
来清晰地对这些消息进行建模,并编写一个可以处理不同消息类型的函数。
8.3 参考代码
package com.example.messaging// 1. 使用 sealed trait 定义消息的基类型
sealed trait Message// 2. 使用 case class 定义包含数据的消息类型
case class TextMessage(sender: String, content: String) extends Message
case class ImageMessage(sender: String, imageUrl: String, caption: String) extends Message// 3. 使用 case object 定义不包含数据的信号类型消息
case object Disconnect extends Message// 4. 创建一个消息处理器对象
object MessageProcessor {// 这个方法使用模式匹配来处理不同类型的消息// (模式匹配是后续章节的核心内容,这里是预演)def process(msg: Message): String = msg match {case TextMessage(sender, content) =>s"Received text from $sender: '$content'"case ImageMessage(sender, url, caption) =>s"Received an image from $sender with caption '$caption'. URL: $url"case Disconnect =>"A client has disconnected."}
}// 5. 在主程序中使用
object MainApp extends App {import com.example.messaging._val text = TextMessage("Alice", "Hello there!")val image = ImageMessage("Bob", "http://example.com/img.png", "My vacation photo")val disconnectSignal = Disconnectprintln(MessageProcessor.process(text))println(MessageProcessor.process(image))println(MessageProcessor.process(disconnectSignal))
}
练习题
题目一:包定义
在一个名为 MyApp.scala
的文件中,使用嵌套风格将 DatabaseService
类定义在 com.myapp.services
包下,将 User
类定义在 com.myapp.models
包下。
题目二:包引入
在一个文件中,如何只引入 scala.collection.mutable
包中的 ArrayBuffer
和 ListBuffer
?
题目三:重命名引入
你的代码中已经有一个 Map
类,现在你需要使用 scala.collection.immutable.Map
。请写出引入语句,将 scala.collection.immutable.Map
重命名为 ImmutableMap
以避免冲突。
题目四:包对象
在 com.utils
包中创建一个包对象,定义一个名为 APP_VERSION
的常量,值为 "1.0.0"
。
题目五:简单样例类
定义一个 case class
Point
,它有两个 Double
类型的字段:x
和 y
。
题目六:样例类实例化
使用上题定义的 Point
样例类,创建一个实例 p1
,表示坐标 (3.0, 4.0),不要使用 new
关键字。
题目七:样例类的 copy
方法
基于上题创建的 p1
实例,使用 copy
方法创建一个新实例 p2
,其 x
坐标与 p1
相同,但 y
坐标为 -4.0
。
题目八:样例类的 equals
创建两个 Point
实例,p3
和 p4
,它们都表示坐标 (1.5, 2.5)。然后判断 p3 == p4
的结果是什么。
题目九:样例对象
使用 sealed trait
和 case object
定义一个 Status
枚举,包含三个状态:Pending
, Success
, Failure
。
题目十:在样例类中使用 var
定义一个 case class
Task
,它有一个不可变的 id
(Int) 和一个可变的 status
(String)。
题目十一:包可见性
在 com.security
包中定义一个 Credentials
类。该类有一个 password
字段,使其只能在 com.security
包内部可见。
题目十二:unapply
方法的作用
样例类自动生成的 unapply
方法最主要的用途是什么?
题目十三:样例对象与样例类的区别
case class
和 case object
最根本的区别是什么?
题目十四:综合 - HTTP请求模型
使用 sealed trait
和样例类/对象为简单的HTTP请求方法进行建模,包括 GET(url: String)
, POST(url: String, body: String)
和一个表示 OPTIONS
请求的样例对象。
题目十五:综合 - 几何图形模型
定义一个 sealed trait
GeometricShape
。然后定义两个 case class
:Circle(radius: Double)
和 Rectangle(width: Double, height: Double)
,都继承自 GeometricShape
。
答案与解析
答案一:
package com.myapp {package models {class User}package services {class DatabaseService}
}
解析: 嵌套包定义允许在同一文件中组织多个相关包的类。
答案二:
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
解析: 使用花括号
{...}
可以从同一个父包中选择性地引入多个成员。
答案三:
import scala.collection.immutable.{Map => ImmutableMap}
解析:
=>
符号用于在引入时为成员指定一个新的名称。
答案四:
// 文件路径: com/utils/package.scala
package compackage object utils {val APP_VERSION = "1.0.0"
}
解析: 包对象必须定义在名为
package.scala
的文件中。
答案五:
case class Point(x: Double, y: Double)
解析: 这是定义样例类的最简洁形式。
x
和y
默认是public val
。
答案六:
val p1 = Point(3.0, 4.0)
解析: 样例类会自动生成伴生对象和
apply
方法,因此可以省略new
。
答案七:
val p1 = Point(3.0, 4.0)
val p2 = p1.copy(y = -4.0)
解析:
copy
方法通过带名参数可以方便地创建修改了部分字段的新实例。
答案八:
结果是 true
。
val p3 = Point(1.5, 2.5)
val p4 = Point(1.5, 2.5)
println(p3 == p4) // 输出: true
解析: 样例类的
equals
方法是基于值的比较,而不是引用比较。只要所有字段的值都相等,实例就相等。
答案九:
sealed trait Status
case object Pending extends Status
case object Success extends Status
case object Failure extends Status
解析:
sealed trait
+case object
是在 Scala 2 中实现类型安全枚举的标准模式。
答案十:
case class Task(val id: Int, var status: String)
解析: 可以在样例类的构造器参数前显式使用
var
来创建可变字段。
答案十一:
package com.security {class Credentials {private[security] var password: String = _}class Authenticator {def check(c: Credentials): Unit = {println(c.password) // 在 com.security 包内可以访问}}
}
解析:
private[security]
将password
的可见性限定在com.security
包内。
答案十二:
unapply
方法最主要的用途是支持模式匹配 。
解析: 它允许样例类实例被“解构”,即在
case
语句中提取出其构造器参数。
答案十三:
最根本的区别是:case class
是一个类,用于创建多个实例;而 case object
是一个单例对象,全局只有一个实例。
答案十四:
sealed trait HttpMethod
case class GET(url: String) extends HttpMethod
case class POST(url: String, body: String) extends HttpMethod
case object OPTIONS extends HttpMethod
解析: 这个模型清晰地区分了需要携带数据的请求 (
GET
,POST
) 和不需要携带数据的信号类请求 (OPTIONS
)。
答案十五:
sealed trait GeometricShape
case class Circle(radius: Double) extends GeometricShape
case class Rectangle(width: Double, height: Double) extends GeometricShape
解析:
sealed trait
与case class
结合是构建代数数据类型 (ADT) 的基础,在函数式编程中非常常见。
日期:2025年9月21日
专栏:Scala教程
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=480jfj75dvu