I have some generic/OOP Scala code (not useful, just classroom excercises)
I have a Container interface, "IterableContainer", that takes in and returns objects whose type is a subclass of AnyRef. It has a concrete sub-class that also takes in and returns objects whose type is a subclass of AnyRef.
trait IterableContainer[Type <: AnyRef] {
def length: Int
def getAt(index: Int): Type
def append(element: Type)
}
class IterableArrayList[T <: AnyRef]() extends IterableContainer[T] {
val underlyingContainer = new ArrayBuffer[T](16)
override def length: Int = {
return underlyingContainer.length
}
override def getAt(index: Int): T =
{
if (index < underlyingContainer.length) {
return underlyingContainer(index)
} else {
return null // Expression of type T does not conform to expected type.
}
}
override def append(element: T) = {
underlyingContainer :+ element // :+ means append
}
}
Can anyone explain why I cannot return null when T is explicitly being stated as a an object of a type that extends AnyRef?
Also, if you understand Scala's generics better than I do, please explain further - they really make no sense to me (relative to C++ generics).
The problem is that T <: AnyRef
does not imply that T >: Null
.
I believe the only counter-example is Nothing
.
Adding an additional (lower) type bound on your IterableArrayList
's type parameter makes the compiler happy:
class IterableArrayList[T >: Null <: AnyRef]() extends IterableContainer[T] {
Now the compiler knows that T
is a subtype of AnyRef
and a supertype of Null
.
Now, you may be thinking to yourself, "Doesn't this cause all sorts of problems for writing library code in Scala? People must have to add T >: Null
everywhere!"
However, the answer is no.
Why? Because Scala programmers pretty much never use null
.
Let's look at List
as an example. There are two ways to get the first element of a list:
def head: A
(scaladoc)
def headOption: Option[A]
(scaladoc)
If you call head
on an empty list, you get an exception. If you call headOption
on an empty list, then you get None
.
That's the usual strategy for Scala libraries: either you throw an exception, or you return an Option
type—you never return null
.
Example using Option[T]
:
override def getAt(index: Int): Option[T] =
if (index < underlyingContainer.length) {
Some(underlyingContainer(index))
} else {
None
}
Example using an exception:
override def getAt(index: Int): T = underlyingContainer(index)
Note that the underlying ArrayBuffer
already throws an IndexOutOfBoundsException
if the index is invalid. This also has the added advantage of working correctly if the user passes in a negative index... (You might want to add a 0 <= index
to your condition if you go with using Option
.)