1. Overview

Like many other computer languages, Scala 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 dangerous because of type erasure. As a result, if we don’t understand how to type cast correctly, 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 ^ | Char

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 a specified host
  case Failure(e: IOException) =>
    // Generic IO/network error handling here.
  case Failure(t) =>
    // A non-IOException occurred.
}

This implementation provides us with several benefits:

  1. Our application logic is more explicit. Notably, we can quickly identify success or failure cases.
  2. Scala unwraps the object type for us, simplifying the intent.
  3. The default cases for success and failure are easy to identify.

Scala makes this effortless, so much 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 derive new classes unnecessarily.

Integration with legacy code also applies, particularly to Java integration. For example, 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’s a consequence of how the JVM supports generic types. We know that 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. Both collections will be indistinguishable from any other List or Map at runtime.

To illustrate this concept, let’s see some good 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 critical takeaway here is that there are limits to Scala’s type system.

5. Type Ascriptions

Type ascription is a concept similar to type annotation. Ascription is an up-casting operation applied to a value to provide hints to the type-checker during the compilation stage.

Due to the inheritance of different types, there could be a lot of types that apply to an expression. We can explicitly provide the value we expect from a particular expression using type ascription. We use the symbol ‘*:’* to provide the ascription type:

val value = Some(100):Option[Float]

In the above code, we specify the type of the expression Some(100) explicitly as Option[Float].

The type ascription explicitly converts a sequence type to pass to a vararg method using the : _* symbol:

def varargFn(str: String*) = str.length
val input = Seq("Hello", "World")
assert(varargFn(input: _*) == 2)

In the above code block, input variable is converted from Seq[String] to vararg using type ascription.

We should note that type ascription isn’t the same as invoking asInstanceOf(). The asInstanceOf() method performs a runtime operation, whereas type ascription is applied at compilation time. Hence, there’s no risk of getting a runtime exception.

6. Conclusion

In this article, we saw Scala’s primary use of type casts. We also examined 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 complete source code can be found over on GitHub.


» 下一篇: 配置SBT的堆大小