Scala中的caseclass(二)

31 阅读5分钟

(一)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. equalshashCode 方法

自动生成的行为:

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 当

  • 数据是不可变的
  • 需要基于值的相等性
  • 会进行模式匹配
  • 需要自动的 toStringcopy 等方法
  • 用作集合元素或 Map 的键

使用普通 Class 当

  • 需要可变状态
  • 有复杂的业务逻辑和行为
  • 需要性能优化
  • 需要继承另一个类
  • 实现特定的设计模式