The type bounds control what types are valid parameters.
The covariance/contravariance annotations control the sub/super-typing relationship between instances with different parameters. Foo[+T]
means Foo[A]
is a subtype of Foo[B]
if A
is a subtype of B
. Foo[-T]
means the reverse.
class Foo1[T <: Bar]()
class Foo2[+T <: Bar]()
trait Bar
trait Baz extends Bar
val f1: Foo1[Bar] = new Foo1[Baz]() // compile error
val f2: Foo2[Bar] = new Foo2[Baz]() // works just fine
One benefit as opposed to using type boundaries like Foo1[_ <: Bar]
everywhere is the compiler will enforce certain properties on the class itself. For instance, this won't compile:
class Foo[+T]() {
def f(t: T): Unit = {}
}
Neither will this:
class Foo[-T]() {
def f(): T = { ??? }
}
As far as I know, Java has no way to explicitly represent covariance or contravariance. This has led to a lot of bugs, especially with arrays being implicitly covariant even though they shouldn't be since they are mutable.