泛型听起来很高大上,但它其实就是编程里的“占位符”。
Java 程序员刚接触 Scala 泛型时,往往会被里面的 +、-、<: 等符号绕晕。今天咱们用通俗的“快递盒”和“修理店”的例子,把 Scala 泛型最核心的 3 个知识点吃透。
一、 为什么要用泛型?(万能的“快递盒”)
如果没有泛型,当你需要写一个容器来装东西时,如果装衣服,得写一个 ClothingBox;如果装手机,得写一个 PhoneBox。这会导致写出大量重复的垃圾代码。
泛型(Type Parameters)就是“把数据类型当成参数传进来”。在 Scala 中,我们用方括号 [T] 来代表这个尚未确定的万能类型(Java 中用 <T>)。
// 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)。
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] 的儿子。
// 声明为协变 [+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] 的子类?!
别慌,我们把场景从“装东西的箱子”切换到“手机维修店”,逻辑就顺通了:
// 声明为逆变 [-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逆变反向游(万能车修店能修跑车)。