Kotlin “By” Class Delegation: Favor Composition Over Inheritance

Eric Lin
Atlas
Published in
5 min readOct 30, 2017

When people ask me why I choose Kotlin over Java, I often say, “Because Kotlin is a better Java.” You get more than half of Effective Java implemented for you. In chapter 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 the special keyword by to help you with implementing delegation in a single line.

Some side notes:

  • by can be used with properties as well. I will just focus on class delegation in this post.
  • Kotlin also has some other language features to “reduce” the use of inheritance, like typealias for tagging classes and class 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 just to "be cool."

Problem 1: indeterministic inherited behaviors

I took this example from Fragmented episode #87. The podcast is awesome, and I recommend Android developers listen to it.

Let’s say you want to extend a standard collection to change its default behavior. Maybe you want to count how many items are added into a set. Here is how you would probably implement that:

public class CountingSet extends HashSet<Long> {

private long addedCount = 0;

@Override
public boolean add(Long aLong) {
addedCount++;
return super.add(aLong);
}

@Override
public boolean addAll(Collection<? extends Long> c) {
addedCount = addedCount + c.size();
return super.addAll(c);
}

public long getAddedCount() {
return addedCount;
}
}

Simple, right? All you need to do is extend the existing HashSet to avoid reinventing 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 the following example usage, you will find it surprising:

val countingSet = CountingSet()
countingSet.addAll(setOf(1, 3, 4))
println("added count is ${countingSet.addedCount}") // prints: added count is 6

The problem here is inside the HashSet#addAll():

public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}

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 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 mentioning here.

Sometimes it’s very tempting to write some “template methods” in an abstract class and call them at constructor:

abstract class Animal {

init {
println("I'm ${getName()}")
}

abstract fun getName(): String
}

class Dog(private val name: String) : Animal() {

override fun getName(): String = name
}

fun main(args: Array<String>) {
val dog = Dog("Puff") // prints: I'm null
}

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 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, as the core libraries in Google, has implemented a set 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:

public class CountingSet extends ForwardingSet<Long> {

private final Set<Long> delegate = Sets.newHashSet();
private long addedCount = 0L;

@Override
protected Set<Long> delegate() {
return delegate;
}

@Override
public boolean add(Long element) {
addedCount++;
return delegate().add(element);
}

@Override
public boolean addAll(Collection<? extends Long> collection) {
addedCount += collection.size();
return delegate().addAll(collection);
}
}

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:

/**
* @return The {@link AppCompatDelegate} being used by this Activity.
*/
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}

// inside AppCompatDelegate.java
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
if (Build.VERSION.SDK_INT >= 24) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}

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:

class CountingSet(private val delegate: MutableSet<Long> = HashSet()) : MutableSet<Long> by delegate {

private var addCount = 0L

override fun add(element: Long): Boolean {
addCount++
return delegate.add(element)
}

override fun addAll(elements: Collection<Long>): Boolean {
addCount += elements.size
return delegate.addAll(elements)
}
}

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:

class MySetMap : Set<Long> by HashSet(), Map<Long, Long> by HashMap() {
override val size: Int
get() = TODO("not implemented")

override fun isEmpty(): Boolean {
TODO("not implemented")
}
}

This 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 introduced the by keyword, making delegation so easy to implement. We should challenge ourselves to avoid inheritance more by using language features.

--

--

Eric Lin
Atlas
Writer for

Honest Android developer. Loves Kotlin and new technologies.