1. Overview

Scala, like many other computer languages, supports type casting or type coercion. In this tutorial, we’ll look at those mechanisms and learn which is more idiomatic in Scala.

We should note that type casting in Scala is fraught with danger because of type erasure. As a result, if we don’t understand how to correctly type cast, we can introduce subtle bugs.

2. Type Cast Mechanisms in Scala

Scala provides three main ways to convert the declared type of an object to another type:

  1. Value type casting for intrinsic types such as Byte, Int, Char, and Float
  2. Type casting via the asInstanceOf[T] method
  3. Pattern matching to effect type casting using the match statement

2.1. Value Type Casting

Conversion between value types is defined as:

Byte —> Short —> Int —> Long —> Float —> Double

The arrows denote that a given value type on the left-hand side of the arrow can be promoted to the right-hand side. For example, a Byte can be promoted to a Short. The opposite, however, is not true. Scala will not allow us to assign in the opposite direction:

val byte: Byte = 0xF
val shortInt: Short = byte // OK
val byte2: Byte = shortInt // Will not compile

Note that we don’t use special syntax or methods for value type casting.

2.2. Type Casting via asInstanceOf[T]

We can coerce an existing object to another type with the asInstanceOf[T] method. Additionally, Scala supports a companion method, isInstanceOf[T], that we can use in conjunction with it.

To see these methods in action, let’s consider a few class definitions:

class T1
class T2 extends T1
class T3

Now, let’s use those classes to see how type casting via asInstanceOf[T] works:

val t2 = new T2
val t3 = new T3
val t1: T1 = t2.asInstanceOf[T1]

assert(t2.isInstanceOf[T1] == true)
assert(t3.isInstance[T1] == false)
val anotherT1 = t3.asInstanceOf[T1]  // Run-time error

This is similar to Java. However, in the next section, we’ll learn how this can be improved with Scala’s match statement.

2.3. Type Casting via Pattern Matching

We can use isInstanceOf[T] and asInstanceOf[T] as shown in the previous section. But, if we have even moderately complex logic based on object types, we can end up with a series of if-else statements that can be difficult to maintain.

Fortunately, Scala’s pattern matching solves this challenge for us. Consider a function, retrieveBaeldungRSSArticles(), that retrieves information about Baeldung’s current articles via RSS. This function can fail in a variety of ways, including both networking and XML parsing.

To solve this, we can wrap the method call with Try and implement pattern matching on Success and Failure responses:

Try(retrieveBaeldungRSSArticles()) match {
  case Success(lines) if lines.isEmpty =>
    // No content was found
  case Success(lines) =>
    // Can process a successful response
  case Failure(e: MalformedURLException) =>
    // Error w/the URL we used to connect
  case Failure(e: UnknownHostException) =>
    // Failed to find specified host
  case Failure(e: IOException) =>
    // Generic IO/network error handling here.
  case Failure(t) =>
    // A non-IOException occurred.

This implementation provides us with a number of benefits:

  1. Our application logic is clearer. Notice how we can easily identify success or failure cases.
  2. Scala unwraps the object type for us, again simplifying the intent.
  3. The default cases for success and failure are easy for us to identify.

Scala makes all of this effortless, so much so that we rarely have to think about type casts.

3. When Do We Need Type Casts?

So, is there ever a time when type casting in Scala is required? The list is surprisingly short:

  • Error handling as shown in the previous section
  • Integration with legacy code that we can’t change
  • Frameworks that we’re writing

The last case should be given some careful thought. We should leverage Scala’s functional capabilities and write functions that take functions as parameters. This way, we enable the users of our framework to focus on writing application code instead of forcing them to unnecessarily derive new classes.

Integration with legacy code also applies, particularly to Java integration. Many older Java frameworks return opaque objects. If we want to use them in Scala, we should cast them to strongly-typed objects.

4. Type Erasure

Type erasure is an important concept to be aware of. It is a consequence of how the JVM supports generic types. As we know, generic types allow us to parameterize a class or trait. The most common use of generics is with Scala’s collections classes, for example:

val counts: List[Int] = List.empty[Int]
val teamScores: Map[String, Int] = Map[String, Int]("Team A" -> 10, "Team B" -> 5, "Team C" -> 13)

Here, we’ve indicated that these collections can only accept objects of the specified type. However, this is a compile-time feature. At runtime, both collections will be indistinguishable from any other List or Map.

To illustrate this concept, let’s see some valid examples that are, perhaps, a bit surprising, as all of the assertions are true:

val l1 = List(1, 2, 3)
val l2 = List("a", "b", "c")
assert(l1.isInstanceOf[List[Int]], "l1 is a List[Int]")
assert(l1.isInstanceOf[List[String]], "l1 is a List[String]")
assert(l2.isInstanceOf[List[Int]], "l1 is a List[Int]")
assert(l2.isInstanceOf[List[String]], "l1 is a List[String]")

The important takeaway here is that there are limits to Scala’s type system.

5. Conclusion

In this article, we showed the basic use of type casts in Scala. We also looked at how Scala’s match statement allows for fluid management of type casts.

Finally, we noted that the subtleties of Scala’s type system are an advanced topic. For this reason, we need to be careful when we type cast.

As always, the full source code can be found over on GitHub.

Comments are closed on this article!