从 Scala 普通类到 Case Class:三段代码拆解 “数据类” 的设计进化

72 阅读4分钟

Scala 中,“如何高效封装数据并适配集合操作” 是基础但关键的问题。以下通过三段递进式代码,从 “普通类手动实现” 到 “Case Class 自动封装”,逐步解析 Scala 数据类的设计逻辑与最佳实践。

一、第一段代码:普通类的 “手动式数据封装”

这是一段普通类 + 手动重写方法的代码,代表了 “无语法糖时的基础实现方式”

代码核心逻辑

package caseclass
import scala.collection.mutable

object caseclass1 {
  // 普通类Book:需手动定义属性与方法
  class Book(var id:Int, val name:String) {
    // 手动重写equals:判断id和name是否一致
    override def equals(obj: Any): Boolean = {
      val other = obj.asInstanceOf[Book]
      other.id == id && other.name == name
    }
    // 手动重写hashCode:仅基于id计算
    override def hashCode(): Int = id
  }

  def main(args: Array[String]): Unit = {
    // 测试1:Int类型Set去重(内置类型自带规范方法)
    val set1: mutable.Set[Int] = mutable.Set(1,2)
    set1 += 1; set1 += 1; set1 += 1; set1 += 1
    println(set1) // 输出Set(1, 2)(自动去重)

    // 测试2:Book对象的相等性与Set去重
    val book1 = new Book(1,"西游记")
    val book2 = new Book(1,"西游记")
    println(book1 == book2) // 输出true(equals判断一致)

    val set2: mutable.Set[Book] = mutable.Set()
    set2 += book1; set2 += book2
    println(set2) // 输出Set(Book@1)(去重成功)
  }
}

设计痛点

普通类要实现 “数据相等性判断” 和 “Set 去重”,必须手动重写equalshashCode,但存在明显问题:

  1. 代码冗余:每定义一个数据类都要写重复的equals/hashCode
  2. 易出错:示例中hashCode仅基于id计算,若两个Bookid相同但name不同,会出现 “equals不等但hashCode相同” 的情况,导致 Set 性能退化
  3. 不健壮equals中用asInstanceOf强制类型转换,若传入非Book类型对象会直接抛异常

二、第二段代码:Case Class 的 “自动式数据封装”

这是 Case Class 的实现,它是 Scala 为 “数据封装” 设计的语法糖,解决了普通类的痛点。

代码核心逻辑

package caseclass
import scala.collection.mutable

object caseclass2 {
  // Case Class Book:仅定义属性,自动生成方法
  case class Book(var id:Int, val name:String) {}

  def main(args: Array[String]): Unit = {
    // Case Class的对象创建:可省略new(自动生成apply方法)
    val book1 = new Book(1,"西游记")
    val book2 = Book(1,"西游记") // 更简洁的写法

    println(book1 == book2) // 输出true(自动生成的equals判断一致)

    val set2: mutable.Set[Book] = mutable.Set()
    set2 += book1; set2 += book2
    println(set2) // 输出Set(Book(1,西游记))(去重成功,toString自动生成)
  }
}

Case Class 的自动能力

Case Class 会自动为开发者生成以下内容(无需手动编写):

  1. equals方法:基于所有构造参数判断相等,且通过模式匹配处理类型转换(避免异常)
  2. hashCode方法:基于所有构造参数计算哈希值
  3. toString方法:自动生成包含参数的字符串
  4. 伴生对象的apply方法:允许省略new关键字创建对象

对比普通类的优势

  • 代码更简洁:一行case class替代普通类的多行动态
  • 更可靠:自动生成的方法严格遵循规范,避免手动编写的错误
  • 更高效:自带常用方法,减少样板代码开发量

三、第三段代码:Case Class 的 “默认规范”

这一段代码进一步补充了 Case Class 的默认特性,明确其 “数据类” 的设计定位。

代码逻辑

package caseclass
// case class的核心特性注释
// 1.可以省略new
// 2.属性默认是不可变的。val 修饰
// 3.他会自动去实现toString,equals,hashcode等方法
object caseclass3 {
  // Case Class Student:属性默认是val(不可变)
  case class Student (name:String, age:Int)

  def main(args: Array[String]): Unit = {
    val st1 = Student("小花",18) // 省略new
    val st2 = Student("小花",18)
    println(st1) // 输出Student(小花,18)(自动toString)
    println(st1 == st2) // 输出true(自动equals)
  }
}

Case Class 的默认规则

这段代码的注释点明了 Case Class 的核心特性:

  1. 属性默认不可变:Case Class 的构造参数默认是val修饰,若要可变需显式写var
  2. 省略new:通过伴生对象的apply方法实现
  3. 自动实现常用方法toString/equals/hashCode无需手动编写

四、三段代码的进化逻辑:从 “手动” 到 “自动”

三段代码的设计思路,本质是 Scala 对 “数据类” 的优化过程:

代码段类类型实现复杂度可靠性适用场景
第一段普通类高(手动写方法)低(易出错)复杂业务逻辑类
第二段Case Class(含 var)低(自动生成)需修改属性的数据类
第三段Case Class(默认 val)极低极高纯数据封装(不可变)

五、建议

  1. 若要封装纯数据 ,优先用 Case Clas,兼顾简洁性与不可变性
  2. 若数据需动态修改,在 Case Class 中显式用var修饰属性
  3. 避免用普通类封装数据 —— 手动重写方法不仅繁琐,还容易出现规范错误