When people ask me why Kotlin over Java, I often say because Kotlin is a better Java. You get more than half of Effective Java implemented for you. In charter 4 of the book, the author lists many items about inheritance because it is very error prone. The most well known item probably would be Item 16: Favor composition over inheritance. To implement that item, Kotlin introduces a special key work by
to help you with implementing delegation in a single line.
Some side notes:
by
keyword can be used with properties as well. I will just focus on class delegation in this post and maybe do another one for it.- Kotlin also has some other language features to “reduce” the use of inheritance like
typealias
for tagging classes andclass are final by default
.
Inheritance can be problematic
Before jumping directly into the by
keyword, let’s try to understand the why first. This is always the important part of learning new things. Without knowing what problems the tool tries to solve, you will pretty much end up using the tool for “being cool”.
problem 1: indeterministic inherited behaviors
I took the example from Fragmented episode #87. The podcast is awesome, and I recommend Android developers to listen to it.
For some reasons you try to extending a standard collection to change its default behavior. Maybe you want to count how many items are added into a set, and here is how you would probably implement it:
|
|
Simple, right? All you need to is to extend the existing HashSet to avoid reinvent the wheel and increment the addedCount
every time either add()
or addAll()
is called. It seems very promising but when you actually try to get the addedCount
with following example usage, you will find it surprising:
|
|
The problem here is inside the HashSet#addAll()
:
|
|
It iterates over the collection c
and calls add()
method for each element, so you end up counting each number twice.
You might argue since we know the implementation details of HashSet, we can just increment the counter inside add()
. That would indeed solve this particular problem, but it is still problematic since you don’t own the HashSet class and therefore you cannot guarantee it would never change.
problem 2: initialization order in constructor
This might not be directly related to the by
keyword, but it is something worth to mention here.
Sometimes it’s very attempting to write some “template methods” in an abstract class and calling them at constructor:
|
|
The reason getName()
returns null is because in java super(), the constructor from super class, must be called first. Therefore when line println("I'm ${getName()}")
is executed, the property name
in Dog
has not been assigned yet.
This is a common issue, and even IntelliJ will try to warn you:
With many potential issues, you should just avoid inheritance and start program to interfaces not implementations when possible.
Delegation in Java
I want to include some examples in Java to show delegation is already widely used in many libraries and frameworks.
Not surprisingly [Guava][https://github.com/google/guava], as the core libraries in Google, has implemented a sets of Forwarding collections
to help us to implement common collection interfaces without extending them.
CountingSet v2
The same counting set can be rewritten with ForwardingSet:
|
|
The implementation looks similar to the previous one, but this time we are not dependent on the actual implementation of add()
and addAll()
since we just “forward” the method calls to the delegate()
. We can safely replace the delegate with other types without knowing its implementation details.
It is still an abstract class because Java does not support “delegation” at language level like Kotlin does.
Another delegation we use a lot as Android developers is in AppCompatActivity:
|
|
It follows the same pattern. It creates and “delegates” different implementation of AppCompatDelegate.
Delegation in Kotlin
The actual sample code here is going to be very short since Kotlin supports class delegation with by
keyword, and we can have delegation in one line.
CountingSet v3
In kotlin the same counting set is:
|
|
A few things to know here:
#1 The target type must be an interface. It cannot be an abstract class.
#2 The delegate can be a primary constructor property or a new instance. For example you can create a set by writing just class MySet : Set<Long> by HashSet()
In our example we need to call the delegate in two overridden functions so we make it a private property.
#3 You can have more than one delegations. For example:
|
|
would work just fine, but since both Map
and Set
define size
and isEmpty()
, you would need to implement those two methods so that the compiler won’t get confused.
Thoughts
Kotlin introduces the by
keyword and it makes delegation so easy to implement. We should challenge ourselves to avoid inheritance more by using the language features.