Scala - Type Casting/Conversion

Scala handles numeric conversions through a combination of automatic widening and explicit narrowing. Widening conversions (smaller to larger types) happen implicitly, while narrowing requires...

Key Insights

  • Scala provides both implicit and explicit type conversion mechanisms, with numeric widening conversions happening automatically while narrowing requires explicit casting with potential data loss
  • The asInstanceOf[T] method performs runtime type casting but throws ClassCastException on failure, while pattern matching with type checks offers safer alternatives for complex type hierarchies
  • String conversions leverage dedicated methods like toInt, toDouble for parsing, and toString for serialization, with Try-based wrappers recommended for handling parsing failures gracefully

Numeric Type Conversions

Scala handles numeric conversions through a combination of automatic widening and explicit narrowing. Widening conversions (smaller to larger types) happen implicitly, while narrowing requires explicit casting.

// Implicit widening conversions
val intValue: Int = 42
val longValue: Long = intValue        // Int -> Long (automatic)
val floatValue: Float = intValue      // Int -> Float (automatic)
val doubleValue: Double = longValue   // Long -> Double (automatic)

// Explicit narrowing conversions
val largeDouble: Double = 123.456
val toInt: Int = largeDouble.toInt           // 123 (truncates decimal)
val toLong: Long = largeDouble.toLong        // 123L
val toFloat: Float = largeDouble.toFloat     // 123.456f

// Potential data loss in narrowing
val largeInt: Int = 130
val toByte: Byte = largeInt.toByte    // -126 (overflow wraps around)

// Chain conversions
val result: Short = 42.5.toInt.toShort  // 42

The numeric type hierarchy in Scala follows: Byte -> Short -> Int -> Long -> Float -> Double. Each type provides conversion methods (toByte, toShort, toInt, toLong, toFloat, toDouble) for explicit casting.

object NumericConversions {
  def demonstrateOverflow(): Unit = {
    val maxInt: Int = Int.MaxValue
    println(s"Max Int: $maxInt")
    
    val overflowByte: Byte = maxInt.toByte
    println(s"Overflow to Byte: $overflowByte")  // -1
    
    val overflowShort: Short = maxInt.toShort
    println(s"Overflow to Short: $overflowShort") // -1
  }
  
  def safeConversion(value: Double): Option[Int] = {
    if (value >= Int.MinValue && value <= Int.MaxValue) {
      Some(value.toInt)
    } else {
      None
    }
  }
}

Reference Type Casting with asInstanceOf

The asInstanceOf[T] method performs runtime type casting for reference types. It’s Scala’s equivalent to Java’s cast operator but should be used sparingly due to runtime exceptions.

trait Animal
class Dog extends Animal {
  def bark(): String = "Woof!"
}
class Cat extends Animal {
  def meow(): String = "Meow!"
}

val animal: Animal = new Dog

// Successful cast
val dog: Dog = animal.asInstanceOf[Dog]
println(dog.bark())  // "Woof!"

// Failed cast throws ClassCastException
try {
  val cat: Cat = animal.asInstanceOf[Cat]  // Runtime error!
  println(cat.meow())
} catch {
  case e: ClassCastException => 
    println(s"Cast failed: ${e.getMessage}")
}

For collections, asInstanceOf casts the collection type but doesn’t validate element types due to type erasure:

val intList: List[Int] = List(1, 2, 3)
val anyList: List[Any] = intList

// This compiles but is unsafe
val stringList: List[String] = anyList.asInstanceOf[List[String]]

// Runtime error occurs when accessing elements
try {
  val firstString: String = stringList.head  // ClassCastException
} catch {
  case e: ClassCastException => 
    println("Element type mismatch at runtime")
}

Safe Type Checking with Pattern Matching

Pattern matching provides type-safe alternatives to asInstanceOf, combining type checking and casting in a single operation.

def processAnimal(animal: Animal): String = animal match {
  case dog: Dog => s"Dog says: ${dog.bark()}"
  case cat: Cat => s"Cat says: ${cat.meow()}"
  case _ => "Unknown animal"
}

val myDog: Animal = new Dog
println(processAnimal(myDog))  // "Dog says: Woof!"

For optional casting without exceptions, combine isInstanceOf checks with pattern matching:

