Scala泛型中的Liskov哲学Part1.泛型

Part1. 泛型与类型约束

我们在学习 Java 时就已经接触到泛型的相关概念了。当我们不确定某个参数的类型具体是哪个类型,就会使用泛型 E 来代替。泛型最常用在 Collection 集合当中,表示 “这是一个装载什么元素的” 集合。

同时,为了限制这个泛型 E 类型,我们又为泛型制定了上界,下界的概念,又称之为类型约束。在 Scala 中,类型约束的方式除了泛型上下界之外,还可以通过视图界定,和上下文界定来实现,这两种方式主要和之前所学习的隐式转换和隐式值有关联。

此外,设 T1T2 存在继承关系,P1P2 也存在继承关系,设函数 f1 : T2 => P1 ,而 f2 : T1 => P2,我们能否认为函数 f2 能够替代 f1 的功能,或者能否认为 f2f1 的子函数?首先,这在 Scala 中,答案是肯定的。笔者会在后文从里氏替换原则的角度介绍这么理解的缘由。

定义第一个泛型类

Java 使用尖括号 <> (有人也称其为钻石符号)来定义泛型(Scala 中往往称它是 "类型参数",但意思是相同的) ,而 Scala 则使用中括号 [] 表示泛型。我们来尝试第一个例子:定义一个 Message 类,将它内部包含的信息种类定义为泛型。

class Message[E](s: E) {
  def get: E = s
}
复制代码

无论是定义泛型类还是定义泛型方法,都是将 [] 写在参数列表之前。

def getMid[T](list: List[T]): T = {
    list(list.length / 2)
}
复制代码

有关泛型的基本用法,笔者不在此阐述。在这里着重介绍 Scala 的类型约束部分。

类型约束

类型约束分为上界( Upper Bounds ) 和下界( Lower Bounds )。

在 Java 的泛型机制中,如果限定某个泛型 E 是 A 类型的子类型(或者称 A 是泛型 E 的上界),其语法如:<T extends A>。而 Scala 则使用 [E <: A] 来表示( <: 类似于逻辑上的 <= )。 另外,Java 中的 ? 占位符在 Scala 中使用 _ 来表示。

现在,试着使用 Scala 编写一个通用的比较方法 greater,它可以对实现了 Comparable 接口的类实例进行比较。

def compareVal[T <: Comparable[T]](t1: T, t2: T): T = {
  if (t1.compareTo(t2) > 0) t1 else t2
}
复制代码

在主函数中调用此函数,传入两个 java.lang.Integer 类型的实例:

println(
	compareVal(java.lang.Integer.valueOf(32), java.lang.Integer.valueOf(12))
)
复制代码

在这里不使用 Scala 的 Int 类型,因为 Scala 中的 AnyVal 类型没有实现 Comparable 接口(这是 Java 范畴的内容,Scala 使用 Ordered 特质)。不过,Scala 提供了从 AnyVal 到 Java 包装类的隐式转换函数,而 Java 包装类是实现了 Comparable 接口的。因此,我们在调用这个函数时,需要用下面的写法表示在调用此方法之前,将 Scala 的 Int 类型入参全部隐式转换为 java.lang.Integer 类型。

compareVal[java.lang.Integer](31,21)
复制代码

下界与上转型对象

在 Java 的泛型机制中,如果限定某个泛型 E 是 A 类型的父类型(或者称 A 是泛型 E 的下界),其语法如: <T super A>。Scala 使用 [E >: A] 来表示(>: 类似于逻辑上的 >=)。

使用下界有时会引发直观上难以接受的现象。我们首先声明四个类:动物类( Animal ),鸟类( Bird ),鹦鹉类( Parrot )。此三者拥有继承关系,另外还有一个无关的车类( Car )。

class Animal {
  def sound(): Unit = println("this is an animal")
}

class Bird extends Animal {
  override def sound(): Unit = println("this is a bird")
  def fly(): Unit = println("This bird is flying...")
}

