# Downcasting
Downcasting discovers the specific type of a previously-upcast object.
Upcasts are always safe because the base class cannot have a bigger interface than the derived class. Every base-class member is guaranteed to exist and is therefore safe to call. Although object-oriented programming is primarily focused on upcasting, there are situations where downcasting can be a useful and expedient approach.
Downcasting happens at runtime, and is also called run-time type identification (RTTI).
Consider a class hierarchy where the base type has a narrower interface than the derived types. If you upcast an object to the base type, the compiler no longer knows the specific type. In particular, it cannot know what extended functions are safe to call:
// DownCasting/NarrowingUpcast.kt
package downcasting
interface Base {
fun f()
}
class Derived1 : Base {
override fun f() {}
fun g() {}
}
class Derived2 : Base {
override fun f() {}
fun h() {}
}
fun main() {
val b1: Base = Derived1() // Upcast
b1.f() // Part of Base
// b1.g() // Not part of Base
val b2: Base = Derived2() // Upcast
b2.f() // Part of Base
// b2.h() // Not part of Base
}
To solve this problem, there must be some way to guarantee that a downcast is correct, so you don’t accidentally cast to the wrong type and call a non-existent member.
# Smart Casts
Smart casts in Kotlin are automatic downcasts. The is
keyword checks whether an object is a particular type. Any code within the scope of that check assumes that it is that type:
// DownCasting/IsKeyword.kt
import downcasting.*
fun main() {
val b1: Base = Derived1() // Upcast
if(b1 is Derived1)
b1.g() // Within scope of "is" check
val b2: Base = Derived2() // Upcast
if(b2 is Derived2)
b2.h() // Within scope of "is" check
}
If b1
is of type Derived1
, you can call g()
. If b2
is of type Derived2
, you can call h()
.
Smart casts are especially useful inside when
expressions that use is
to search for the type of the when
argument. Note that, in main()
, each specific type is first upcast to a Creature
, then passed to what()
:
// DownCasting/Creature.kt
package downcasting
import atomictest.eq
interface Creature
class Human : Creature {
fun greeting() = "I'm Human"
}
class Dog : Creature {
fun bark() = "Yip!"
}
class Alien : Creature {
fun mobility() = "Three legs"
}
fun what(c: Creature): String =
when (c) {
is Human -> c.greeting()
is Dog -> c.bark()
is Alien -> c.mobility()
else -> "Something else"
}
fun main() {
val c: Creature = Human()
what(c) eq "I'm Human"
what(Dog()) eq "Yip!"
what(Alien()) eq "Three legs"
class Who : Creature
what(Who()) eq "Something else"
}
In main()
, upcasting happens when assigning a Human
to Creature
, passing a Dog
to what()
, passing an Alien
to what()
, and passing a Who
to what()
.
Class hierarchies are traditionally drawn with the base class at the top and derived classes fanning down below it. what()
takes a previously-upcast Creature
and discovers its exact type, thus casting that Creature
object down the inheritance hierarchy, from the more-general base class to a more-specific derived class.
A when
expression that produces a value requires an else
branch to capture all remaining possibilities. In main()
, the else
branch is tested using an instance of the local class Who
.
Each branch of the when
uses c
as if it is the type we checked for: calling greeting()
if c
is Human
, bark()
if it’s a Dog
and mobility()
if it’s an Alien
.
# The Modifiable Reference
Automatic downcasts are subject to a special constraint. If the base-class reference to the object is modifiable (a var
), then there’s a possibility that this reference could be assigned to a different object between the instant that the type is detected and the instant when you call specific functions on the downcast object. That is, the specific type of the object might change between type detection and use.
In the following, c
is the argument to when
, and Kotlin insists that this argument be immutable so that it cannot change between the is
expression and the call made after the ->
:
// DownCasting/MutableSmartCast.kt
package downcasting
class SmartCast1(val c: Creature) {
fun contact() {
when (c) {
is Human -> c.greeting()
is Dog -> c.bark()
is Alien -> c.mobility()
}
}
}
class SmartCast2(var c: Creature) {
fun contact() {
when (val c = c) { // [1]
is Human -> c.greeting() // [2]
is Dog -> c.bark()
is Alien -> c.mobility()
}
}
}
The c
constructor argument is a val
in SmartCast1
and a var
in SmartCast2
. In both cases c
is passed into the when
expression, which uses a series of smart casts.
In [1], the expression val c = c
looks odd, and only used here for convenience—we don’t recommend “shadowing” identifier names in normal code. val c
creates a new local identifier c
that captures the value of the property c
. However, the property c
is a var
while the local (shadowed) c
is a val
. Try removing the val c =
. This means that the c
will now be the property, which is a var
. This produces an error message for line [2]:
- Smart cast to ‘Human’ is impossible, because ‘c’ is a mutable property that could have been changed by this time
is Dog
and is Alien
produce similar messages. This is not limited to while
expressions; there are other situations that can produce the same error message.
The change described in the error message typically happens through concurrency, when multiple independent tasks have the opportunity to change c
at unpredictable times. (Concurrency is an advanced topic that we do not cover in this book).
Kotlin forces us to ensure that c
will not change from the time that the is
check is performed and the time that c
is used as the downcast type. SmartCast1
does this by making the c
property a val
, and SmartCast2
does it by introducing the local val c
.
Similarly, complex expressions cannot be smart-cast because the expression might be re-evaluated. Properties that are open
for inheritance can’t be smart-cast because their value might be overridden in subclasses, so there’s no guarantee the value will be the same on the next access.
# The as
Keyword
The as
keyword forcefully casts a general type to a specific type:
// DownCasting/Unsafe.kt
package downcasting
import atomictest.*
fun dogBarkUnsafe(c: Creature) =
(c as Dog).bark()
fun dogBarkUnsafe2(c: Creature): String {
c as Dog
c.bark()
return c.bark() + c.bark()
}
fun main() {
dogBarkUnsafe(Dog()) eq "Yip!"
dogBarkUnsafe2(Dog()) eq "Yip!Yip!"
(capture {
dogBarkUnsafe(Human())
}) contains listOf("ClassCastException")
}
dogBarkUnsafe2()
shows a second form of as
: if you say c as Dog
, then c
is treated as a Dog
throughout the rest of the scope.
A failing as
cast throws a ClassCastException
. A plain as
is called an unsafe cast.
When a safe cast as?
fails, it doesn’t throw an exception, but instead returns null
. You must then do something reasonable with that null
to prevent a later NullPointerException
. The Elvis operator (described in [Safe Calls & the Elvis Operator](javascript:void(0))) is usually the most straightforward approach:
// DownCasting/Safe.kt
package downcasting
import atomictest.eq
fun dogBarkSafe(c: Creature) =
(c as? Dog)?.bark() ?: "Not a Dog"
fun main() {
dogBarkSafe(Dog()) eq "Yip!"
dogBarkSafe(Human()) eq "Not a Dog"
}
If c
is not a Dog
, as?
produces a null
. Thus, (c as? Dog)
is a nullable expression and we must use the safe call operator ?.
to call bark()
. If as?
produces a null
, then the whole expression (c as? Dog)?.bark()
will also produce a null
, which the Elvis operator handles by producing "Not a Dog"
.
# Discovering Types in Lists
When used in a predicate, is
finds objects of a given type within a List
, or any iterable (something you can iterate through):
// DownCasting/FindType.kt
package downcasting
import atomictest.eq
val group: List<Creature> = listOf(
Human(), Human(), Dog(), Alien(), Dog()
)
fun main() {
val dog = group
.find { it is Dog } as Dog? // [1]
dog?.bark() eq "Yip!" // [2]
}
Because group
contains Creature
s, find()
returns a Creature
. We want to treat it as a Dog
, so we explicitly cast it at the end of line [1]. There might be zero Dog
s in group
, in which case find()
returns a null
so we must cast the result to a nullable Dog?
. Because dog
is nullable, we use the safe call operator in line [2].
You can usually avoid the code in line [1] by using filterIsInstance()
, which produces all elements of a specific type:
// DownCasting/FilterIsInstance.kt
import downcasting.*
import atomictest.eq
fun main() {
val humans1: List<Creature> =
group.filter { it is Human }
humans1.size eq 2
val humans2: List<Human> =
group.filterIsInstance<Human>()
humans2 eq humans1
}
filterIsInstance()
is a more readable way to produce the same result as filter()
. However, the result types are different: while filter()
returns a List
of Creature
(even though all the resulting elements are Human
), filterIsInstance()
returns a list of the target type Human
. We’ve also eliminated the nullability issues seen in FindType.kt
.
Exercises and solutions can be found at www.AtomicKotlin.com.