Scala superimposes a very elegant class hierarchy over Java’s type system, balooning out from Any at the top, into AnyRef and AnyVal to cover Java’s Objects and primitives respectively, then finally converging, collapsing the reference types onto Null and all types onto Nothing. As I understand it, Nothing is a subtype of everything; Null a subtype of all subtypes of AnyRef/java.lang.Object. [ see http://www.scala-lang.org/node/128 ]
However, there seem to be a few irregularities, a few places where it does not work to simply think of all Scala types as elements of a seamless type hierarchy. I find this irksome, and want to understand the places where I might be surprised.
So far, I know of a few irregularities:
1) Although Null is a subtype of AnyRef, calling null.isInstanceOf[AnyRef] (or other subtypes of AnyRef) returns false. I suspect this was chosen to be consistent with the behavior of Java’s instanceof operator.
2) Everything is covariant to Nothing, regardless of variance annotations. If I have a method that returns a type T that is not marked covariant, I can override that method to return type Nothing. [NOTE: this claim is mistaken, see answers and comments below!]
3) I can’t apply isInstanceOf to the type AnyVal [ See Why can AnyVal not be used in an isInstanceOf check? and How to test a value on being AnyVal? ]
4) It is illegal to ask whether something isInstanceOf[Null], which is a perfectly coherent thing to ask (although not particularly necessary, since “myVar == null” would give the same answer)
Are there other examples of irregularities or special cases in Scala’s type hierarchy? I feel like these are worth learning and understanding to avoid unwelcome surprises.
1)
"A string is a subtype of AnyRef".isInstanceOf[AnyRef]returnstrue. This is true for other subtypes ofAnyRefas well, except forNull. The only irregularity there is done to be consistent with Java, as you said.2) If
Bis a subtype ofA, that isB <: A, then you can always override a method:to:
This is called refining the return type, and is always allowed. Since
Nothingis a subtype of every other type (Nothing <: Afor allA), you can always refine your return type toNothing(e.g. by throwing an exception in the body of the method). This is a pretty regular property. The return type covariance is not directly related to variance annotations on type parameters.3) The other questions cover this nicely.
4) This is because the
Nulltype does not exist in the Java runtime. I guess if you wanted to emulate this, you could create your owninstanceOfmethod – you would first have to check if the argument isnull, otherwise, do the normalisInstanceOfcheck.There are other irregularities, yes. See for example: If an Int can't be null, what does null.asInstanceOf[Int] mean?
Arrays are another example, where you may pay the uniformity of generic arrays with boxing/unboxing or
instanceofchecks at runtime. Thenew Array[Any]is translated into an object array – storing an integer into the array will result in boxing it. Whenever you use anArray[T], whereThas no upper bound, the array will be patterned matched against the correct runtime array type every time you index an element.To better understand how you might be surprised it’s useful to think in terms of how these constructs are translated to the JVM where there is a notion of primitive and reference types, boxing/unboxing and different array classes.