class Parrot extends Bird{
  override def sound(): Unit = println("this is a parrot")
  override def fly(): Unit = println("This parrot is flying...")
}

class Car

复制代码

在主函数中定义一个泛型函数如下:

def getBirds[T >: Bird](things : ListBuffer[T]): ListBuffer[T] = things
复制代码

显然,我们为这个泛型 T 规定了下界,凭借直觉来看,我们装入 things 内部的元素至少应该是 Bird ,或者是 Animal 类型。如果传入的是 Parrot 类型,由于它突破了下界的 "底线",编译器应当报出一个编译错误。比如说下面这样的写法:

// 从常理来说,我们传入的 Parrot 实例不符合下界的约束,这段代码会报错。
getBirds(ListBuffer(new Parrot,new Bird)) foreach (_.sound())
复制代码

而实际上,这段代码是正确的。执行结果是:

this is a parrot
this is a bird
复制代码

在上述代码中, ListBuffer[T] 中的 T 究竟是什么类型的呢?打开交互式终端 REPL ,将上述的所有声明输入进去,并执行下面的代码:

getBirds(ListBuffer(new Parrot,new Bird))
复制代码

执行这段代码后,会返回:

scala>  getBirds(ListBuffer(new Parrot,new Bird))
res1: scala.collection.mutable.ListBuffer[Bird] = ListBuffer(Parrot@64aad809, Bird@1f03fba0)
复制代码

res1 反映的结果来看,当我们传入 Parrot 的实例时,它应该是被视作为 Bird上转型对象,因此程序没有报错。

之前的文章中曾经提醒过,操作上转型对象时,由于 Java 的动态绑定机制,因此程序总是选择执行尽可能最 “具体” 的方法。所以,控制台的第一行打印的是 this is a parrot ,而不是 this is a bird 。那么下面做第二个实验,如果这个列表中混入了一个最高级的 Animal 实例。那么下面的代码可以运行吗?

getBirds(ListBuffer(new Parrot,new Animal)) foreach (_.fly())
复制代码

编译器会直接指出错误:无法解析 fly 方法。REPL 显示,这个 T 类型目前被认为是 Animal

scala> getBirds(ListBuffer(new Parrot,new Animal))
res2: scala.collection.mutable.ListBuffer[Animal] = ListBuffer(Parrot@73afe2b7, Animal@9687f55)
复制代码

很显然,为了保持这个列表的兼容性,泛型 TParrotAnimal 之间必须选择更抽象的那个类型,且显然不是所有的动物都具备 fly 方法。如果此时干脆传入一个与之毫不相关的类:Car,此时,T 只能是最抽象的 Object 类型。

scala> getBirds(ListBuffer(new Parrot,new Car))
res3: scala.collection.mutable.ListBuffer[Object] = ListBuffer(Parrot@1e495414, Car@3711c71c)
复制代码

在这种情况下,连 map(_.sound()) 都无法调用。

泛型章节小结

笔者根据上一小节的现象,给出以下推导:

  1. 对类型参数进行推断时,解释器总是选择 “最高级” (或者称 “最兼容” )的那个类型,直到 Object 类。

  2. 由于父类不一定拥有子类的方法,因此被推断出来的 T 类型实例不可以调用子类的方法。

  3. 和上界的限制有所不同,传入元素的实际类型 S 可以是下界 T 的子类,而它会被转换成 T 类型的上转型对象 。在调用其方法时,遵守 Java 的动态绑定机制。

Part2. 视图界定与上下文界定

视图界定和上下文界定是属于 Scala 独有的类型约束方式,不仅如此,这里还大量使用了隐式转换的技巧。在本章的例子开始,我们将不再使用 Java 的 Comparator<T>Comparable<T> 接口实现比较功能,而使用 Scala 的 Ordered[T]Ordering[T] 特质取代之。

视图界定

视图界定的符号是 <% ,表示 “ View Bounds ”。它的使用范围比泛型 <: 符号更广,如果存在 T <% S ,则 T 未必是严格继承于 S 的子类,因为我们可以间接地提供由 TS 的隐式转换函数来建立 " T <: S " 的关系,此时可称 ST视界

