I’m new to using Scala and am trying to see if a list contains any objects of a certain type.
When I make a method to do this, I get the following results:
var l = List("Some string", 3)
def containsType[T] = l.exists(_.isInstanceOf[T])
containsType[Boolean] // val res0: Boolean = true
l.exists(_.isInstanceOf[Boolean]) // val res1: Boolean = false
Could someone please help me understand why my method doesn’t return the same results as the expression on the last line?
Thank you, Johan
CodePudding user response:
Scala uses the type erasure model of generics. This means that no information about type arguments is maintained at runtime, so there's no way to determine at runtime the specific type arguments of the given List
object. All the system can do is determine that an element of a List
is of some arbitrary type parameters. You can verify this behavior by trying other types as well:
var l = List("Some string", 3)
def containsType[T]: Boolean = {
l.exists(_.isInstanceOf[T])
}
println(containsType[Boolean])
println(containsType[Double])
println(containsType[Unit])
println(containsType[Long])
Output:
true
true
true
true
Another way of saying it is that isInstanceOf
is already a polymorphic function (and a synthetic function - a function generated by the compiler) and it does not work with generic type arguments like T
, that is why no matter what type you give to the polymorphic function containsType
, it will always result in true
, because calls to it are equivalent to containsType[_]
from the JVM's point of view - which is a placeholder for Any
type.
Polymorphic functions can only be called with specific (concrete) type arguments like Boolean
, Double
, String
, etc. That is why:
println(l.exists(_.isInstanceOf[Boolean]))
Gives:
true
To alert you of the possibly non-intuitive runtime behavior, the compiler usually emits unchecked warnings. For example, if you had run your code in the Scala REPL, you would have received this:
CodePudding user response:
Alin's answer details perfectly why the generic isn't available at runtime. You can get a bit closer to what you want with the magic of ClassTag
, but you still have to be conscious of some issues with Java generics.
import scala.reflect.ClassTag
var l = List("Some string", 3)
def containsType[T](implicit cls: ClassTag[T]): Boolean = {
l.exists(cls.runtimeClass.isInstance(_))
}
Now, whenever you call containsType
, a hidden extra argument of type ClassTag[T]
gets passed it. So when you write, for instance, println(containsType[String])
, then this gets compiled to
scala.this.Predef.println($anon.this.containsType[String](ClassTag.apply[String](classOf[java.lang.String])))
An extra argument gets passed to containsType
, namely ClassTag.apply[String](classOf[java.lang.String])
. That's a really long winded way of explicitly passing a Class<String>
, which is what you'd have to do in Java manually. And java.lang.Class
has an isInstance
function.
Now, this will mostly work, but there are still major caveats. Generics arguments are completely erased at runtime, so this won't help you distinguish between an Option[Int]
and an Option[String]
in your list, for instance. As far as the JVM is concerned, they're both Option
.
Second, Java has an unfortunate history with primitive types, so containsType[Int]
will actually be false in your case, despite the fact that the 3
in your list is actually an Int
. This is because, in Java, generics can only be class types, not primitives, so a generic List
can never contain int
(note the lowercase 'i', this is considered a fundamentally different thing in Java than a class).
Scala paints over a lot of these low-level details, but the cracks show through in situations like this. Scala sees that you're constructing a list of String
s and Int
s, so it wants to construct a list of the common supertype of the two, which is Any
(strings and ints have no common supertype more specific than Any
). At runtime, Scala Int
can translate to either int
(the primitive) or Integer
(the object). Scala will favor the former for efficiency, but when storing in generic containers, it can't use a primitive type. So while Scala thinks that your list l
contains a String
and an Int
, Java thinks that it contains a String
and a java.lang.Integer
. And to make things even crazier, both int
and java.lang.Integer
have distinct Class
instances.
So summon[ClassTag[Int]]
in Scala is java.lang.Integer.TYPE
, which is a Class<Integer>
instance representing the primitive type int
(yes, the non-class type int
has a Class
instance representing it). While summon[ClassTag[java.lang.Integer]]
is java.lang.Integer::class
, a distinct Class<Integer>
representing the non-primitive type Integer
. And at runtime, your list contains the latter.
In summary, generics in Java are a hot mess. Scala does its best to work with what it has, but when you start playing with reflection (which ClassTag
does), you have to start thinking about these problems.
println(containsType[Boolean]) # false
println(containsType[Double]) # false
println(containsType[Int]) # false (list can't contain primitive type)
println(containsType[Integer]) # true (3 is converted to an Integer)
println(containsType[String]) # true (class type so it works the way you expect)
println(containsType[Unit]) # false
println(containsType[Long]) # false