Inheritance
As with many object-oriented languages, Kotlin supports class inheritance. A subclass inherits from a superclass, and what it “inherits” are all of the properties, functions, constructors, and other stuff that the superclass makes available.
What Happens By Default
The various versions of the Foo
class shown above do not inherit explicitly from anything. The default base class for a Kotlin class is Any
, and so Foo
is said to derive from Any
.
Extending a Class
In Java, we use the extends
keyword to denote inheritance:
class Foo extends Bar {
}
Ruby uses the <
symbol:
class Foo < Bar
end
Kotlin is a bit closer to Ruby syntax, but it uses a colon as the symbol:
class Foo(var count: Int) : Any() {
fun something() {
count += 1
println("something() was called $count times")
}
}
fun main() {
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
println("the final count was ${foo.count}")
}
Here, we are explicitly declaring that the superclass of Foo
is Any
.
Chaining to the Superclass Constructors
The ()
at the end of Any()
in the above example is actually a call to the superclass constructor. You will need to supply parameters to that call, if the superclass requires them:
open class Base(val tag: String)
class Foo(var count: Int) : Base("Foo Example") {
fun something() {
count += 1
println("$tag: something() was called $count times")
}
}
fun main() {
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
println("the final count was ${foo.count}")
}
Here, Base
has a constructor requiring a tag
parameter. Foo
needs to supply that parameter when it calls the Base()
constructor. But then Foo
can reference the tag
property that it inherits, including it as part of the printed output:
Foo Example: something() was called 1 times
Foo Example: something() was called 2 times
Foo Example: something() was called 3 times
the final count was 3
Open Classes
You will notice that the declaration of Base
has something beyond a constructor parameter: it is prefixed with the open
keyword.
In Java, by default, any class can be extended by a subclass. To prevent a class from being extended, you have to mark it as final
.
Kotlin goes the opposite route. Kotlin classes cannot be extended by default. You have to mark classes that can be extended using open
. Failure to do that results in an error. So, if we remove the open
from the previous example:
class Base(val tag: String)
class Foo(var count: Int) : Base("Foo Example") {
fun something() {
count += 1
println("$tag: something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
…we get a compile error:
error: this type is final, so it cannot be inherited from
class Foo(var count: Int) : Base("Foo Example") {
^
This has an important side effect: you cannot create subclasses of arbitrary classes that you do not control. Only if the implementer of that other class is willing to support subclasses will the implementer use the open
keyword, and only those classes can you extend.
This approach is uncommon in major programming languages. Basically, Kotlin recognizes that inheritance is a potentially fragile relationship between two classes. Ideally, the classes adhere to the Liskov substitution principle. This basically states that instances of a subclass should be usable anywhere that instances of the superclass can be used. A programming language cannot guarantee that, and developers sometimes get sloppy when creating subclasses. So, Kotlin forces developers to make a conscious decision to allow subclasses, in hopes that it helps those developers take appropriate defensive programming steps to ensure that subclasses behave as expected.
Overriding Functions
As you might expect, a subclass can call functions that it inherits from its superclass:
open class Base(val tag: String) {
fun gimmeTheTag() = tag
}
class Foo(var count: Int) : Base("Foo Example") {
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
fun main() {
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
println("the final count was ${foo.count}")
}
And, as you might expect, a subclass can override functions of its superclasses… at least, if those too are marked as open
. By default, a Kotlin function is not open and cannot be overridden. Not only must the superclass function be marked as open
, but the overriding function must be marked with override
:
open class Base(val tag: String) {
open fun gimmeTheTag() = tag
}
class Foo(var count: Int) : Base("Foo Example") {
override fun gimmeTheTag() = "Not the Original Tag"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
fun main() {
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
println("the final count was ${foo.count}")
}
Here, gimmeTheTag()
is an open
function in Base
, so Foo
can override
it with its own implementation. The string interpolation will use the overridden function:
Not the Original Tag: something() was called 1 times
Not the Original Tag: something() was called 2 times
Not the Original Tag: something() was called 3 times
the final count was 3
By default, open
is transitive. Once a function is marked as open
, it is open
for all subclasses down the hierarchy, regardless of whether intervening classes override
the function or not:
open class Base(val tag: String) {
open fun gimmeTheTag() = tag
}
open class Intermezzo(val thing: String) : Base(thing)
class Foo(var count: Int) : Intermezzo("Foo Example") {
override fun gimmeTheTag() = "Not the Original Tag"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
fun main() {
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
println("the final count was ${foo.count}")
}
Here, even though Foo
now extends Intermezzo
, it can still override gimmeTheTag()
. It could do so even if Intermezzo
itself overrides gimmeTheTag()
… unless Intermezzo
marks its version of that function as final
:
open class Base(val tag: String) {
open fun gimmeTheTag() = tag
}
open class Intermezzo(val thing: String): Base(thing) {
final override fun gimmeTheTag() = "-$thing-"
}
class Foo(var count: Int) : Intermezzo("Foo Example") {
override fun gimmeTheTag() = "Not the Original Tag"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
This results in an error:
error: 'gimmeTheTag' in 'Intermezzo' is final and cannot be overridden
override fun gimmeTheTag() = "Not the Original Tag"
^
Once again, the objective is to force everybody to think about the ramifications of allowing functions to be overridden and the ramifications of overriding those functions.
Overriding Properties
It is also possible for a class to declare a property as open
, allowing subclasses to override it.
Mostly this is needed for properties declared as constructor parameters. For example, let’s go back to this script:
open class Base(val tag: String) {
open fun gimmeTheTag() = tag
}
open class Intermezzo(val thing: String) : Base(thing)
class Foo(var count: Int) : Intermezzo("Foo Example") {
override fun gimmeTheTag() = "Not the Original Tag"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
fun main() {
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
println("the final count was ${foo.count}")
}
Note that the constructor parameter for Base
is called tag
, while the constructor parameter for Intermezzo
is called thing
. They happen to be the same, um, thing, since Intermezzo
passes its thing
property to the call to the Base
constructor.
Suppose, though, that we wanted to name these the same, such as having them both be tag
:
open class Base(val tag: String) {
open fun gimmeTheTag() = tag
}
open class Intermezzo(val tag: String): Base(tag)
class Foo(var count: Int) : Intermezzo("Foo Example") {
override fun gimmeTheTag() = "Not the Original Tag"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
This results in a compile error:
error: 'tag' hides member of supertype 'Base' and needs 'override' modifier
open class Intermezzo(val tag: String): Base(tag)
^
This is because both are declared as properties, and by default you cannot re-declare a property by reusing the name.
One way to address this is to realize that we do not need the constructor parameter in Intermezzo
to be a property. It could be a plain constructor parameter:
open class Base(val tag: String) {
open fun gimmeTheTag() = tag
}
open class Intermezzo(tag: String): Base(tag)
class Foo(var count: Int) : Intermezzo("Foo Example") {
override fun gimmeTheTag() = "Not the Original Tag"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
This works, even though we reuse the tag
name in Intermezzo
, because a plain tag
constructor parameter does not conflict with an inherited val
property, even one that itself is declared as a constructor parameter.
What the compile error suggests, though, is that we can make tag
in Base
be open
, then override
it in Intermezzo
:
open class Base(open val tag: String) {
open fun gimmeTheTag() = tag
}
open class Intermezzo(override val tag: String) : Base(tag)
class Foo(var count: Int) : Intermezzo("Foo Example") {
override fun gimmeTheTag() = "Not the Original Tag"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
fun main() {
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
println("the final count was ${foo.count}")
}
In other words, you can override properties and change their behavior — we will examine this more much later in the book.
Chaining to Superclass Functions
Like Java, Kotlin uses a super.
prefix to allow code in a subclass to specifically call implementations in a superclass. Typically you do this in a function that you overrode, to execute the superclass implementation as part of the overridden function’s implementation.
open class Base(val tag: String) {
open fun gimmeTheTag() = tag
}
class Foo(var count: Int) : Base("Foo Example") {
override fun gimmeTheTag() = super.gimmeTheTag() + "-Extended"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
fun main() {
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
println("the final count was ${foo.count}")
}
Here, the Foo
implementation of gimmeTheTag()
calls the superclass implementation and appends -Extended
to it. As a result, we get the combined results of both the Base
and the Foo
implementations of gimmeTheTag()
:
Foo Example-Extended: something() was called 1 times
Foo Example-Extended: something() was called 2 times
Foo Example-Extended: something() was called 3 times
the final count was 3
Inheritance Tests
Sometimes, we need to know whether a reference to some supertype is an instance of some subtype.
In Java, usually we do this via instanceof
. So if we have a class hierarchy like this:
class Animal {
// stuff
}
class Frog extends Animal {
// froggy stuff
}
class Axolotl extends Animal {
// axolotly stuff
}
…we can use instanceof
to see if some Animal
is really a Frog
:
void something(Animal critter) {
if (critter instanceof Frog) {
// do froggy things
}
}
In Ruby, kind_of?
or is_a?
are the typical ways of accomplishing the same thing:
def something(critter)
if critter.is_a? Frog
# do froggy things
end
end
Kotlin has syntax similar to Java, with a shorter keyword: is
. So, we can do this:
open class Animal
class Frog : Animal()
class Axolotl : Animal()
fun main() {
val critter: Animal = Frog()
if (critter is Frog) println("Ribbit!") else println("Ummm... whatever noise an axolotl makes!")
}
Here, critter
is an Animal
, but we use is
to determine whether it is really pointing to an instance of Frog
or not.
Inheritance and when
In Kotlin, we can use is
not only as part of an expression, but we can use it as a test for a when
clause. We can rewrite the previous example using a when
:
open class Animal
class Frog : Animal()
class Axolotl : Animal()
fun main() {
val critter: Animal = Frog()
when (critter) {
is Frog -> println("Ribbit!")
else -> println("Ummm... whatever noise an axolotl makes!")
}
}
This is helpful if you have a series of subtypes to check and handle separately, though you may be better served by overriding functions and putting the logic in the classes themselves (e.g., have a makeSound()
function that all Animal
classes implement, so you can just call makeSound()
on any Animal
and get the desired result).
Casting
Java programmers have a long history with casting objects. This turns a reference to a supertype into a subtype. In Java, we use the subtype name in parentheses to perform the cast. So, if we have the Animal
, Frog
, and Axolotl
types as shown above, we might have a method that looks like this:
void something(Animal critter) {
if (critter instanceof Frog) {
Frog frog = (Frog)critter;
// do something with the frog, preferably not involving boiling water
}
else {
Axolotl axolotl = (Axolotl)critter;
// do... oh, I don't know, I'm sure you can think of something
}
}
In Kotlin, casting gets a bit more interesting, because the compiler tries to help reduce the number of casts that you need. In fact, well-written Kotlin code rarely involves casting.
Manual
That being said, you can certainly cast types. In Kotlin, that is performed by using the as
keyword:
val critter: Animal = Frog()
val kermit = critter as Frog
Here, kermit
will be of type Frog
, as we manually cast the Animal
to Frog
via as Frog
.
Smart Casts
Let’s go back to the Java snippet for a moment:
void something(Animal critter) {
if (critter instanceof Frog) {
Frog frog = (Frog)critter;
// do something with the frog, preferably not involving boiling water
}
else {
Axolotl axolotl = (Axolotl)critter;
// do... oh, I don't know, I'm sure you can think of something
}
}
In the if
block, we cast critter
to be a Frog
. We know this is safe, because we checked the type validity with instanceof
in the if
test.
So, if we know that critter
is really a Frog
… why doesn’t Java?
Presumably, the Kotlin developers asked themselves that sort of question, though possibly not involving frogs. Inside of if
and when
blocks, if we know that a variable is really of some subtype, the compiler lets us skip the casts and allows us to refer to the variable as the subtype, even though it was declared as a supertype.
For example, let’s give our creatures some functions, then call those functions:
open class Animal
class Frog : Animal() {
fun hop() = println("Hop!")
}
class Axolotl : Animal() {
fun swim() = println("Swish!")
}
fun main() {
val critter: Animal = Frog()
when (critter) {
is Frog -> critter.hop()
is Axolotl -> critter.swim()
}
}
In a Java-style world, this would not compile. critter
is an Animal
, and Animal
has neither hop()
nor swim()
. However, Kotlin realizes that the is Frog
code will only be executed if critter
is a Frog
, so it allows us to call hop()
on critter
, as if critter
were declared as a Frog
instead of as an Animal
. Similarly, we can call swim()
on critter
if it is Axolotl
.
However, this only works when the compiler is certain of the type. As a result, this subtly-different edition of that code fails with a compile error:
open class Animal
class Frog : Animal() {
fun hop() = println("Hop!")
}
class Axolotl : Animal() {
fun swim() = println("Swish!")
}
val critter: Animal = Frog()
when (critter) {
is Frog -> critter.hop()
else -> critter.swim()
}
Here, we replace is Axolotl
with else
. The Kotlin compiler knows that critter
is not a Frog
, as otherwise we would not be executing the else
. However, critter
might not be an Axolotl
, as it could be an instance of Animal
. As a result, Kotlin cannot assume that it is safe to call swim()
on critter
, as while an Axolotl
can swim()
, an Animal
cannot. So, we get a compile error:
error: unresolved reference: swim
else -> critter.swim()
To get it to compile, we need to manually cast the critter
to be an Axolotl
:
open class Animal
class Frog : Animal() {
fun hop() = println("Hop!")
}
class Axolotl : Animal() {
fun swim() = println("Swish!")
}
fun main() {
val critter: Animal = Frog()
when (critter) {
is Frog -> critter.hop()
else -> (critter as Axolotl).swim()
}
}
But, by doing this, we might crash, if critter
is neither a Frog
nor an Axolotl
. So, while this compiles:
open class Animal
class Frog : Animal() {
fun hop() = println("Hop!")
}
class Axolotl : Animal() {
fun swim() = println("Swish!")
}
val critter: Animal = Animal()
when (critter) {
is Frog -> critter.hop()
else -> (critter as Axolotl).swim()
}
…it crashes at runtime, such as this error message from Kotlin/JVM:
java.lang.ClassCastException: Test$Animal cannot be cast to Test$Axolotl
at Test.<init>(Unknown Source)
Kotlin’s “smart casts” — as this automatic casting is called — not only saves you typing, but it also helps you avoid improper manual casts. In general, if you find yourself using as
in Kotlin, you should be asking yourself: if the Kotlin compiler does not know that this object is of this subtype… how do I know that this object will always be of this subtype?
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.