和之前的泛型上界下界有所不同,视图界定的理念更趋近于:利用隐式转换函数,将某些不匹配的 T 类型转换成适配的类型(类似于一种适配器模式)。

下面举一个实例,3 美元和 20 元人民币之间如何比较大小呢?不同的货币之间是无法直接进行比较的。然而不论是何种货币,它们的价值总是和另外一种物品挂钩——那就是黄金。显然,要想对不同价值的货币进行比较,只需要将它们转换为等价的黄金,然后比较哪一方可兑换更多的黄金就可以说明问题了。

下面给出一些模板类的定义:

abstract class Money

case class RMB(value : Double) extends Money
case class Dollar(value: Double) extends Money

case class Gold(weight: Double) extends Ordered[Gold] {
  override def compare(that: Gold): Int = {
    val flag: Double = this.weight - that.weight
    if (flag > 0) 1
    else if (flag < 0) -1
    else 0
  }
}
复制代码

其中,只有 Gold 实现了 Ordered 特质,因此只有 Gold 之间可以通过 weight 属性相互比较。额外提醒,Ordered 特质的用法和 Java 的 Comparable<T> 本质上相同,除此之外,任何实现了 Ordered 接口的类可以使用 < , > , >= ... 等符号直接进行比较。

下面再定义一个 gt 方法,它判别前一种货币是否比后者更值钱(或者等值)——通过它们可兑换黄金的多少判断。

def gt[T <% Gold](c1 : T,c2 :T): Boolean ={
  // 这个 > 方法实际上来自于 Ordered[Gold]。
  c1 >= c2
}
复制代码

其中,T <% Gold 表示允许都将其它不具备比价功能的类 T ,比如本代码块中的 Money 类转换成等值的 Gold 再比价。而这要求我们提供对应的隐式转换函数:

implicit def Money2Gold(money: Money) : Gold = {
    money match {
        case RMB(value) => Gold(value / 272.00d)
        case Dollar(value) => Gold(value / 38.00d)
    }
}

复制代码

在这段代码中,我们使用了模式匹配来完成对不同种货币的黄金兑换功能。

println(gt(RMB(20), Dollar(3)))
复制代码

随后就可以通过 gt 方法实现对不同种货币的比较了。当然,用于比较的 T 可以是任意其它类型,只要提供对应的隐式转换函数将其转换为 Gold ,就可以比较出它们的 "价值" 大小。

更推荐的写法

这段代码运行起来不会有任何问题,然而编译器还是会弹出一个 warning。原因是它更推崇将视图界定等效替换成隐式参数的写法。我们刚才定义的 gt 最好是这样定义的:

def gt[T](c1: T, c2: T)(implicit money2Gold: T => Gold): Boolean = c1 >= c2
复制代码

这样的写法暴露了隐式参数的参数列表,对于代码的调用者而言,他可以主动地自定义 T => Gold 函数的细节,而不仅依赖于定义域内提供的隐式转换函数了。

上下文界定

上下文界定的符号为 : ,形式为 [T:M] 。它虽然和上界 <: 下界 >: 符号仅相差了 <> 号,但是代表着截然不同的概念:上下文界定 [T:M] 表示这里依赖一个隐式值(或称上下文)来给定一个 M[T] ,并调用 M[T] 的一些功能。

先从一个具体的例子开始说起。声明一个函数 gtOrEq :它用于比较两个 Person 对象 ,并返回岁数较大的那一个 ( 两者同岁返回前者 )。

// 起这个方法名主要是为了避嫌...Ordering 内部也有一个 gteq 方法。
def gtOrEq[Person](x: Person, y: Person)(implicit ordering: Ordering[Person]): Person = {
    if(ordering.gteq(x, y)) x else y
}
复制代码

Person 是一个简单的样例类。

case class Person(age: Int)
复制代码

