类型参数
一、泛型
Scala 支持类型参数化,使得我们能够编写泛型程序。
1.1 泛型类
Java 中使用 <>
符号来包含定义的类型参数,Scala 则使用 []
。
1 | class Pair[T, S](val first: T, val second: S) { |
1 | object ScalaApp extends App { |
1.2 泛型方法
函数和方法也支持类型参数。
1 | object Utils { |
二、类型限定
2.1 类型上界限定
Scala 和 Java 一样,对于对象之间进行大小比较,要求被比较的对象实现 java.lang.Comparable
接口。所以如果想对泛型进行比较,需要限定类型上界为 java.lang.Comparable
,语法为 S <: T
,代表类型 S 是类型 T 的子类或其本身。示例如下:
1 | // 使用 <: 符号,限定 T 必须是 Comparable[T]的子类型 |
1 | // 测试代码 |
扩展:如果你想要在 Java 中实现类型变量限定,需要使用关键字 extends 来实现,等价的 Java 代码如下:
1
2
3
4
5
6
7
8
9
10
11
12 >public class Pair<T extends Comparable<T>> {
> private T first;
> private T second;
> Pair(T first, T second) {
> this.first = first;
> this.second = second;
> }
> public T smaller() {
> return first.compareTo(second) < 0 ? first : second;
> }
>}
>
2.2 视图界定
在上面的例子中,如果你使用 Int 类型或者 Double 等类型进行测试,点击运行后,你会发现程序根本无法通过编译:
1 | val pair1 = new Pair(10, 12) |
之所以出现这样的问题,是因为 Scala 中的 Int 类并没有实现 Comparable 接口。在 Scala 中直接继承 Comparable 接口的是特质 Ordered,它在继承 compareTo 方法的基础上,额外定义了关系符方法,源码如下:
1 | // 除了 compareTo 方法外,还提供了额外的关系符方法 |
之所以在日常的编程中之所以你能够执行 3>2
这样的判断操作,是因为程序执行了定义在 Predef
中的隐式转换方法 intWrapper(x: Int)
,将 Int 类型转换为 RichInt 类型,而 RichInt 间接混入了 Ordered 特质,所以能够进行比较。
1 | // Predef.scala |
要想解决传入数值无法进行比较的问题,可以使用视图界定。语法为 T <% U
,代表 T 能够通过隐式转换转为 U,即允许 Int 型参数在无法进行比较的时候转换为 RichInt 类型。示例如下:
1 | // 视图界定符号 <% |
注:由于直接继承 Java 中 Comparable 接口的是特质 Ordered,所以如下的视图界定和上面是等效的:
1
2
3
4
5 > // 隐式转换为 Ordered[T]
> class Pair[T <% Ordered[T]](val first: T, val second: T) {
> def smaller: T = if (first.compareTo(second) < 0) first else second
> }
>
2.3 类型约束
如果你用的 Scala 是 2.11+,会发现视图界定已被标识为废弃。官方推荐使用类型约束 (type constraint) 来实现同样的功能,其本质是使用隐式参数进行隐式转换,示例如下:
1 | // 1.使用隐式参数隐式转换为 Comparable[T] |
当然,隐式参数转换也可以运用在具体的方法上:
1 | object PairUtils{ |
2.4 上下文界定
上下文界定的形式为 T:M
,其中 M 是一个泛型,它要求必须存在一个类型为 M[T]的隐式值,当你声明一个带隐式参数的方法时,需要定义一个隐式默认值。所以上面的程序也可以使用上下文界定进行改写:
1 | class Pair[T](val first: T, val second: T) { |
在上面的示例中,我们无需手动添加隐式默认值就可以完成转换,这是因为 Scala 自动引入了 Ordering[Int]这个隐式值。为了更好的说明上下文界定,下面给出一个自定义类型的比较示例:
1 | // 1.定义一个人员类 |
2.5 ClassTag上下文界定
这里先看一个例子:下面这段代码,没有任何语法错误,但是在运行时会抛出异常:Error: cannot find class tag for element type T
, 这是由于 Scala 和 Java 一样,都存在类型擦除,即泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉。对于下面的代码,在运行阶段创建 Array 时,你必须明确指明其类型,但是此时泛型信息已经被擦除,导致出现找不到类型的异常。
1 | object ScalaApp extends App { |
Scala 针对这个问题,提供了 ClassTag 上下文界定,即把泛型的信息存储在 ClassTag 中,这样在运行阶段需要时,只需要从 ClassTag 中进行获取即可。其语法为 T : ClassTag
,示例如下:
1 | import scala.reflect._ |
2.6 类型下界限定
2.1 小节介绍了类型上界的限定,Scala 同时也支持下界的限定,语法为:U >: T
,即 U 必须是类型 T 的超类或本身。
1 | // 首席执行官 |
2.7 多重界定
类型变量可以同时有上界和下界。 写法为 :
T > : Lower <: Upper
;不能同时有多个上界或多个下界 。但可以要求一个类型实现多个特质,写法为 :
T < : Comparable[T] with Serializable with Cloneable
;你可以有多个上下文界定,写法为
T : Ordering : ClassTag
。
三、Ordering & Ordered
上文中使用到 Ordering 和 Ordered 特质,它们最主要的区别在于分别继承自不同的 Java 接口:Comparable 和 Comparator:
- Comparable:可以理解为内置的比较器,实现此接口的对象可以与自身进行比较;
- Comparator:可以理解为外置的比较器;当对象自身并没有定义比较规则的时候,可以传入外部比较器进行比较。
为什么 Java 中要同时给出这两个比较接口,这是因为你要比较的对象不一定实现了 Comparable 接口,而你又想对其进行比较,这时候当然你可以修改代码实现 Comparable,但是如果这个类你无法修改 (如源码中的类),这时候就可以使用外置的比较器。同样的问题在 Scala 中当然也会出现,所以 Scala 分别使用了 Ordering 和 Ordered 来继承它们。
下面分别给出 Java 中 Comparable 和 Comparator 接口的使用示例:
3.1 Comparable
1 | import java.util.Arrays; |
3.2 Comparator
1 | import java.util.Arrays; |
使用外置比较器还有一个好处,就是你可以随时定义其排序规则:
1 | // 按照年龄大小排序 |
3.3 上下文界定的优点
这里再次给出上下文界定中的示例代码作为回顾:
1 | // 1.定义一个人员类 |
使用上下文界定和 Ordering 带来的好处是:传入 Pair
中的参数不一定需要可比较,只要在比较时传入外置比较器即可。
需要注意的是由于隐式默认值二义性的限制,你不能像上面 Java 代码一样,在同一个上下文作用域中传入两个外置比较器,即下面的代码是无法通过编译的。但是你可以在不同的上下文作用域中引入不同的隐式默认值,即使用不同的外置比较器。
1 | implicit val ImpPersonOrdering = new PersonOrdering |
四、通配符
在实际编码中,通常需要把泛型限定在某个范围内,比如限定为某个类及其子类。因此 Scala 和 Java 一样引入了通配符这个概念,用于限定泛型的范围。不同的是 Java 使用 ?
表示通配符,Scala 使用 _
表示通配符。
1 | class Ceo(val name: String) { |
目前 Scala 中的通配符在某些复杂情况下还不完善,如下面的语句在 Scala 2.12 中并不能通过编译:
1 | def min[T <: Comparable[_ >: T]](p: Pair[T]) ={} |
可以使用以下语法代替:
1 | type SuperComparable[T] = Comparable[_ >: T] |
参考资料
- Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1
- 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7