10 类型参数
笔记衔接前序《Scala 隐式转换与 Java 交互》,严格匹配第 10 章课程体系,针对有 Java 泛型基础的大数据专业大学生设计,全程Java 语法对比 + 课程配套源码逐行拆解 + Spark 源码应用 + 避坑指南 + 作业全解析。类型参数(泛型)是 Scala 高级语法的核心,Spark/Flink 源码中 80% 以上的 API 都使用了泛型与型变,是后续学习大数据框架的必备基础。
- 前置知识:Java 泛型、Scala 面向对象、隐式转换、集合体系
- 开发环境:JDK1.8+、Scala2.12.x、IDEA + Scala 插件、Maven3.3.9+
- 配套源码:课程第 10 章所有示例代码(RunCompare.scala、RunTypeVarible.scala 等)
核心学习目标:
- 理解 Scala 泛型的核心思想,掌握泛型类、泛型特质、泛型函数的用法
- 熟练掌握 4 种泛型界定(上下界、视图、上下文、多重)的区别与应用
- 彻底搞懂 Scala 型变(不变、协变、逆变)的原理与使用场景
- 掌握泛型通配符的用法,理解 Java 泛型擦除问题
- 独立完成课程配套作业,掌握泛型在实际开发中的应用技巧
第一章 Scala 泛型基础
1.1 泛型概述
泛型即参数化类型,将操作的数据类型作为参数传递,实现代码复用与类型安全。
为什么需要泛型?
在没有泛型的时代,集合可以存放任意类型的对象,会导致两个严重问题:
- 运行时类型转换异常:存入 Integer 却当作 String 取出
- 强制类型转换繁琐:每次取出元素都需要强制类型转换
泛型的核心价值:
- 编译时类型检查:在编译阶段就发现类型错误,避免运行时异常
- 代码复用:一套代码可以处理多种数据类型
- 消除强制类型转换:代码更简洁、可读性更高
Scala 与 Java 泛型语法对比
| 特性 | Java 语法 | Scala 语法 | 说明 |
|---|---|---|---|
| 泛型声明 | <> | [] | 唯一语法差异,其他语义基本一致 |
| 泛型类 | class Apple<T> | class Apple[T] | T 表示类型参数 |
| 泛型接口 | interface Fruit<T> | trait Fruit[T] | Scala 特质对应 Java 接口 |
| 泛型方法 | <T> void add(T t) | def add[T](t: T): Unit | Scala 泛型参数放在方法名后 |
1.2 泛型类与泛型特质(对应 RunCTrait.scala)
泛型类和泛型特质是指在类或特质的定义中声明类型参数,类或特质的成员可以使用这些类型参数。
课程源码拆解
package Scala10
// 泛型类:支持两个类型参数 T1、T2
class Animal[T1, T2](var name: T1, var age: T2) {
// 方法返回值使用类型参数 T1
def getName(): T1 = name
// 方法返回值使用类型参数 T2
def getAge(): T2 = age
// 方法参数使用类型参数 T1
def setName(name: T1): Unit = this.name = name
// 方法参数使用类型参数 T2
def setAge(age: T2): Unit = this.age = age
}
// 子类继承泛型类,指定具体类型参数
class Dog(name: String, age: Int) extends Animal[String, String]("animal", "99") {
// 重写方法,返回值类型与父类一致
override def getAge(): String = "88"
}
// 泛型特质
trait LanguageMap[A, B] {
def getLanguage(key: A): B
}
// 实现泛型特质,指定具体类型参数
class BigData extends LanguageMap[String, String] {
val map = Map("spark" -> "scala", "hadoop" -> "java", "kafka" -> "scala")
override def getLanguage(key: String): String = map.getOrElse(key, "c")
}
object RunCTrait {
def main(args: Array[String]): Unit = {
// 创建泛型类实例,自动推断类型参数
val animal = new Animal("wangwang", 5)
println(s"age=${animal.getAge()}, type=${animal.getAge().getClass.getSimpleName}") // age=5, type=Integer
println(s"name=${animal.getName()}, type=${animal.getName().getClass.getSimpleName}") // name=wangwang, type=String
// 调用子类方法
val d = new Dog("dog1", 100)
println(s"dog age=${d.getAge()}") // dog age=88
// 调用泛型特质实现类
val language = new BigData()
println(language.getLanguage("hadoop")) // java
}
}✨ 大数据应用:Spark 中
RDD[T]、DataFrame、Dataset[T]都是泛型类,T表示 RDD 中元素的类型。
1.3 泛型函数(对应 RunFunction.scala)
泛型函数是指在函数定义中声明类型参数,函数的参数或返回值可以使用这些类型参数。
课程源码拆解
package Scala10
import scala.collection.mutable.ListBuffer
object RunFunction {
def main(args: Array[String]): Unit = {
// 方式1:自动推断类型参数
add(2, "scala") // x=2, type=Integer, y=scala, type=String
add("java", "hadoop") // x=java, type=String, y=hadoop, type=String
// 方式2:显式指定类型参数
add[String, Int]("flume", 3) // x=flume, type=String, y=3, type=Integer
// 泛型函数实现数组拷贝
val c = copy(Array("123", "java"), ListBuffer[String]())
println(c) // ListBuffer(123, java)
}
// 泛型函数:支持两个类型参数 S、T
def add[S, T](x: S, y: T): Unit = {
println(s"x=$x, type=${x.getClass.getSimpleName}, y=$y, type=${y.getClass.getSimpleName}")
}
// 泛型函数:数组拷贝,输入和输出元素类型必须一致
def copy[T](arr: Array[T], lb: ListBuffer[T]): ListBuffer[T] = {
arr.foreach(x => lb += x)
lb
}
}💡 技巧:Scala 编译器通常可以自动推断类型参数,无需显式指定,只有在推断失败时才需要手动指定。
1.4 泛型通配符(对应 RunSymbol.scala)
泛型通配符用于表示 "任意类型",Scala 提供了两种写法:
- 简写形式:
_(相当于 Java 的?) - 完整形式:
T forSome { type T }
课程源码拆解
package Scala10
object RunSymbol {
def main(args: Array[String]): Unit = {
// 通配符使用示例
printAll(List[String]("spark", "flink"))
printAll(List[Int](2, 3))
// 简写形式
printAllsimple(List[String]("hadoop", "hbase"))
// 多参数通配符
printMap(Map[String, String]("spark" -> "Scala", "flink" -> "java"))
printMap(Map[String, Int]("spark" -> 2, "flink" -> 1))
}
// 完整形式:List[T] forSome { type T }
def printAll(list: List[T] forSome { type T }): Unit = {
list.foreach(x => print(s"x=$x, type=${x.getClass.getSimpleName} ,"))
println()
}
// 简写形式:List[_],与上面完全等价
def printAllsimple(list: List[_]): Unit = {
list.foreach(x => print(s"x=$x, type=${x.getClass.getSimpleName} ,"))
println()
}
// 多参数通配符:Map[_, _]
def printMap(map: Map[_, _]): Unit = {
map.foreach(x => println(s"key=${x._1}, keyType=${x._1.getClass.getSimpleName}, value=${x._2}, valueType=${x._2.getClass.getSimpleName}"))
}
}1.5 Java 泛型擦除问题(对应 ListTest.java)
Java 和 Scala 的泛型都是伪泛型,在编译阶段会进行类型擦除,泛型信息在运行时会被擦除为 Object 类型。
课程源码拆解
package com.shf.scala10;
import java.util.ArrayList;
import java.util.List;
public class ListTest {
public static void main(String[] args) {
ArrayList<String> sList = new ArrayList<>();
ArrayList<Integer> iList = new ArrayList<>();
// 运行时 sList 和 iList 的类型完全相同,都是 ArrayList
System.out.println(sList.getClass()); // class java.util.ArrayList
System.out.println(iList.getClass()); // class java.util.ArrayList
System.out.println(iList.getClass() == sList.getClass()); // true
// 编译错误:无法使用 instanceof 判断泛型类型
// Object ob = sList instanceof ArrayList<String> ? null : null;
}
}
class Person<T> {
// 编译错误:静态成员不能使用泛型类型参数
// static T name;
public void setName(T name) {
}
}⚠️ 避坑指南:
- 泛型类型参数不能用于静态成员
- 运行时无法获取泛型的具体类型
- 不能创建泛型数组(
new T[10]编译错误)
第二章 Scala 泛型界定(重点)
泛型界定用于限制泛型类型参数的范围,确保只有符合条件的类型才能作为类型参数传入。Scala 提供了 4 种泛型界定:上下界界定、视图界定、上下文界定、多重界定。
2.1 上下界界定
上下界界定是最基础的泛型界定,用于限制类型参数的继承关系。
上界界定(T <: U)
定义:泛型类型参数 T 必须是 U 或 U 的子类(相当于 Java 的 ? extends U)。
下界界定(T >: L)
定义:泛型类型参数 T 必须是 L 或 L 的父类(相当于 Java 的 ? super L)。
课程源码拆解(对应 RunCompare.scala)
package Scala10
object RunCompare {
def main(args: Array[String]): Unit = {
// String 实现了 Comparable[String],符合上界要求
val r1 = compare("1", "2")
println(s"r1=$r1") // r1=2
// Person 实现了 Comparable[Person],符合上界要求
val bigperson = compare(Person("java", 30), Person("scala", 5))
println(s"bigperson=$bigperson") // bigperson=Person(java,30)
}
// 上界界定:T 必须是 Comparable[T] 的子类
def compare[T <: Comparable[T]](x: T, y: T): T = {
if (x.compareTo(y) > 0) x else y
}
}
// Person 类实现 Comparable 接口,满足上界要求
case class Person(var name: String, var age: Int) extends Comparable[Person] {
override def compareTo(o: Person): Int = {
if (this.age > o.age) 1
else if (this.age == o.age) 0
else -1
}
}⚠️ 注意:
compare(4, 5)会编译错误!因为 Int 类型没有直接实现 Comparable [Int] 接口,需要使用视图界定解决。
2.2 视图界定(T <% V)
定义:泛型类型参数 T 可以隐式转换为 V 类型(不仅限于继承关系)。
视图界定解决了上界界定的局限性:即使 T 没有直接继承 V,只要存在隐式转换可以将 T 转换为 V,就符合要求。
课程源码拆解(对应 RunCompare.scala)
package Scala10
object RunCompare {
def main(args: Array[String]): Unit = {
// Int 可以隐式转换为 RichInt,RichInt 实现了 Comparable[Int]
val r2 = compareWithInt(4, 5)
println(s"r2=$r2") // r2=5
}
// 视图界定:T 可以隐式转换为 Comparable[T]
def compareWithInt[T <% Comparable[T]](x: T, y: T): T = {
if (x.compareTo(y) > 0) x else y
}
}✨ 原理:Scala 预定义了隐式转换
int2RichInt,可以将 Int 转换为 RichInt,而 RichInt 实现了 Comparable [Int] 接口,因此compareWithInt(4, 5)可以正常运行。
⚠️ 注意:视图界定在 Scala 2.13 中已被废弃,推荐使用上下文界定代替。
2.3 上下文界定(T : M)
定义:在当前作用域内,必须存在一个 M[T] 类型的隐式值(与隐式转换无关)。
上下文界定是 Scala 泛型最常用的界定方式,广泛应用于排序、序列化等场景。
课程源码拆解(对应 RunExternalCom.scala)
package Scala10
object RunExternalCom {
def main(args: Array[String]): Unit = {
// 定义隐式值:Person 类型的比较器
implicit val po = new pOdering
val p = new Pair[Person](Person(2), Person(10))
// 方式1:手动传递比较器
println(p.com(new pOdering)) // Person(10)
// 方式2:自动注入隐式值
println(p.com) // Person(10)
}
// 上下文界定:必须存在 Ordering[T] 类型的隐式值
class Pair[T: Ordering](val x: T, val y: T) {
// 隐式参数:接收 Ordering[T] 类型的比较器
def com(implicit ord: Ordering[T]): T = {
if (ord.compare(x, y) > 0) x else y
}
}
case class Person(val age: Int)
// 自定义 Person 类型的比较器
class pOdering extends Ordering[Person] {
override def compare(x: Person, y: Person): Int = {
if (x.age > y.age) 1 else -1
}
}
}✨ 大数据应用:Spark 中的
sortBy、sortByKey算子就是通过上下文界定Ordering实现的。
2.4 多重界定
多重界定用于同时满足多个界定条件,Scala 支持多种形式的多重界定:
| 多重界定类型 | 语法 | 说明 |
|---|---|---|
| 多重视图界定 | T <% V1 <% V2 | T 可以同时隐式转换为 V1 和 V2 |
| 多重上下文界定 | T : M1 : M2 | 同时存在 M1 [T] 和 M2 [T] 类型的隐式值 |
| 上下界多重界定 | T >: L <: U | T 必须是 L 的父类且是 U 的子类 |
| 多特质界定 | T <: A with B | T 必须同时实现 A 和 B 特质 |
课程源码拆解(对应 RunMulti.scala)
package Scala10
object RunMulti extends App {
class A[T]
class B[T]
// 定义隐式值
implicit val a = new A[String]
implicit val b = new B[String]
// 多重上下文界定:同时存在 A[T] 和 B[T] 类型的隐式值
def test1[T: A: B](x: T): Unit = {
println(x)
}
test1("test1") // 输出:test1
// 定义隐式转换
implicit def tToA[T](x: T) = new A[T]
implicit def tToB[T](x: T) = new B[T]
// 多重视图界定:T 可以同时隐式转换为 A[T] 和 B[T]
def test2[T <% A[T] <% B[T]](x: T) = {
println(x)
}
test2("test2") // 输出:test2
}第三章 Scala 型变(难点)
型变是 Scala 泛型最独特、最难理解的特性,用于描述泛型类型之间的继承关系。Java 泛型是不变的,而 Scala 支持三种型变:不变、协变、逆变。
3.1 型变概述
假设存在类型继承关系:Student extends Person,那么泛型类型 Container[Student] 和 Container[Person] 之间的继承关系由型变决定:
| 型变类型 | 语法 | 继承关系 | 说明 |
|---|---|---|---|
| 不变 | Container[T] | Container[Student] 与 Container[Person] 无继承关系 | Java 泛型默认是不变的 |
| 协变 | Container[+T] | Container[Student] 是 Container[Person] 的子类 | 泛型的继承方向与类型参数一致 |
| 逆变 | Container[-T] | Container[Person] 是 Container[Student] 的子类 | 泛型的继承方向与类型参数相反 |
3.2 不变(默认)
定义:当类型 S 是类型 T 的子类型时,A[S] 与 A[T] 不存在继承关系。
Scala 泛型默认是不变的,与 Java 泛型行为一致。
课程源码拆解(对应 RunTypeVarible.scala)
package Scala10
object RunTypeVarible extends App {
// 不变泛型类
class Container[T](value: T) {
private var _value = value
def getValue(): T = _value
def setValue(value: T): Unit = _value = value
}
abstract class Person10 {
def name: String
}
case class Student10(name: String) extends Person10
case class Teacher10(name: String) extends Person10
val s1: Person10 = new Student10("s101") // 正常:Student10 是 Person10 的子类
val s2: Container[Student10] = new Container[Student10](Student10("stu02"))
// 编译错误:Container[Student10] 不是 Container[Person10] 的子类(不变)
// val c2: Container[Person10] = s2
}3.3 协变(+T)
定义:当类型 S 是类型 T 的子类型时,A[S] 是 A[T] 的子类型。
协变最典型的应用是 Scala 不可变集合,例如 List[+A],因此 List[String] 是 List[Any] 的子类。
课程源码示例
// Scala 标准库 List 是协变的
val students: List[Student10] = List(Student10("stu05"), Student10("stu06"))
val teachers: List[Teacher10] = List(Teacher10("tea05"), Teacher10("tea06"))
// 正常:List[Student10] 是 List[Person10] 的子类(协变)
def printAll(persons: List[Person10]): Unit = {
persons.foreach(person => println(person.name))
}
printAll(students) // 正常运行
printAll(teachers) // 正常运行3.4 逆变(-T)
定义:当类型 S 是类型 T 的子类型时,A[T] 是 A[S] 的子类型。
逆变通常用于消费者类型,即处理类型 T 的对象,例如打印机、比较器等。
课程源码拆解(对应 RunTypeVarible.scala)
package Scala10
object RunTypeVarible extends App {
abstract class Person10 {
def name: String
}
case class Student10(name: String) extends Person10
case class Teacher10(name: String) extends Person10
// 逆变泛型类:打印机
abstract class Printer[-T] {
def printName(value: T): Unit
}
// Person 打印机:可以打印所有 Person 对象
class PersonPrinter extends Printer[Person10] {
override def printName(value: Person10): Unit = {
println(s"person name is ${value.name}")
}
}
// Student 打印机:只能打印 Student 对象
class StudentPrinter extends Printer[Student10] {
override def printName(value: Student10): Unit = {
println(s"student name is ${value.name}")
}
}
// 方法参数是 Printer[Student10] 类型
def printStudent(value: Printer[Student10]): Unit = {
value.printName(Student10("studentprinter"))
}
val stuprinter = new StudentPrinter
val perprinter = new PersonPrinter
printStudent(stuprinter) // 正常:StudentPrinter 是 Printer[Student10]
// 正常:PersonPrinter 是 Printer[Person10],而 Printer[Person10] 是 Printer[Student10] 的子类(逆变)
printStudent(perprinter)
}💡 理解逆变:
PersonPrinter可以打印任何 Person 对象,当然也可以打印 Student 对象- 因此
PersonPrinter可以替代StudentPrinter使用- 所以
Printer[Person10]是Printer[Student10]的子类
3.5 型变注意事项与解决方案
协变注意事项
问题:协变类型参数 +T 不能直接出现在方法参数的位置(会违背里氏替换原则)。
// 编译错误:协变类型 T 出现在方法参数位置
class P[+A] {
def method(x: A): Unit = {}
}解决方案:将方法参数类型设置为 T 的父类型(下界界定)。
// 正确写法:使用下界界定 R >: A
class P[+A] {
def method[R >: A](x: R): Unit = {}
}逆变注意事项
问题:逆变类型参数 -T 不能直接出现在方法返回值的位置。
解决方案:将方法返回值类型设置为 T 的子类型(上界界定)。
第四章 课程配套作业全解析
对应课程第 10 章课后作业,所有代码与课程要求完全一致,可直接复制运行。
一、简答题
1. 为什么 RichInt 实现的是 Comparable [Int] 而不是 Comparable [RichInt]?
答:
- 性能考虑:RichInt 是 Int 的包装类,实现 Comparable [Int] 可以直接与原生 Int 类型进行比较,无需频繁拆箱装箱,性能更高。
- 隐式转换设计:Scala 预定义了隐式转换
int2RichInt,可以将 Int 自动转换为 RichInt。如果 RichInt 实现 Comparable [RichInt],那么3.compareTo(5)需要先将两个 Int 都转换为 RichInt,而实现 Comparable [Int] 只需要转换一个。 - 接口通用性:实现 Comparable [Int] 使得 RichInt 可以与任何 Int 类型的值进行比较,包括原生 Int 和其他包装类,通用性更强。
2. 查看 Iterable [+A] 特质。哪些方法使用了类型参数 A?为什么在这些方法中类型参数位于协变点?
答:
- Iterable [+A] 中使用类型参数 A 的方法:
def iterator: Iterator[A]:返回 A 类型的迭代器def head: A:返回第一个元素def last: A:返回最后一个元素def take(n: Int): Iterable[A]:返回前 n 个元素def filter(p: A => Boolean): Iterable[A]:过滤元素def map[B](f: A => B): Iterable[B]:映射元素
- 为什么类型参数位于协变点:
- 协变点是指方法返回值的位置或函数参数的参数位置(即函数的输入类型)。
- 上面的方法中,A 要么出现在返回值位置(如
head: A),要么出现在函数参数的输入位置(如filter(p: A => Boolean)中的 A 是函数的输入类型)。 - 这些位置都符合协变的要求:当
Student是Person的子类时,Iterable[Student]的这些方法返回的Student对象可以安全地赋值给Person类型的变量,不会出现类型错误。
二、编程题
1. 定义一个不可变类 Pair [T,S],带一个 swap 方法,返回组件交换过位置的新对偶。
// 不可变泛型类 Pair
case class Pair[T, S](first: T, second: S) {
// swap 方法:返回新的 Pair,交换 first 和 second 的位置
def swap: Pair[S, T] = Pair(second, first)
}
object Homework1 {
def main(args: Array[String]): Unit = {
val p1 = Pair("scala", 10)
println(p1) // Pair(scala,10)
val p2 = p1.swap
println(p2) // Pair(10,scala)
}
}2. 定义一个可变类 Pair [T],带一个 swap 方法,交换对偶中组件的位置。
// 可变泛型类 Pair
class Pair[T](var first: T, var second: T) {
// swap 方法:交换 first 和 second 的值
def swap(): Unit = {
val temp = first
first = second
second = temp
}
override def toString: String = s"Pair($first, $second)"
}
object Homework2 {
def main(args: Array[String]): Unit = {
val p = new Pair(1, 2)
println(p) // Pair(1, 2)
p.swap()
println(p) // Pair(2, 1)
}
}3. 给定可变类 Pair [S, T],使用类型约束定义一个 swap 方法,当类型参数相同时可以被调用。
// 可变泛型类 Pair[S, T]
class Pair[S, T](var first: S, var second: T) {
// swap 方法:使用类型约束 S =:= T,只有当 S 和 T 类型相同时才能调用
def swap(implicit ev: S =:= T): Unit = {
// 类型转换:将 second 转换为 S 类型
val temp = first
first = second.asInstanceOf[S]
second = temp.asInstanceOf[T]
}
override def toString: String = s"Pair($first, $second)"
}
object Homework3 {
def main(args: Array[String]): Unit = {
// 类型相同,可以调用 swap
val p1 = new Pair(1, 2)
println(p1) // Pair(1, 2)
p1.swap()
println(p1) // Pair(2, 1)
// 类型不同,编译错误:无法找到隐式值 String =:= Int
// val p2 = new Pair("scala", 10)
// p2.swap()
}
}💡 说明:
=:=是 Scala 预定义的类型约束,用于证明两个类型相等,只有当 S 和 T 是相同类型时,编译器才会生成对应的隐式值。
学习总结与最佳实践
- 泛型基础:Scala 泛型语法使用
[],与 Java<>语义基本一致,支持泛型类、泛型特质、泛型函数。Java 和 Scala 泛型都是伪泛型,存在类型擦除问题。 - 泛型界定:
- 上下界:限制继承关系(
T <: U、T >: L) - 视图界定:基于隐式转换(
T <% V,已废弃) - 上下文界定:基于隐式值(
T : M,最常用) - 多重界定:同时满足多个条件
- 上下界:限制继承关系(
- 型变核心:
- 不变:默认行为,无继承关系
- 协变(
+T):生产者类型(产生 T 类型的对象),如不可变集合 - 逆变(
-T):消费者类型(处理 T 类型的对象),如打印机、比较器
- 大数据应用:Spark 中
RDD[+T]是协变的,Ordering[T]上下文界定用于排序,Function1[-T1, +R]是逆变 + 协变的函数类型。 - 最佳实践:
- 优先使用上下文界定代替视图界定
- 不可变集合使用协变,可变集合使用不变
- 消费者类型使用逆变,生产者类型使用协变
- 避免滥用型变,保持代码可读性