gtOrEq 方法需要接收一个 Ordering[Person]比较器,这个由我们自行实现。由于全局只需要一个这样的比较器,因此这里使用 object 关键字创建了一个单例对象。

implicit object PersonComparator extends Ordering[Person] {
    override def compare(x: Person, y: Person): Int = x.age - y.age
}
复制代码

现在, gtOrEq 方法可以自动地通过 PersonComparator 实现对 Person 的比较了。

很显然,这个 gtOrEq 方法可以进行归纳,因为它不仅限于比较 Person 类,其实对于任何一个 T 类型, gtOrEq 方法只需要保证能够接收上下文中提供的 Ordering[T] 作为比较规则,并调用其 gteq 方法即可正常运作。使用上下文界定能够很容易地表述这个逻辑,写法上也要更加简洁。

def gtOrEq[T : Ordering](x: T, y: T): T = {
    if (implicitly[Ordering[T]].gteq(x, y)) x else y
}
复制代码

这里没有定义隐式参数列表,那么 gtOrEq 函数内部现在如何调用 Ordering[T] 隐式值呢?所有的隐式转换,都会在编译期间完成绑定,Scala 提供了 implictly[E] 来主动获取定义域内满足 E 类型的隐式值。从上述的代码来看,我们正是通过 implicitly[Ordering[T]] 获取到 PersonComparator 比较器的。

Part3. 协变,逆变,不变

AB 的父类,另有一个泛型类 C[T] ,并给出不变,逆变,协变的定义。

不变*( invariant )*,即 C[T]C[A]C[B] 没有从属关系,C[T] 的继承顺序和 T 的继承顺序无关。

协变*( covariant)*,即 C[+T]C[A]C[B] 的父类,C[T]的继承顺序和 T 的继承顺序一致。

逆变*( contravariant )*,即 C[-T]C[B]C[A] 的父类。C[T]的继承顺序和 T 的继承顺序相反。

在这里,+ 号和 - 号均指代型变注解。它们只能用于泛型类,而不能用于泛型方法,因为我们说的型变(协变和逆变),不变概念是在描述类型参数和包含它的泛型类的关系。除此之外,型变注解可以和上界 <: 和下界 >: 符号搭配使用。

先举个不变的例子。在 Java 中,其 List 类是不变的。这意味着 List[String] 并不是 List[Object] 的子类,因此,这样的赋值是错误的:

List<String> list = new LinkedList<Object>();
复制代码

现在,我们上手 Scala 代码,尝试这三种情况会有哪些区别。首先给出三个非常简单的类声明:

class Box[T]
class Bird
class Parrot extends Bird
复制代码

如果这个 Box 是不变的 Box[T]

val box1  : Box[Bird] = new Box[Bird]     // access.
val box2  : Box[Bird] = new Box[Parrot]   // Oops!
val box3  : Box[Parrot] = new Box[Bird]   // Oops!
复制代码

如果这个 Box 是协变的 Box[+T]

val box1  : Box[Bird] = new Box[Bird]     // access.
val box2  : Box[Bird] = new Box[Parrot]   // access.
val box3  : Box[Parrot] = new Box[Bird]   // Oops!
复制代码

如果这个 Box 是逆变的 Box[-T]

val box1  : Box[Bird] = new Box[Bird]     // access.
val box2  : Box[Bird] = new Box[Parrot]   // Oops!
val box3  : Box[Parrot] = new Box[Bird]   // access.
复制代码

如果一个类包含了多个泛型参数,则可能会同时存在逆变和协变的关系,比如:

class Function[-T,+R]
复制代码

它表示,随着 Function[T,R] 的不断继承,T 会变得更加抽象(越趋近于 Any),而 R 会变得更加具体。那么,Scala 引入协变,逆变的意义何在? 简单来说,Scala 想要为函数 function 提供一个像 OOP 那样可被 "继承和拓展" 的特性,以此来实现 "更伟大的 FP"。

深入里氏替换原则

