Skip to content

泛型听起来很高大上,但它其实就是编程里的“占位符”。

Java 程序员刚接触 Scala 泛型时,往往会被里面的 +、-、<: 等符号绕晕。今天咱们用通俗的“快递盒”和“修理店”的例子,把 Scala 泛型最核心的 3 个知识点吃透。

一、 为什么要用泛型?(万能的“快递盒”) ​

如果没有泛型,当你需要写一个容器来装东西时,如果装衣服,得写一个 ClothingBox;如果装手机,得写一个 PhoneBox。这会导致写出大量重复的垃圾代码。

泛型(Type Parameters)就是“把数据类型当成参数传进来”。在 Scala 中,我们用方括号 [T] 来代表这个尚未确定的万能类型(Java 中用 <T>)。

scala
// T 是一个代号,代表“未来的某种类型”
class Box[T](val item: T) {
  def peek(): T = item
}

object GenericDemo extends App {
  // 1. 创建一个装字符串的盒子
  val stringBox = new Box[String]("一件帅气的皮衣")
  
  // 2. 创建一个装整数的盒子(Scala 会自动推导类型,[Int] 可以省略)
  val intBox = new Box(100) 

  // 3. 取出数据:不需要强转类型,拿出来直接就能用,编译期绝对安全
  val gift: String = stringBox.peek()
  println(s"盒子里装的是: $gift")
}

二、 上界与下界(给万能盒子加上“安检标准”) ​

虽然盒子是万能的,但有时候我们不希望别人什么垃圾都往里塞。比如,你做了一个手机充电器,它就只能给“手机”或者“手机的子类(如 iPhone)”充电,不能拿来给“白菜”充电。

这时候,就要用边界(Bounds)来限制泛型 T 的范围。

2.1 上界约束 [T <: 某个类](最多到什么级别) ​

语法 T <: A 意思:T 最多只能到 A 级别。换句话说,T 必须是 A 或者 A 的子类(相当于 Java 的 T extends A)。

Scala
class Phone
class iPhone extends Phone
class Huawei extends Phone
class Brick // 砖头

// 【安检限制】:这个充电类只能接收手机或手机的子类
class BatteryCharger[T <: Phone]

val c1 = new BatteryCharger[iPhone] // ✅ 正常,iPhone 是手机
val c2 = new BatteryCharger[Huawei] // ✅ 正常,华为是手机
// val c3 = new BatteryCharger[Brick]  // 💥 报错!砖头不能充电,被安检拦下

2.2 下界约束 [T >: 某个类](最少是什么级别) ​

语法 T >: B 意思:T 最少得是 B。换句话说,T 必须是 B 或者 B 的父类/祖先类(相当于 Java 的 T super B)。

三、 协变 [+T] 与 逆变 [-T](父子关系能不能连带?) ​

这是 Scala 泛型最核心、最难理解的内容。

已知在面向对象中:iPhone 是 Phone 的子类。那么请问:装满 iPhone 的箱子 Box[iPhone],可以当作装手机的箱子 Box[Phone] 用吗?

Scala 默认是不允许的。但它提供了 + 和 - 符号,让我们能极其优雅地控制这种“连带的父子关系”:

3.1 协变 [+T] —— 顺着父子关系走 ​

如果在方括号里加个 + 号,代表协变。意思是:既然 iPhone 是 Phone 的儿子,那么 Box[iPhone] 连带着也变成了 Box[Phone] 的儿子。

scala
// 声明为协变 [+T]
class LogisticsBox[+T](val item: T)

object TestCovariant extends App {
  val iphoneBox: LogisticsBox[iPhone] = new LogisticsBox(new iPhone)

  // ✅ 编译通过!因为是协变,拿一箱“iPhone”去冒充一箱“手机”送给物流,完全合情合理
  val phoneBox: LogisticsBox[Phone] = iphoneBox
}

适用场景:通常用于只读、提取数据的只读容器(如 Scala 原生的 List[+A])。

3.2 逆变 [-T] —— 把父子关系反过来转! ​

如果在方括号里加个 - 号,代表逆变。这听起来非常违背直觉:Box[Phone] 居然反过来变成了 Box[iPhone] 的子类?!

别慌,我们把场景从“装东西的箱子”切换到“手机维修店”,逻辑就顺通了:

scala
// 声明为逆变 [-T]:这是一个维修店组件
class RepairShop[-T] {
  def fix(item: T): Unit = println("全面拆解并成功修理!")
}

object TestContravariant extends App {
  // 1. 老张开了一家“万能手机维修店”,只要是手机(Phone)都能修
  val generalShop = new RepairShop[Phone]

  // 2. 小李在旁边开了一家“iPhone 专门维修店”
  // ✅ 奇迹时刻:编译通过!老张的“万能手机店”可以直接赋值给小李的“iPhone专修店”
  val iphoneShop: RepairShop[iPhone] = generalShop
  
  // 【大白话逻辑】:
  // 顾客要求这家店“最起码能修 iPhone”。
  // 而老张的店连大范围的“所有手机”都能修,那拿来当“iPhone专修店”用,完全是大材小用、绰绰有余!
  // 所以,全能店(父类容器)完美替代了专修店(子类容器),关系反转成功!
}

适用场景:通常用于传参、消费数据、过滤器或比较器(如 Ordering[-T])。

🎯 4. 入门总结口诀 ​

你可以把这几句口诀记在心里,无论是写代码还是面试,一想就通:

方括号 [T] 传类型,省去强转很轻松。

<: 上界卡住爹,:> 下界圈住儿。

+T 协变顺着走(衣服箱子也是箱子),

-T 逆变反向游(万能车修店能修跑车)。

基于 Vite 强力驱动 | 纯静态轻量托管