始终记住,如果Third
extendsSecond
则只要Second
需要 a ,Third
就可以提供 a 。这称为亚型多态性。
考虑到这一点,second.printInvariant(new Third)
编译是很自然的。您提供了 aThird
这是 的子类型Second
,因此它会签出。这就像将 Apple 提供给采用 Fruit 的方法。
这意味着你的方法
def printCovariant[A<:B](a: A): Unit = println(a)
可以写成:
def printCovariant(a: B): Unit = println(a)
不丢失任何信息。由于子类型多态性,第二个接受 B 及其所有子类,这与第一个相同。
您的第二个错误案例也是如此 - 这是子类型多态的另一种情况。您可以通过新的 Third 因为 Third 实际上是 Second(请注意,我使用的是从面向对象表示法获取的子类和超类之间的“ is-a ”关系)。
如果您想知道为什么我们甚至需要上限(子类型多态性还不够吗?),请观察以下示例:
def foo1[A <: AnyRef](xs: A) = xs
def foo2(xs: AnyRef) = xs
val res1 = foo1("something") // res1 is a String
val res2 = foo2("something") // res2 is an Anyref
现在我们确实观察到了差异。尽管子类型多态性允许我们在两种情况下都传入一个字符串,但只有方法foo1
可以引用其参数的类型(在我们的例子中是一个字符串)。方法foo2
很乐意接受一个字符串,但不会真正知道它是一个字符串。因此,当您想要保留类型时,上限可以派上用场(在您的情况下,您只需打印出值,因此您并不真正关心类型 - 所有类型都有一个 toString 方法)。
编辑:(
额外的细节,你可能已经知道了,但为了完整起见,我会把它写出来)
上限的用途比我在此处描述的要多,但是在参数化方法时,这是最常见的情况。当参数化一个类时,你可以使用上限来描述协方差和下限来描述逆变。例如,
class SomeClass[U] {
def someMethod(foo: Foo[_ <: U]) = ???
}
表示foo
方法的参数someMethod
在其类型上是协变的。怎么样?好吧,通常情况下(也就是说,没有调整方差),子类型多态性不允许我们传递Foo
带有其类型参数的子类型的参数化。如果T <: U
,那并不意味着Foo[T] <: Foo[U]
。我们说它Foo
的类型是不变的。但是我们只是调整了方法以接受Foo
参数化 withU
或其任何子类型。现在这实际上是协方差。因此,只要someMethod
涉及到 - 如果某种类型T
是 的子类型U
,那么Foo[T]
就是 的子类型Foo[U]
。太好了,我们实现了协方差。但请注意,我说的是“只要someMethod
是有关”。Foo
在这种方法中它的类型是协变的,但在其他方法中它可能是不变的或逆变的。
这种差异声明称为使用点差异,因为我们在使用时声明类型的差异(这里它用作方法参数类型someMethod
)。这是 Java 中唯一的一种差异声明。使用使用站点差异时,请注意get-put 原则(google it)。基本上,这个原则说我们只能从协变类(我们不能放)中获取东西,反之亦然,对于逆变类(我们可以放但不能得到)。在我们的例子中,我们可以这样演示:
class Foo[T] { def put(t: T): Unit = println("I put some T") }
def someMethod(foo: Foo[_ <: String]) = foo.put("asd") // won't compile
def someMethod2(foo: Foo[_ >: String]) = foo.put("asd")
更一般地说,我们只能将协变类型用作返回类型,将逆变类型用作参数类型。
现在,使用站点声明很好,但在 Scala 中,利用声明站点差异(Java 没有的东西)更为常见。这意味着我们将Foo
在定义时描述泛型类型的变化Foo
。我们会简单地说class Foo[+T]
。Foo
现在我们在编写使用;的方法时不需要使用边界。我们宣称Foo
在其类型、每个用例和每个场景中都是永久协变的。
有关 Scala 差异的更多详细信息,请随时查看我关于此主题的博客文章。