里氏替换原则*(Liskov Substitution Principle)*:如果在任何需要 U 类型的地方,都可以使用 T 类型替换,那么就可以安全地认为 T 是类型 U 的子类型。在 OOP 程序中,里氏替换原则最常用,也比较容易理解。比如下面这样一段 Java 代码:

List<String> list = new LinkedList<>();
复制代码

显然,list 是一个上转型对象,它使用了功能更强大的子类代替实现了父类(实际上是接口)的功能。显然,List 要求实现的功能,LinkedList 全都能做。它满足里氏替换原则,因此使用起来不会带来任何问题。

但反过来,在需要 LinkedList 的场合,却赋予了一个更抽象的 List ,则就是一段不安全的代码。原因是:程序可能依赖子类提供更精细的功能,但是其父类却未必能提供。除非将更细化的代码放到了父类上,但这样的程序本身违背了 OCP 开闭原则,也不符合逻辑上的认识。

描述函数间的 "继承" 关系

在 FP 编程中,里氏替换原则的哲学是这样表述的:如果函数 T ,它能实现和函数 U 同样的功能,且仅需要更抽象的参数,就能够提供更具体的值,则我们可以认为函数 T 是函数 U 的子类型,或者称函数 T 比函数 U 功能更 "强大" 。没错,在 Scala 中可不只有上转型对象,还可以有 "上转型函数"。

笔者暂且用简略的符号来表述 TU 的 "子函数":用 P1P2,... 代表一个函数的参数列表中可能出现的类型,R1 , R2 ... 代表一个函数的返回值中可能出现的类型,设 U : (P1,P2) => (R1,R2) ,则 T : (↑P1,↑P2) => (↓R1,↓R2)。其中 代表着对应 Pi 类型的上级父类, 代表着对应 Ri 类型的下级子类。

显然,这里的参数类型呈现出两种现象:

  1. 位于参数列表的类型,其继承顺序和函数本身的继承关系是相反的,因为函数越 "强大",参数越抽象
  2. 位于返回值的类型,其继承顺序和函数本身的继承顺序是一致的,因为函数越 “强大”,返回值越具体

说得再明确一点:

  1. 参数列表和函数是逆变关系。
  2. 返回值和函数是协变关系。

如果我们需要定义一个存在上述继承关系的 (P1,P2)=>(R1,R2)函数式接口,它应该是这样:

trait Function2_2[-P1,-P2,+R1,+R2] {
  
  def supply(p1 : P1, p2 : P2) : (R1,R2)
  
}
复制代码

在这个类的描述中,所有在函数的参数列表中出现的 Pn 都是逆变的,因此这里又被称之为逆变点。同样,所有在返回值中出现的 Rn 都是协变的,因此返回值这里又被称之为协变点

编译器会对你的程序进行检查:如果在逆变点中出现的参数是协变的,或者说应该在协变点中出现的参数是逆变的,它都会提示你出现了这样的错误:

//逆变的 R1 类型出现在了逆变点的位置在 ...
Contravariant type R1 occurs in covariant position in ...
//协变的 P1 类型出现在了逆变点的位置在 ...
Covariant type P1 occurs in contravariant position in ...
复制代码

为什么编译器会对此进行拦截呢?因为这两种情况都违背了函数间的里氏替换原则:

  1. 功能更 "强大" 的函数不应该去消费更具体的参数(不能用的更多)。
  2. 功能更 "强大" 的函数不应该生产出更抽象的返回值(不能造的更少)。

这样的原则满足了程序调用者的 ”贪心策略" :当他不太清楚一个函数具体需要接受什么样的参数时,一定会希望仅仅传入最通用的类型,就能够使这个函数正常工作。这样,程序调用者就不必为了得到正确的调用结果而被迫去追究类型参数的细节内容。

更 "贪心" 的是,程序调用者又希望这个函数的返回值是细节齐全的。比如,对于某个字符串处理函数而言,程序员当然希望这个函数能够返回具体的 String 类型,而不是粗略地返回一个 Any / Object 类型。这样,程序调用者就不必再进行类型检查,或者是进行强制转换等诸多繁琐的工程了。

