(一)case class的定义
case class 是 Scala 中一种特殊的类,它用于创建不可变的数据容器。
语法如下:
case class ClassName(parameter1: Type1, parameter2: Type2,...)
例如,我们要创建一个学生的case class:
case class Student(name: String, age: Int, grade: String)
(二)case class的特点
(1)不可变性
case class 创建的对象是不可变的,一旦创建,其属性值不能被修改。
(2)实例化可以省略new
普通的class 在实例化时,必须要写new的。
(3)自动重写方法:
toString, equals, hashCode, copy。
自动生成方法
Case Class 自动生成方法详解
1. toString 方法
自动生成的行为:
case class Person(name: String, age: Int, email: String = "")
val alice = Person("Alice", 30, "alice@example.com")
val bob = Person("Bob", 25)
println(alice.toString) // Person(Alice,30,alice@example.com)
println(bob) // Person(Bob,25,) - 隐式调用 toString
等同于手动实现:
class ManualPerson(val name: String, val age: Int, val email: String = "") {
override def toString: String =
s"${getClass.getSimpleName}($name,$age,$email)"
}
特点:
- 包含类名和所有参数值
- 参数按定义顺序排列
- 默认参数也会显示
- 易读的调试信息
2. equals 和 hashCode 方法
自动生成的行为:
case class Point(x: Int, y: Int)
val p1 = Point(1, 2)
val p2 = Point(1, 2) // 相同的值
val p3 = Point(1, 3) // 不同的值
// equals 方法
println(p1 == p2) // true - 基于值的比较
println(p1 == p3) // false
println(p1.equals(p2)) // true
println(p1 eq p2) // false - 引用比较
// hashCode 方法
println(p1.hashCode == p2.hashCode) // true - 相同的值产生相同的hash
println(p1.hashCode == p3.hashCode) // false
等同的手动实现:
class ManualPoint(val x: Int, val y: Int) {
// equals 方法
override def equals(obj: Any): Boolean = obj match {
case that: ManualPoint =>
this.x == that.x && this.y == that.y
case _ => false
}
// hashCode 方法 - 基于所有字段
override def hashCode(): Int = {
31 * x.hashCode() + y.hashCode()
}
}
hashCode 实现原理:
// 编译器生成的 hashCode 类似这样
override def hashCode(): Int = {
var result = 1
result = 31 * result + name.hashCode // 对每个字段
result = 31 * result + age.hashCode
result = 31 * result + email.hashCode
result
}
重要特性:
case class Book(title: String, author: String, isbn: String)
val book1 = Book("Scala编程", "Martin", "123")
val book2 = Book("Scala编程", "Martin", "123")
val book3 = Book("Scala编程", "Martin", "456")
// 集合操作依赖于 equals/hashCode
val set = Set(book1, book2, book3) // 只有 book1 和 book3
println(set.size) // 2 - book1 和 book2 被认为是相同的
// 在 Map 中作为键
val library = Map(
book1 -> "书架A",
book2 -> "书架B" // 会覆盖 book1 的位置
)
println(library) // Map(Book(Scala编程,Martin,456) -> 书架B)
3. copy 方法
基本用法:
case class User(
username: String,
email: String,
active: Boolean = true,
createdAt: java.time.LocalDate = java.time.LocalDate.now()
)
val original = User("alice", "alice@example.com")
// 1. 修改单个字段
val updatedEmail = original.copy(email = "new.alice@example.com")
println(updatedEmail) // User(alice,new.alice@example.com,true,...)
// 2. 修改多个字段
val deactivated = original.copy(active = false, email = "inactive@example.com")
// 3. 创建完全相同的副本
val clone = original.copy()
// 4. 保持部分字段不变
val sameUserDifferentEmail = original.copy(email = "another@example.com")
编译器生成的 copy 方法:
// 对于 case class User(username: String, email: String, active: Boolean)
// 编译器会生成:
def copy(
username: String = this.username,
email: String = this.email,
active: Boolean = this.active
): User = new User(username, email, active)
实用场景示例:
case class Order(
id: String,
customer: String,
items: List[String],
total: Double,
status: String = "pending",
createdAt: Long = System.currentTimeMillis()
)
// 创建初始订单
val order1 = Order("001", "Alice", List("Book", "Pen"), 50.0)
// 更新订单状态
val order2 = order1.copy(
items = order1.items :+ "Notebook",
total = order1.total + 15.0,
status = "processing"
)
// 完成订单
val order3 = order2.copy(status = "completed")
println(s"Original: $order1")
println(s"Updated: $order2")
println(s"Completed: $order3")
链式更新模式:
case class Config(
host: String = "localhost",
port: Int = 8080,
timeout: Int = 5000,
ssl: Boolean = false
)
// 使用 copy 进行配置构建
val config = Config()
.copy(host = "api.example.com")
.copy(port = 443)
.copy(ssl = true)
.copy(timeout = 10000)
// 更简洁的方式
val config2 = Config(
host = "api.example.com",
port = 443,
ssl = true,
timeout = 10000
)
嵌套 case class 的 copy:
case class Address(street: String, city: String, zip: String)
case class Employee(name: String, age: Int, address: Address)
val emp1 = Employee(
"Bob",
30,
Address("Main St", "NYC", "10001")
)
// 修改嵌套对象 - 需要创建新的 Address
val emp2 = emp1.copy(
age = 31,
address = emp1.address.copy(city = "Brooklyn", zip = "11201")
)
println(emp1)
println(emp2)
copy 方法的注意事项:
case class Product(name: String, price: Double)
val product = Product("Laptop", 999.99)
// 1. copy 不修改原对象
println(product) // Product(Laptop,999.99) - 不变
val discounted = product.copy(price = 799.99)
println(discounted) // Product(Laptop,799.99)
// 2. 可以指定任意字段组合
val renamed = product.copy(name = "Gaming Laptop")
// 3. 默认值会被使用
case class Settings(enabled: Boolean = false, count: Int = 0)
val s1 = Settings()
val s2 = s1.copy(count = 5) // enabled 保持默认值 false
总结对比
| 方法 | 普通类需要 | Case Class 自动生成 |
|---|---|---|
toString | 手动实现 | 生成 ClassName(field1,field2,...) |
equals | 手动实现 | 基于所有字段的值比较 |
hashCode | 手动实现 | 基于所有字段计算 |
copy | 无此方法 | 生成带默认参数的复制方法 |
这些自动生成的方法使得 case class 特别适合:
- 不可变数据模型
- 值对象(Value Objects)
- 模式匹配
- 函数式编程中的状态更新
- 数据转换和传输
(三)case class 与普通class的区别
一、核心区别概述
| 特性 | Case Class | 普通 Class |
|---|---|---|
| 实例化 | 不需要 new | 需要 new |
| 相等性比较 | 基于值 | 基于引用 |
| 不可变性 | 默认不可变 | 默认可变 |
| 模式匹配 | 天然支持 | 需要手动实现 |
| 自动方法 | 生成多个实用方法 | 很少自动生成 |
| 用途 | 值对象/数据载体 | 业务逻辑/行为封装 |
二、详细对比示例
1. 实例化方式
// Case Class
case class Person(name: String, age: Int)
val p1 = Person("Alice", 30)
val p2 = Person.apply("Bob", 25)
// 普通 Class
class Employee(name: String, age: Int)
val e1 = new Employee("Alice", 30)
2. 相等性比较
// Case Class - 值相等
case class PointCC(x: Int, y: Int)
val cc1 = PointCC(1, 2)
val cc2 = PointCC(1, 2)
println(cc1 == cc2)
println(cc1 eq cc2)
println(cc1.equals(cc2))
// 普通 Class - 引用相等
class PointNC(x: Int, y: Int)
val nc1 = new PointNC(1, 2)
val nc2 = new PointNC(1, 2)
println(nc1 == nc2)
println(nc1 eq nc2)
println(nc1.equals(nc2))
3. 自动生成的方法
// Case Class 自动生成以下方法:
case class User(name: String, age: Int)
println(User("Alice", 30))
val u1 = User("Alice", 30)
val u2 = User("Alice", 30)
println(u1 == u2)
val u3 = u1.copy(age = 31)
val u4 = User.apply("Bob", 25)
val User(name, age) = u1
println(name)
// 普通 Class 需要手动实现:
class ManualUser(val name: String, val age: Int) {
override def toString: String = s"ManualUser($name,$age)"
override def equals(obj: Any): Boolean = obj match {
case that: ManualUser =>
this.name == that.name && this.age == that.age
case _ => false
}
override def hashCode(): Int = 31 * name.hashCode + age
def copy(name: String = this.name, age: Int = this.age): ManualUser =
new ManualUser(name, age)
}
4. 模式匹配支持
// Case Class - 天然支持
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
def area(shape: Shape): Double = shape match {
case Circle(r) => math.Pi * r * r
case Rectangle(w, h) => w * h
}
// 普通 Class - 需要手动实现 unapply
class CircleClass(val radius: Double)
object CircleClass {
def unapply(c: CircleClass): Option[Double] = Some(c.radius)
}
val circle = new CircleClass(5.0)
circle match {
case CircleClass(r) => println(s"Radius: $r")
case _ => println("Unknown")
}
5. 不可变性 vs 可变性
// Case Class - 默认不可变
case class ImmutablePerson(name: String, age: Int)
val p = ImmutablePerson("Alice", 30)
// 如果需要可变,必须显式声明
case class MutablePerson(var name: String, var age: Int)
val mp = MutablePerson("Alice", 30)
mp.name = "Bob"
// 普通 Class - 默认可变
class RegularPerson(var name: String, var age: Int)
val rp = new RegularPerson("Alice", 30)
rp.name = "Bob"
// 普通 Class 也可以定义为不可变
class ImmutableRegularPerson(val name: String, val age: Int)
val irp = new ImmutableRegularPerson("Alice", 30)
6. 集合中的行为
import scala.collection.mutable
// Case Class - 在 Set 中自动去重
case class Student(id: Int, name: String)
val set1 = mutable.Set(
Student(1, "Alice"),
Student(1, "Alice")
)
println(set1.size)
// 普通 Class - 不去重
class Teacher(val id: Int, val name: String)
val set2 = mutable.Set(
new Teacher(1, "Alice"),
new Teacher(1, "Alice")
)
println(set2.size)
7. 继承限制
// Case Class 不能继承另一个 Case Class
case class Person(name: String, age: Int)
// 但可以继承普通类或特质
trait Identifiable { def id: Int }
case class Customer(id: Int, name: String) extends Identifiable
// 普通 Class 没有这个限制
class Animal(name: String)
class Dog(name: String, breed: String) extends Animal(name)
8. 序列化支持
import java.io._
// Case Class 天然支持序列化
case class Data(id: Int, value: String) extends Serializable
val data = Data(1, "test")
val bytes = serialize(data)
val deserialized = deserialize[Data](bytes)
println(deserialized == data)
// 普通 Class 需要显式实现 Serializable
class RegularData(val id: Int, val name: String) extends Serializable
def serialize[T](obj: T): Array[Byte] = {
val baos = new ByteArrayOutputStream()
val oos = new ObjectOutputStream(baos)
oos.writeObject(obj)
oos.close()
baos.toByteArray
}
def deserialize[T](bytes: Array[Byte]): T = {
val bais = new ByteArrayInputStream(bytes)
val ois = new ObjectInputStream(bais)
ois.readObject().asInstanceOf[T]
}
三、使用场景建议
使用 Case Class 的场景:
case class UserDTO(id: Long, username: String, email: String)
case class DatabaseConfig(
url: String,
user: String,
password: String,
poolSize: Int = 10
)
case class GameState(
score: Int = 0,
level: Int = 1,
playerPosition: (Int, Int) = (0, 0)
)
sealed trait Expr
case class Number(n: Int) extends Expr
case class Add(left: Expr, right: Expr) extends Expr
case class Multiply(left: Expr, right: Expr) extends Expr
使用普通 Class 的场景:
class BankAccount(initialBalance: Double) {
private var balance = initialBalance
def deposit(amount: Double): Unit = {
require(amount > 0)
balance += amount
}
def withdraw(amount: Double): Boolean = {
if (amount <= balance) {
balance -= amount
true
} else false
}
def currentBalance: Double = balance
}
class Counter {
private var count = 0
def increment(): Unit = count += 1
def decrement(): Unit = count -= 1
def getCount: Int = count
}
class QueryBuilder {
private var selectClause = ""
private var fromClause = ""
private var whereClause = ""
def select(columns: String*): this.type = {
selectClause = columns.mkString(", ")
this
}
def from(table: String): this.type = {
fromClause = table
this
}
def build: String = s"SELECT $selectClause FROM $fromClause"
}
四、性能考虑
case class Point(x: Int, y: Int)
class FastPoint(val x: Int, val y: Int)
val n = 1000000
val ccStart = System.currentTimeMillis()
val ccPoints = (1 to n).map(i => Point(i, i * 2))
val ccTime = System.currentTimeMillis() - ccStart
val ncStart = System.currentTimeMillis()
val ncPoints = (1 to n).map(i => new FastPoint(i, i * 2))
val ncTime = System.currentTimeMillis() - ncStart
println(s"Case Class: ${ccTime}ms, Normal Class: ${ncTime}ms")
总结
优先选择 Case Class 当:
- 数据是不可变的
- 需要基于值的相等性
- 会进行模式匹配
- 需要自动的
toString、copy等方法 - 用作集合元素或 Map 的键
使用普通 Class 当:
- 需要可变状态
- 有复杂的业务逻辑和行为
- 需要性能优化
- 需要继承另一个类
- 实现特定的设计模式