Skip to main content

Kotlin Type Checks

In programming, it's often necessary to check the type of an object before performing operations on it. Kotlin provides elegant ways to verify and cast types safely, preventing potential runtime errors while making your code more readable and concise.

Introduction to Type Checking

Type checking is the process of verifying whether an object belongs to a specific type or class. In Kotlin, this is particularly important due to the language's strong type system. Proper type checking helps to:

  • Prevent ClassCastExceptions
  • Enable safe downcasting
  • Write more robust conditional logic
  • Take advantage of Kotlin's smart cast feature

The is Operator

The is operator in Kotlin is used to check if an object conforms to a given type. It's similar to instanceof in Java but more powerful.

Basic Usage of is

kotlin
fun main() {
val object1: Any = "I am a string"
val object2: Any = 42

// Check if object is of specific type
if (object1 is String) {
println("object1 is a String")
} else {
println("object1 is not a String")
}

if (object2 is Int) {
println("object2 is an Int")
} else {
println("object2 is not an Int")
}
}

Output:

object1 is a String
object2 is an Int

You can also use the negated form with !is:

kotlin
fun main() {
val value: Any = 3.14

if (value !is String) {
println("value is not a String")
}

if (value !is Int) {
println("value is not an Int")
}
}

Output:

value is not a String
value is not an Int

Smart Casts

One of Kotlin's most powerful features is smart casting. When you use the is operator to check a type, Kotlin automatically casts the variable to that type within the scope where the check is true.

kotlin
fun main() {
val anyValue: Any = "Hello, Kotlin!"

// Check if anyValue is a String
if (anyValue is String) {
// Inside this block, anyValue is automatically cast to String
// You can directly call String methods without explicit casting
println("String length: ${anyValue.length}")
println("Uppercase: ${anyValue.uppercase()}")
}
}

Output:

String length: 14
Uppercase: HELLO, KOTLIN!

Notice that we didn't have to explicitly cast anyValue to access String methods like length and uppercase(). Kotlin did this automatically for us.

Smart Casts and Immutability

Smart casts only work for immutable values (val) or for variables that couldn't have changed since the check. This is because Kotlin needs to ensure the type hasn't changed between the check and usage.

kotlin
fun main() {
// Smart cast works with val
val immutableValue: Any = "I cannot change"
if (immutableValue is String) {
println(immutableValue.length) // Smart cast works
}

// Smart cast doesn't work with mutable properties
var mutableValue: Any = "I can change"
if (mutableValue is String) {
println(mutableValue.length) // Smart cast still works here
}

// But if the variable could be modified elsewhere:
externalFunction() // Might change mutableValue

if (mutableValue is String) {
// We would need an explicit cast here if externalFunction() could change mutableValue
println((mutableValue as String).length)
}
}

fun externalFunction() {
// Some code that could potentially modify variables
}

The as Operator (Type Casting)

When you need to explicitly cast a value to a specific type, you can use the as operator.

kotlin
fun main() {
val obj: Any = "Type casting example"

// Explicit casting using 'as'
val str = obj as String
println("The string has ${str.length} characters")

// This would throw ClassCastException
try {
val num = obj as Int
println(num)
} catch (e: Exception) {
println("Cast failed: ${e.message}")
}
}

Output:

The string has 21 characters
Cast failed: java.lang.String cannot be cast to java.lang.Integer

Safe Cast with as?

To avoid exceptions when casting, use the safe cast operator as?. It returns null if the casting isn't possible.

kotlin
fun main() {
val obj1: Any = "Safe casting"
val obj2: Any = 42

// Safe casts
val str = obj1 as? String // Works, returns "Safe casting"
val num = obj2 as? Int // Works, returns 42
val failed = obj2 as? String // Cast isn't possible, returns null

println("str = $str")
println("num = $num")
println("failed = $failed")

// Using with null check
val safeString = obj2 as? String
if (safeString != null) {
println("Length: ${safeString.length}")
} else {
println("The object is not a String")
}
}

Output:

str = Safe casting
num = 42
failed = null
The object is not a String

Real-World Application: Processing Different Types

Let's look at a practical example of using type checks to process different types in a collection:

kotlin
fun main() {
// A list containing various types
val mixed = listOf("Kotlin", 42, 3.14, true, "Programming")

// Process each element based on its type
for (item in mixed) {
processItem(item)
}
}

fun processItem(item: Any) {
when (item) {
is String -> println("String: \"$item\" (length: ${item.length})")
is Int -> println("Integer: $item (doubled: ${item * 2})")
is Double -> println("Double: $item (squared: ${item * item})")
is Boolean -> println("Boolean: $item (inverted: ${!item})")
else -> println("Unknown type: $item")
}
}

Output:

String: "Kotlin" (length: 6)
Integer: 42 (doubled: 84)
Double: 3.14 (squared: 9.8596)
Boolean: true (inverted: false)
String: "Programming" (length: 11)

This example demonstrates how to use type checking with when expressions, which is a common pattern in Kotlin.

Another Real-World Example: Handling UI Events

In Android development, you might need to handle different types of UI events:

kotlin
interface UIEvent
data class ClickEvent(val x: Int, val y: Int) : UIEvent
data class KeyEvent(val keyCode: Int) : UIEvent
data class TouchEvent(val pressure: Float) : UIEvent

fun handleUIEvent(event: UIEvent) {
when (event) {
is ClickEvent -> {
println("Click at coordinates (${event.x}, ${event.y})")
// Perform click specific operations
}
is KeyEvent -> {
println("Key pressed with code ${event.keyCode}")
// Perform key press specific operations
}
is TouchEvent -> {
println("Touch with pressure ${event.pressure}")
// Perform touch specific operations
}
else -> {
println("Unknown event type")
}
}
}

fun main() {
val events = listOf(
ClickEvent(10, 20),
KeyEvent(65), // 'A' key
TouchEvent(0.75f)
)

for (event in events) {
handleUIEvent(event)
}
}

Output:

Click at coordinates (10, 20)
Key pressed with code 65
Touch with pressure 0.75

Summary

Kotlin's type checking mechanisms provide a robust way to work with different types safely:

  • is operator: Checks if a value is of a specific type
  • Smart Casts: Automatically casts a value after a type check, making your code cleaner
  • as operator: Explicitly casts to a type (can throw an exception if the cast is invalid)
  • as? operator: Safely casts to a type, returning null if the cast isn't possible

Type checking in Kotlin is not only safer but also more concise than in many other languages. By leveraging these features, you can write code that is both robust and readable.

Exercises

  1. Create a function that takes an Any parameter and returns its string representation in a standardized format for various types.
  2. Implement a simple calculator that processes a list of operations represented as different classes (Addition, Subtraction, etc.).
  3. Write a function that safely extracts information from different data types in a collection.
  4. Create a hierarchy of shapes and a function that calculates the area of any shape using type checks.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)