Scala Case Class 实战教学:从问题引入到完整应用

60 阅读5分钟

导入

在之前我们学习了 Scala 中的一些数据结构 Set,我们知道 Set 中是不能放入重复的元素的。下面我们来看一个特殊的问题。在集合操作中,当我们使用普通类作为 Set 的元素时,即使两个对象的内容完全相同,Set 也可能无法正确识别它们是重复的。这是因为 Set 依赖对象的 hashCodeequals 方法来判断元素是否重复,而普通类默认的这两个方法是基于对象引用的。

一、case class 的定义和基本语法

定义

case class 是 Scala 中一种特殊的类,它用于创建不可变的数据容器。它被设计为轻量级的数据承载者,特别适合用于模式匹配和不可变数据建模。

语法

定义 case class 的语法非常简单明了:

case class ClassName(parameter1: Type1, parameter2: Type2, ...)

例如,我们要创建一个学生的 case class:

case class Student(name: String, age: Int, grade: String)

好处

  1. 自动方法生成:编译器自动生成常用方法,减少样板代码
  2. 不可变性:默认提供不可变特性,更适合函数式编程
  3. 模式匹配友好:天然支持模式匹配操作
  4. 值语义:基于值的相等性判断

二、继承的好处之复用代码

示例

让我们通过两个具体的例子来对比普通类和 case class 在实现相同功能时的差异,特别是在集合操作中的表现。

案例:普通类实现相等性判断

代码

package caseclass

import scala.collection.mutable

object caseclass1 {
  class Book(val id: Int, val name: String) {
    override def equals(obj: Any): Boolean = {
      val other = obj.asInstanceOf[Book]
      other.id == id && other.name == name
    }
  }

  def main(args: Array[String]): Unit = {
    val set1: mutable.Set[Int] = mutable.Set(1, 2)

    set1 += 1
    set1 += 1
    set1 += 1
    set1 += 1
    println(set1)

    val book1 = new Book(1, "西游记")
    val book2 = new Book(1, "西游记")

    println(book1 == book2)

    val set2: mutable.Set[Book] = mutable.Set()
    set2 += book1
    set2 += book2
    println(set2)
  }
}

结果展示

3601.png

代码分析

  1. 普通类的局限性:这个例子展示了普通类在使用时需要手动重写方法的问题
  2. 不完整的相等性实现:虽然我们重写了 equals 方法来判断两个 Book 对象是否相等,但没有重写 hashCode 方法
  3. Set 去重失败的原因:Scala 的 Set 在判断元素是否重复时,会同时使用 hashCodeequals 方法。由于 hashCode 方法没有被重写,两个内容相同的 Book 对象产生了不同的哈希码
  4. 结果解释:控制台输出显示 Set 中确实有两个元素,虽然打印出来只显示了一个引用地址,但实际上它们被视为不同的对象

继承的特点

这个案例突显了普通类在实现值语义时的复杂性:

  • 必须手动实现多个方法,工作量大
  • 容易出错,特别是 hashCodeequals 需要保持一致
  • 代码重复,每个类都需要类似的样板代码

三、case class 的特点和方法重写

格式

case class 通过简洁的语法自动完成了很多工作:

case class 类名(参数列表)

案例:使用 case class 简化实现

代码

package caseclass

import scala.collection.mutable

object caseclass2 {
  case class Book(val id: Int, val name: String){}

  def main(args: Array[String]): Unit = {

    val book1 = new Book(1, "西游记")
    println(book1)
    val book2 = Book(1, "西游记")
    println(book1 == book2)
    
    val set2: mutable.Set[Book] = mutable.Set()
    set2 += book1
    set2 += book2
    println(set2)
  }
}

结果展示

3602.png

代码分析

  1. 语法简洁性:case class 的定义非常简洁,一行代码就完成了类的定义
  2. 自动方法生成:编译器自动为 Book 类生成了:
    • toString 方法:提供格式化的字符串表示
    • equals 方法:基于所有属性值的比较
    • hashCode 方法:基于所有属性值的哈希计算
    • apply 方法:工厂方法,允许省略 new 关键字
  3. 省略 new 关键字Book(1, "西游记") 这种创建方式更加简洁
  4. 正确的集合行为:由于 equalshashCode 都被正确实现,Set 能够正确识别重复元素
  5. 结果验证:Set 中只包含一个元素,证明去重功能正常工作

案例:case class 完整特性演示

代码

package caseclass

object caseclass3 {

  case class Student (name:String, age:Int)

  def main(args: Array[String]): Unit = {
    // val st1 = new Student("小花", 18)
    // 可以省略 new
    val st1 = Student("小花", 18)
    val st2 = Student("小花", 18)
    // 属性默认是不可变的。val 修饰
    // st1.name = "小花花"

    println(st1)
    println(st1 == st2)
  }
}

结果展示

3603.png

代码分析

  1. 不可变性:case class 的参数默认为 val 类型,创建后不能修改
  2. 省略 new 的便利:使用伴生对象的 apply 方法创建实例,代码更简洁
  3. 自动的 toString:自动生成的 toString 方法提供了清晰的对象表示
  4. 基于值的相等性equals 方法比较的是所有属性的值,而不是对象引用
  5. 其他隐含特性
    • 自动生成 unapply 方法,支持模式匹配
    • 提供 copy 方法,用于创建修改后的新实例
    • 自动生成伴生对象

四、case class 与普通 class 的区别

区别1:更加轻量级,特别适合用于定义数据

case class 专门为承载数据而设计,它去掉了普通类中可能不需要的复杂性,专注于数据的存储和基本操作。

区别2:内置很多方法

case class 自动提供了以下方法,而普通类需要手动实现:

  • toString():友好的字符串表示
  • equals()hashCode():基于值的比较
  • copy():创建修改副本
  • apply():工厂方法
  • unapply():模式匹配支持

五、总结

  1. case class 的定义语法是什么?

    • 答:case class 类名(参数1: 类型, 参数2: 类型, ...)
  2. case class 的特点有哪些?

    • 答:主要特点包括:
      • 可以省略 new 关键字创建实例
      • 属性默认是不可变的(val 修饰)
      • 自动实现 toStringequalshashCode 等方法
      • 支持模式匹配
      • 提供 copy 方法