Type Checks with is and !is
Use the is operator to verify if an object matches a specific type at runtime. The negation !is checks if an object does not belong to a type.
val inputObj: Any = "Hello Kotlin"
if (inputObj is String) {
println("String length: ${inputObj.length}")
}
if (inputObj !is String) { // Equivalent to !(inputObj is String)
println("Not a String instance")
} else {
println("String length: ${inputObj.length}")
}
Smart Casts
In most scenarios, explicit type casts are unnecessary in Kotlin. The compiler tracks is checks for immutable values and automatical inserts safe casts when needed.
fun showSmartCast(value: Any) {
if (value is String) {
// `value` is automatically cast to String here
println("Processed string length: ${value.length}")
}
}
The compiler recognizes safe casts even when a negative check triggers an early return:
fun processValue(value: Any) {
if (value !is String) return
// `value` is cast to String after the early return
println("Valid string length: ${value.length}")
}
Smart casts also work in conditions using && and ||:
// `value` is cast to String on the right side of ||
if (value !is String || value.isEmpty()) return
// `value` is cast to String on the right side of &&
if (value is String && value.length > 5) {
println("Long string detected: ${value.length} characters")
}
For when expressions and while loops, the compiler applies smart casts similarly:
fun evaluateInput(inputValue: Any) {
when (inputValue) {
is Int -> println("Incremented value: ${inputValue + 2}")
is String -> println("String length + 1: ${inputValue.length + 1}")
is DoubleArray -> println("Array sum: ${inputValue.sum()}")
}
}
Rules for Smart Cast Eligibility
- Local
valvariables: Always eligible, except for local delegated properties. - Class
valproperties: Eligible if the property isprivateorinternal, and the check occurs within the same module. Not eligible foropenproperties or those with custom getters. - Local
varvariables: Eligible only if the variable is not modified between the check and usage, not captured in a modifying lambda, and not a local delegated property. - Class
varproperties: Never eligible, since they can be modified externally at any time.
Unsafe Type Casts with as
The as operator performs an unsafe cast that throws a ClassCastException if the conversion is not possible. To handle nullable sources, use a nullable type in the cast target:
val unknownVal: Any? = "Test String"
// Throws ClassCastException if unknownVal is not a String
val nonNullableStr: String = unknownVal as String
// Safe for nullable sources by casting to a nullable String type
val nullableStr: String? = unknownVal as String?
Safe Nullable Casts with as?
To avoid exceptions during failed casts, use the as? operator, wich returns null instead of throwing an error:
val randomObj: Any = 42
val castResult: String? = randomObj as? String // Returns null instead of throwing
Type Erasure and Generic Type Checks
Kotlin anforces generic type safety at compile time, but generic type parameters are erased at runtime. This means you cannot check if an instance belongs to a specific generic type like List<Int> at runtime.
You can check for a star-projected generic type:
fun processGenericItem(something: Any) {
if (something is List<*>) {
something.forEach { println("Item: $it") } // Items are typed as Any?
}
}
If the generic type's parameters are already validated at compile time, you can check the non-generic part of the type (omitting the angle brackets):
fun processStringList(stringList: List<String>) {
if (stringList is ArrayList) {
// `stringList` is smart-cast to ArrayList<String>
println("ArrayList capacity: ${stringList.capacity()}")
}
}
Reified Type Parameters for Inline Functions
Inline functions with reified type parameters preserve type information at runtime, allowing checks like arg is T. However, type erasure still applies to the generic parameters of the argument itself:
inline fun <reified FirstType, reified SecondType> Pair<*, *>.castToPair(): Pair<FirstType, SecondType>? {
if (first !is FirstType || second !is SecondType) return null
return first as FirstType to second as SecondType
}
val mixedPair: Pair<Any?, Any?> = "sample text" to setOf(4, 5, 6)
val stringToAny = mixedPair.castToPair<String, Any>()
val stringToInt = mixedPair.castToPair<String, Int>()
val stringToSet = mixedPair.castToPair<String, Set<*>>()
val stringToIntSet = mixedPair.castToPair<String, Set<Int>>() // Bypasses type safety
Unchecked Type Casts
When type erasure prevents runtime validation but you have logical guarantees of type safety, you can use an unchecked cast. The compiler will issue a warning, which can be suppressed with @Suppress("UNCHECKED_CAST"):
fun readConfigMap(file: File): Map<String, *> = file.inputStream().use {
TODO("Implement map parsing from file input stream")
}
// Assume the file contains a Map<String, Boolean>
val configFile = File("boolean_config.map")
@Suppress("UNCHECKED_CAST")
val booleanConfig: Map<String, Boolean> = readConfigMap(configFile) as Map<String, Boolean>
For inline functions with reified types, you can create type-safe unchecked casts:
inline fun <reified T> List<*>.filterAndCastToList(): List<T>? =
if (all { it is T }) {
@Suppress("UNCHECKED_CAST")
this as List<T>
} else null
On the JVM, array types retain element type information (excluding nullability and generic parameters). Casts to array types are partially checked:
// Cast succeeds even if the array contains nullable or non-null List instances
val genericArray: Array<*> = arrayOf(listOf("a"), null)
val castArray = genericArray as Array<List<String>?>