def safeCast[T](obj: Any)(implicit tag: scala.reflect.ClassTag[T]): Option[T] = {
  obj match {
    case t: T => Some(t)
    case _ => None
  }
}

val mixedList: List[Any] = List(1, "hello", 2.5, new Dog)

mixedList.foreach { item =>
  safeCast[Int](item).foreach(i => println(s"Int: $i"))
  safeCast[String](item).foreach(s => println(s"String: $s"))
  safeCast[Dog](item).foreach(d => println(s"Dog: ${d.bark()}"))
}

String Conversions and Parsing

String conversions in Scala use dedicated parsing methods that throw exceptions on invalid input. Wrapping these in Try provides safer handling.

import scala.util.{Try, Success, Failure}

// Basic string to numeric conversions
val str1 = "42"
val int1: Int = str1.toInt              // 42
val double1: Double = str1.toDouble     // 42.0

val str2 = "3.14159"
val double2: Double = str2.toDouble     // 3.14159

// Exception on invalid input
try {
  val invalid: Int = "not a number".toInt
} catch {
  case e: NumberFormatException => 
    println(s"Parse error: ${e.getMessage}")
}

// Safe parsing with Try
def parseIntSafe(str: String): Option[Int] = {
  Try(str.toInt).toOption
}

def parseDoubleSafe(str: String): Option[Double] = {
  Try(str.toDouble).toOption
}

val inputs = List("42", "invalid", "3.14", "100")
val parsed = inputs.flatMap(parseIntSafe)
println(parsed)  // List(42, 100)

Boolean and other type conversions:

// String to Boolean
val boolStr1 = "true"
val bool1: Boolean = boolStr1.toBoolean  // true

val boolStr2 = "false"
val bool2: Boolean = boolStr2.toBoolean  // false

// Any non "true" string becomes false (case-insensitive)
val bool3: Boolean = "yes".toBoolean     // false
val bool4: Boolean = "TRUE".toBoolean    // true

// Numeric to String
val num = 42
val numStr: String = num.toString        // "42"

// Custom parsing logic
def parseBoolean(str: String): Option[Boolean] = {
  str.toLowerCase match {
    case "true" | "yes" | "1" => Some(true)
    case "false" | "no" | "0" => Some(false)
    case _ => None
  }
}

AnyVal and AnyRef Conversions

Understanding Scala’s type hierarchy helps with conversions between value types (AnyVal) and reference types (AnyRef).

// Boxing: AnyVal -> AnyRef (automatic)
val intValue: Int = 42
val boxedInt: AnyRef = intValue  // Boxed to java.lang.Integer

// Unboxing: AnyRef -> AnyVal (explicit)
val anyRef: AnyRef = java.lang.Integer.valueOf(42)
val unboxedInt: Int = anyRef.asInstanceOf[Int]

// Working with Any
def processAny(value: Any): String = value match {
  case i: Int => s"Integer: $i"
  case s: String => s"String: $s"
  case d: Double => s"Double: $d"
  case _ => s"Other: ${value.getClass.getSimpleName}"
}

println(processAny(42))        // "Integer: 42"
println(processAny("hello"))   // "String: hello"
println(processAny(3.14))      // "Double: 3.14"

Custom Type Conversions with Implicit Classes

Scala allows defining custom conversions through implicit classes, enabling fluent type conversion APIs.

object Conversions {
  implicit class StringOps(val str: String) extends AnyVal {
    def toIntOption: Option[Int] = Try(str.toInt).toOption
    def toDoubleOption: Option[Double] = Try(str.toDouble).toOption
    def toBooleanOption: Option[Boolean] = Try(str.toBoolean).toOption
  }
  
  implicit class NumericOps(val num: Double) extends AnyVal {
    def toIntSafe: Option[Int] = {
      if (num >= Int.MinValue && num <= Int.MaxValue) {
        Some(num.toInt)
      } else {
        None
      }
    }
  }
}

import Conversions._

val result1 = "42".toIntOption           // Some(42)
val result2 = "invalid".toIntOption      // None
val result3 = "3.14".toDoubleOption      // Some(3.14)
val result4 = 999999999999.0.toIntSafe   // None (overflow)

This approach provides chainable, safe conversions without cluttering code with explicit Try-catch blocks, making type conversions more idiomatic and maintainable in production Scala applications.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.