泛型让类型带参数;型变(variance) 规定"元素有子类型关系时,容器之间是什么关系"。
+A协变、-A逆变、无标注不变——这一节是主峰的第一段岩缝,也是绝大多数人卡壳的地方。
class Stack[A]: private var elems: List[A] = Nil def push(a: A): Unit = elems = a :: elems def pop(): Option[A] = elems match case Nil => None case h :: t => elems = t; Some(h) val s = Stack[Int]
方法也可以带类型参数:def first[A](xs: List[A]): Option[A] = xs.headOption。
设 Student <: Person(学生是人)。直觉说"学生栈当然也是人栈",但类型系统必须看你怎么用 A:
List[Student] 当 List[Person] 用安全。// 协变的正确性论证 val students: List[Student] = List(alice, bob) val people: List[Person] = students // OK,List 是 +A people.head.greet() // 取出来当 Person 用,安全 // 假设 Stack 可变且协变会发生什么 val stack: Stack[Person] = Stack[Student]() // 假设允许 stack.push(teacher: Person) // 装进了一个老师 val s2 = stack.asInstanceOf[Stack[Student]] s2.pop() // 取出的"学生"其实是老师 —— 类型谎言
Scala 的答案是把决定权交给声明者:class Box[+A]、class Sink[-A]、class Cell[A]。最经典的双重身份是函数:
trait Function1[-T1, +R]: def apply(v: T1): R
参数逆变(能接受更宽的输入)、返回值协变(能给出更窄的输出)——"宽进严出",这是所有类型系统共享的智慧,Java 的 ? super / ? extends 用通配符在调用点表达同一件事,Scala 把它挪到声明点,一次声明处处生效。

def largest[A <: Ordered[A]](xs: List[A]): Option[A] = ??? // ^ 上界:A 必须是 Ordered[A] 的子类型 def enrich[A >: Empty](xs: List[A]): List[A] = ??? // ^ 下界:A 至少要这么宽
上界限定"能力"(必须可比较),下界常用于协变类的方法参数——List[+A] 上定义 def :: [B >: A](b: B): List[B],允许往 List[Student] 前面接一个 Person,结果类型自动放宽为 List[Person]。标准库大量使用这个技巧让协变与灵活性共存。

声明 class Box[+A] 后在里面写一个"装进去"的方法,编译器立刻拦截。把这场对话完整走一遍:
class Box[+A](private var value: A): def put(a: A): Unit = value = a // 编译错误:covariant type A occurs in contravariant position // in type A of parameter a
报错翻译成人话:A 声明为协变(只产出),却在参数位置(消费位)出现,位置冲突。修复有两条正路——用下界把消费位放宽:
class Box2[+A](private var value: A | Null): def put[B >: A](b: B): Unit = value = b.asInstanceOf[A | Null] def get: A = value.asInstanceOf[A] // 标准库的做法:List[+A] 的 prepend 签名正是 def :: [B >: A](b: B): List[B] List(new Student("amy")).::(new Person("boss")) // 结果类型 List[Person]
推演结论:下界 B >: A 让方法"能接受的类型至少像 A 一样宽",消费的不再是 A 本身,协变性得以保全。这是标准库最重要的型变技巧,读懂它,List、Option、Vector 的签名全部透明。
给五个日常类型标注型变,答案附推理:List 是 +A(只读);Function1 是 -T1、+R(吃进宽、吐出窄);Set 是不变(contains 消费元素);Option 是 +A(同 List);Sink/Consumer 类是 -A(只写)。做错最多的通常是 Set——它看起来"像 List",但 contains(A) 这个方法让 A 出现在了消费位。
最后一个易混点是型变与子类型的区别:子类型描述两个类型的关系(Student 是 Person),型变描述两个"容器类型"的关系是否随之成立(List[Student] 是否算 List[Person])。前者是语言给定的,后者是设计者用标注立法的——所以型变标注错了编译器一定会拦,它拦的正是你立的法与你的用法打架。