在下面的类型定义环节中,对于不涉及型变的类型参数 T它所处的位置将成为不变点,或者说这个位置不会再接收逆变或是协变的参数类型(后文有所涉及)。

// T 位于一个不变点中。
class Obj[T]
复制代码

同时具备逆变协变特性的类型

在这里,为了尝试可能的情况和错误,这里所设计的协变,逆变颇有 “刻意” 之嫌。我们暂时不去讨论这样的声明有什么实际用途,仅分析这样做编译器是否会报错。

还有一种特殊的情况,如果说一个类型参数 K 同时出现在参数列表和返回值中,并且还要满足里氏替换原则的规则,即希望传入更抽象的 K ,输出的 ListBuffer 能装载更详细的 K 类型,此时该怎么办?

// 我们希望出现在参数列表的 K 是逆变的,出现在返回值当中的 K 又是协变的。
// 这是笔者为了形象描述给出的符号,Scala 中没有 ± 号表示 K 既协变又逆变。
trait WorkShopK]{
	def process(k : K) : ListBuffer[K]
}
复制代码

我们不能这样做,因为会让 K 具备歧义,但是可以选择等效替代法,即使用另一种符号 O 替换掉其中一种情况,比如借助符号 O 替代掉 WorkShopK 之间的协变关系:

trait WorkShop[-K]{
    // 使用 O<:K 等效替代 +K 。
    // 注意 !!! O 本身是不变的,因为它没有型变符号,只有上界约束。
    // process 被改造成了泛型函数。
	def process[O<:K](k : K) : ListBuffer[O]
}
复制代码

反过来,使用符号 O 等效替代掉 WorkShopK 之间的协变关系似乎也可行:

trait WorkShop[+K]{
    //使用 O>:K 等效替代 -K 。
	def process[O>:K](o : O): ListBuffer[K]
}
复制代码

在大部分情况下,等效替代哪一方应该都能表述同样的语义,但在这里不是。在该变换中, K 出现在函数的协变点位置无可厚非。但是, K 同时又作为 ListBuffer 的类型参数,这引发了冲突。原因有二:

  1. WorkShop 自身的定义中,它和 K 是协变的。假定 WorkShop[T]WorkShop[U] 的子类,那么根据里氏替换原则,分别调用它们各自的 process 方法产出的 ListBuffer[T] 也是 ListBuffer[U] 的子类。说得更简单些,就是 WorkShop[+K] 认为 ListBufferK 之间也应该是协变的。
  2. 然而在 ListBuffer 自身的定义中,ListBuffer 和对应位置的类型参数 ( 这里的 A 其实就是 K ) 是不变的关系。
@SerialVersionUID(3419063961353022662L)
final class ListBuffer[A]
....
复制代码

很显然 WorkShop 的 "期望" 和 ListBuffer 自身的定义两者相悖了,编译器因此会报错:ListBuffer 并不支持装入一个具备协变特征的 K ,或者称 K 出现在了一个不变点的位置。

// 逆变的 X 出现在了不变点的位置上在 ... 
Contravariant type X occurs in covariant position in ...
// 协变的 X 出现在了不变点的位置上在 ...
Covariant type X occurs in invariant position in ...
复制代码

在更早的例子中,为什么使用 O<:K 替换 +K 就不会报错呢?很简单,O 是不变的,它出现在后续任何逆变点,协变点,不变点上都没问题。

下面是一个更容易理解的 "正面例子" :WorkShop 的类型参数 K 是协变的,而 K 同时出现在 Products 的协变点,因此编译器没有提示错误,但是一旦 ProductT 的关系是逆变,或者是不变的,那就不行了。

trait WorkShop[+K]{
  //使用 O>:K 等效替代 -K 。
  def process[O>:K](o: O ): Products[K]
}

//Products 的泛型参数也是协变的。
trait Products[+T]
复制代码

型变章节小结

