Smart Casts Workbook
Ten short exercises on Kotlin smart casts — is and !is, null-check narrowing, smart casts in when, combined conditions, early returns, and where they don't apply.
Practice problems for Kotlin Smart Casts. Each takes a minute or two. A few auto-check: implement the function in the editor and press Run — hidden tests go green when you’re right and red (with a hint) when you’re not, all on JetBrains’ Kotlin server. The rest — the ones that print or make a point about the compiler — stay attempt-then-reveal: press Run to try them, then click Show answer. Nothing here is a trick question, just direct practice of the syntax from the lesson.
the basic cast
1. Check, then use
Given x: Any, print its length when it’s a String — without writing a cast.
fun main() {
val x: Any = "hello"
if (x is String) {
println(x) // TODO: print x.length instead (x is smart-cast to String here)
}
} Show answer Hide answer
if (x is String) {
println(x.length) // x is smart-cast to String here
} 2. Negate and return early
Given x: Any, return early when it is not a String; afterward, use it as a String.
fun main() {
val x: Any = "hello"
// TODO: return early when x is NOT a String
println(x) // TODO: after the guard, print x.length (x is String here)
} Show answer Hide answer
if (x !is String) return
println(x.length) // x is String from here on 3. Null check narrows
Given name: String?, print its length only when it isn’t null — no safe call, no !!.
fun main() {
val name: String? = "Ada"
if (name != null) {
println(name) // TODO: print name.length (name is smart-cast to String)
}
} Show answer Hide answer
if (name != null) {
println(name.length) // name is smart-cast to String
} in when and conditions
4. Smart cast inside when
Implement incIfInt so it returns x + 1 when x is an Int, else 0 — using the smart cast.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun incIfInt() {
Assert.assertEquals("an Int is incremented", 42, incIfInt(41))
Assert.assertEquals("anything else is 0", 0, incIfInt("hi"))
}
}
//sampleStart
fun incIfInt(x: Any): Int =
0 // TODO: x + 1 when x is Int
//sampleEnd Show answer Hide answer
fun incIfInt(x: Any) = when (x) {
is Int -> x + 1
else -> 0
} 5. Combined condition
Implement isNonEmptyString so it returns true only when x is a non-empty String.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun isNonEmptyString() {
Assert.assertTrue("a non-empty String is true", isNonEmptyString("hi"))
Assert.assertFalse("an empty String is false", isNonEmptyString(""))
Assert.assertFalse("a non-String is false", isNonEmptyString(42))
}
}
//sampleStart
fun isNonEmptyString(x: Any): Boolean =
x is String // TODO: also require the String to be non-empty
//sampleEnd Show answer Hide answer
fun isNonEmptyString(x: Any) = x is String && x.isNotEmpty()After the is check on the left, x is smart-cast for the right-hand side.
6. Elvis with an early exit
Given value: Any, bind a String named s or return early if value isn’t one. Use a safe cast.
fun main() {
val value: Any = "hello"
val s = value as String // TODO: use a safe cast (as?) with an Elvis return instead
println(s.uppercase()) // want HELLO
} Show answer Hide answer
val s = value as? String ?: return
println(s.uppercase()) the edges
7. Why won’t this smart-cast?
This fails to compile: a nullable var property name accessed as name.length after a null check. Why, and how do you fix it?
fun main() {
class User(var name: String?)
val user = User("Ada")
if (user.name != null) {
// Printing user.name.length here won't compile: a var property could
// change between the check and the use, so it isn't smart-cast.
// TODO: copy user.name into a local val, then print its length.
println(user.name)
}
} Show answer Hide answer
A var property could change between the check and the use, so the compiler won’t smart-cast it. Copy it into a local val first:
val n = name
if (n != null) println(n.length) 8. After a throw
Given config: Config?, throw IllegalStateException when it’s null; afterward use it as non-null.
fun main() {
class Config { fun load() = println("loaded") }
val config: Config? = Config()
if (config == null) return // TODO: throw IllegalStateException("missing config") instead
config.load() // config is non-null from here on
} Show answer Hide answer
if (config == null) throw IllegalStateException("missing config")
config.load() // config is non-null here 9. When smart casts can’t help
You only have a val x: Any, and you’re certain it’s a String, but there’s no check in the code path. Write the explicit cast to call .length.
fun main() {
val x: Any = "hello"
val length = 0 // TODO: cast x to String explicitly and take its length
println(length) // want 5
} Show answer Hide answer
(x as String).lengthA plain as throws if you’re wrong; prefer a checked is or a safe as? when you aren’t certain.
10. Combine type and null
Implement lengthOrMinusOne so it returns the length when x is a non-null String, else -1.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun lengthOrMinusOne() {
Assert.assertEquals("a String gives its length", 5, lengthOrMinusOne("hello"))
Assert.assertEquals("a non-String gives -1", -1, lengthOrMinusOne(42))
Assert.assertEquals("null gives -1", -1, lengthOrMinusOne(null))
}
}
//sampleStart
fun lengthOrMinusOne(x: Any?): Int =
if (x is String) 0 else -1 // TODO: return x.length in the String branch
//sampleEnd Show answer Hide answer
fun lengthOrMinusOne(x: Any?) = if (x is String) x.length else -1A smart cast to String also rules out null, since String is non-nullable.
Going deeper: stability and the fix
12. Early-return narrowing
Use a !is guard with an early return so that, past the guard, x is a String with no cast. Return its length, or -1 when it isn’t a string.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun len() {
Assert.assertEquals(2, lengthOf("hi"))
Assert.assertEquals(-1, lengthOf(42))
}
}
//sampleStart
fun lengthOf(x: Any): Int {
// TODO: if x isn't a String, return -1; otherwise return its length
return -1
}
//sampleEnd Show answer Hide answer
fun lengthOf(x: Any): Int {
if (x !is String) return -1
return x.length // smart-cast to String from here on
}The smart cast follows control flow: past the return, the only surviving type is String.
13. Why won’t it compile?
This fails: if (user.name != null) println(user.name.length) where name is a var property. Fix it so it compiles, without changing User.
import org.junit.Test
import org.junit.Assert
class User(var name: String?)
class Test {
@Test fun len() {
Assert.assertEquals(3, nameLength(User("Ada")))
Assert.assertEquals(0, nameLength(User(null)))
}
}
//sampleStart
fun nameLength(user: User): Int {
// TODO: return user.name's length, or 0 when null — make it compile
return 0
}
//sampleEnd Show answer Hide answer
fun nameLength(user: User): Int {
val name = user.name // stable local snapshot
return if (name != null) name.length else 0
}A mutable property isn’t stable — a custom getter or another thread could change it between the check and the use — so the compiler refuses to smart-cast it. Copy into a local val (or use user.name?.length ?: 0).
Back to the lesson, Kotlin Smart Casts, or on to the next one: null safety.
Comments