1. Introduction
In this tutorial, we’re going to explore various data types in Scala. We’ll look at how these types are associated with data, the operations we can run on them, and the interoperability with Java.
2. Common Types
2.1. Value Types
As with other programming languages, Scala provides numeric types. There are 7 such types:
- Char (16-bit unsigned Unicode character)
- Byte (8-bit signed value)
- Short (16-bit signed value)
- Int (32-bit signed value)
- Long (64-bit signed value)
- Float (32-bit IEEE 754 single-precision floating-point value)
- Double (64-bit IEEE 754 single-precision floating-point value)
In addition to these, we have 2 more value types:
- Boolean (can have true or false values)
- Unit (carries no meaningful information)
Each has a counterpart in Java. The difference is in the way they are represented. In Scala, these types wrap around the primitive types, and any operation on them is a function call.
In the next section, we’ll have a look at each of these types with examples.
2.2. Byte and Char
A Byte is a signed 8-bit value that can hold values ranging from -128 to 127. A Char is a 16-bit value and it’s used to store human-readable characters.
Let’s take a look at some Byte and Char variables:
val b1: Byte = 100
val c1: Char = 'A'
// val b3: Byte = 169 (will not compile)
A Byte can hold signed 8-bit values. Since the value 169 is beyond that range, the compiler will complain. If we assign 169 to a Char, it’s interpreted as an ASCII Copyright character:
b1 should be(100)
c1 should be('A')
2.3. Integer, Long and Short
An Integer is a 32-bit value and is central to any numeric representation in Scala. The Long and Short types are similar to Integer.
Let’s see an example of how they’re used:
val l1: Long = 65536
val i3: Int = 32768
val s1: Short = 32767
// val s2: Short = 32768 (will not compile)
We can see that we can not assign a value that’s beyond the range which a Short can hold.
2.4. Booleans
Boolean values are the simplest. Unsurprisingly, we can assign either of two values to a variable of Boolean type.
val trueVal = true
val falseVal = !true
val falseValOtherWay = !true
trueVal should be (true)
falseVal should be (false)
2.5. Float and Double
Float and Double types are used to hold real numbers, associated with precisions:
val f1 = 12.05f // 'f' signifies that it is a Float
val d1 = 12.3495067 // inferred as a Double
val d2 = 12.3495067D // 'D' signfies a Double, but is optional
Placing a ‘D’ at the end is optional for a variable of type Double, However, the ‘f’ at the end of the Float is mandatory.
2.6. Literals and Type Inference
Scala doesn’t mandate that the type of the variable always be provided while declaring them. The Scala compiler attempts to infer the type based on what it’s being initialized with.
This is where literals come into play. Let’s see a few examples of defining literals and the type that’s inferred:
// Types are inferred from the values assigned
val i5 = 1234 // inferred as an Int
val i6 = 0xAFBF // HEX value, inferred as an Int
val c5 = 234 // inferred as an Int
val c6 ='®' // inferred as a Char
val l4 = 1234L // 'L' signifies a Long literal
val l3 = 0xCAFEBABEL // 'L' signifies a Long literal, even for a HEX value
val trueVal = true // inferred as a Boolean
val falseVal = !true // inferred as a Boolean
Integer literals for the types Int and Long come in two forms: decimal and hexadecimal. Literals that represent Char types are single-quoted. These types can be any Unicode character, within a pair of single quotes.
For Byte and Short, however, the literals are not enough. This is because, the values which a Short or a Byte can hold form a subset of the values which an Int can hold. Therefore, we have to specify the type:
// Types are inferred from the values assigned
val s2: Short = 32767 // qualify with intended type
val b1: Byte = 127 // qualify with intended type
2.7. String
Now that we’ve seen what literals bring to Scala, we can now see how a String is initialized in Scala:
val name = "Diego Armando Maradona"
val nameWithQuote = "Kerry O\'keffey"
val ageAsString = "100"
A String is encased in a pair of double-quotes. A backslash (\) is used to retain the special characters inside the value, like the single quote in the example above.
3. Conversions Between Types
3.1. Conversion from and to Int
Scala allows conversion of values from other data types to Int and vice versa. Of course, a few rules have to be followed:
// val justAByte: Byte = 129 // Beyond the range of values allowed for Byte, will not compile
val aByteSizedInteger = 127
val byteFromInt = aByteSizedInteger.toByte
val aNonByteInteger = 128
val wrappedByte = aNonByteInteger.toByte // wrapped around
byteFromInt should be (127)
wrappedByte should be (-128)
Scala doesn’t allow us to directly assign a value that is beyond the limit that it can hold to a Byte. However, using conversion function toByte() on an Int, we get a much more predictable and logical result.
Scala wraps around the value that is assigned. This wrapped around value can be easily brought back to an Int also.
3.2. Conversion From Char or String to Int
We can convert a Char or a String to an Int:
val c4: Char = '®'
val symbolToInt = c4.toInt // from a Char
val stringToInt = "100".toInt
What if a String contains non-numeric characters? Can we convert that to an Int? As expected, we will be caught for the mischief:
val i = "ABC".toInt
java.lang.NumberFormatException: For input string: "ABC"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at scala.collection.immutable.StringLike.toInt(StringLike.scala:304)
at scala.collection.immutable.StringLike.toInt$(StringLike.scala:304)
at scala.collection.immutable.StringOps.toInt(StringOps.scala:33)
... 28 elided
3.3. Longs Need Special Mention
The values that a Long can hold are Integers in essence but can hold a much larger range of values. Conversions from Long to Int and vice versa, work under a few constraints:
val l = 65536L // Long literal
val i1 = 65536 // No qualifier, therefore Int
val i2 = l // i2 is a Long because RHS is a Long
// The following line will not compile because Long, a larger size is being cast into Into, a smaller size
// val i2: Int = l // the target variable's type is explicit: an Int
val i3 = l.toInt // Conversion helper function works
val s3 = l.toShort
l should be (65536L)
i1 should be (65536)
i2 should equal(i1.toLong)
i3 should equal(i1)
s3 should be (0) // Oops! A Short is too small for a large value as 65536
Assigning an Int to a Long is safe because the range of values that Long can hold is much larger than that of Int. However, the reverse isn’t safe. This is because, during the assignment, the value may lose precision. That’s why the compiler refuses to assign silently and indicates a type-mismatch instead.
However, if we know what we are doing, we can use the correct toType() function. Since a Short cannot hold a value of 65536, the toShort() function reduces it to zero, indicating that assignment results in shrinking the value.
3.4. Guarding Against Wrong Conversions
Because Scala imposes certain rules governing type conversions, we may need to know beforehand if a value is assignable to a type. To do this, we can make use helpful functions on these types:
val i10 = 127 // Integer 127
val i11 = 128 // Integer 128
i11.isValidByte should be (false)
i10.isValidByte should be (true)
val i31 = 65536
i31.isValidShort should be (false)
val d3 = 2E31
d3.isValidInt should be (false)
The helper functions of the type isValid{type-name}() can help us verify that the value of a variable is within the range that its type can hold.
4. Value and Reference: the Type Hierarchy
Scala follows a well-defined hierarchy of types. The basic data types described above, other than Strings, are all Values in nature. Unsurprisingly, these are also called Value Classes. These Value Classes inherit from a type supplied by Scala, named AnyVal.
On the other hand, String and other Classes which we write (generally referred to as User-defined Class) while implementing a solution, inherit from another type supplied by Scala, named AnyRef:
class Article(heading: String, noOfLines: Int)
val i: Int = 1234
val a1: AnyVal = i // casting an Int to AnyVal
val article = new Article("Baeldung",2000) // an application class
val author = "Eugene"
val parentClass1: AnyRef = article // casting an User-Defined Object to an AnyRef
val parentClass2: AnyRef = author // casting a String to an AnyRef
All classes which we use in Scala, are subclasses of either AnyVal or AnyRef.
5. Is Any Type Left?
Any is the parent of all types. In other words, Any is the supertype of all types, also called the top-type.
It defines certain universal methods such as equals(), hashCode(), and toString(). The two direct subclasses of Any are AnyVal and AnyRef.
6. Conclusion
In this article, we’ve looked at various data types in Scala. We’ve observed how the Scala compiler governs the assignments. Additionally, we have noted the hierarchy of classes that the Scala compiler imposes on the design, with Any being the root of all types.
The code associated with this article can be found over on GitHub.