想通过协变,协变来维持一个泛型类保持安全的继承关系(或称让它满足里氏替换原则),则记住 5 点:

  1. 型变注解用于泛型类,它们不能用在泛型函数上。
  2. 出现在函数形参列表的类型参数都应是逆变的。
  3. 出现在函数返回值的类型参数都应是协变的。
  4. 如果某个类型参数 T 同时也作为其它泛型类的类型参数,则还需要判断此泛型类对应位置的类型参数是逆变还是协变的。
  5. 不变的类型参数可以声明在任何一个逆变点,或者是协变点的位置。反过来,不变点不能出现任何逆变,协变的类型参数。

最后援引一张图片来生动来描述型变关系:

variant.jpg

一个简单的实战

下面有三种原料(按继承顺序为):Metal , Iron , Steel ,以及三种产品(按继承顺序为):Product , Vehicle , Tank

package generic.var1
​
import java.text.SimpleDateFormat
import java.util.Date
​
/**
  * 广泛意义上的金属。
  */
class Metal {
  def purify() : Unit = println("purifying ...")
}
​
/**
  * 铁继承于金属。
  */
class Iron extends Metal {
  def forge() : Unit = println("forging ...")
}
​
/**
  * 钢是基于铁的合金,这里约定它继承于铁。
  */
class Steel extends Iron {
  def finishing() : Unit = println("finishing ...")
}
​
​
/**
  * 广泛意义上的产品。有唯一的出厂日期。
  */
class Product {
​
  val dateOfProduction: String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date)
​
}
​
/**
  * 交通工具继承于产品类,具备行驶的功能。
  */
class Vehicle extends Product {
​
  def run() : Unit = println("running...")
​
}
​
/**
  * 坦克继承于交通工具,具备开炮的功能。
  */
class Tank extends Vehicle {
​
  def fire() : Unit = println("fire")
​
}
复制代码

声明一个带类型参数的 Manufacturer 工厂类,它的原料和产品均为上述的六个类型。该工厂依赖的原料以及产品是受上界约束的类型参数 MP 。现利用逆变,协变,使得这个工厂满足里氏替换原则,并演示 Manufacturer[Iron, Vehicle] 类型的工厂实例是如何安全地赋值给 Manufacturer[Steel, Product] 类型的。

下面给出 Manufacturer 的声明:

abstract class Manufacturer[-M <: Metal, +P <: Product] {
​
  // 这个抽象类/特质只有一个抽象方法,因此该方法又称:
  // Simple Abstract Method 简称 SAM。
  // 对于 SAM ,我们可以类比 Java 8 Lambda 的写法,在 Scala 中实现对匿名实现类的简写。
  // 但是需要确保你的 Scala 版本是 2.12+。
  def convert(m: M): P
​
}
复制代码

主函数如下。

/*
 提示:在 Scala 2.12 版本中,SAM 可以有下列简写方式。
  var manufacturer: Manufacturer[Steel, Product] =
  (m: Steel) => {
    m.purify()
    m.forge()
    m.finishing()
    new Product
  }
​
  var manufacturer2: Manufacturer[Iron, Vehicle] =
  (i: Iron) => {
    i.purify()
    i.forge()
    new Vehicle
  }
  */
​
var manufacturer: Manufacturer[Steel, Product] = new Manufacturer[Steel, Product] {
  override def convert(m: Steel): Product = {
    m.purify()
    m.forge()
    m.finishing()
    new Product
  }
}
​
val manufacturer2: Manufacturer[Iron, Vehicle] = new Manufacturer[Iron, Vehicle] {
  override def convert(m: Iron): Vehicle = {
    m.purify()
    m.forge()
    new Vehicle
  }
}
​
manufacturer.convert(new Steel)
// 从类型定义来看, manufacturer2 可以用更 "粗" 的原料,做更 "精细" 的产品。 
manufacturer = manufacturer2
// 因此 manufacture 的工作不会受到影响,可以正常执行。
manufacturer.convert(new Steel)
复制代码