Delegation and Metaprogramming
Delegation and Metaprogramming
The Gang of Four said it in 1994: favor composition over inheritance. Thirty years later, most Java codebases still reach for extends first and pay the price later — fragile base class problems, god-object hierarchies, and interfaces that you dread implementing because of the sheer number of methods you need to forward.
Consider this scenario. You have a List<T> that you want to wrap with logging behavior. In Java, your options are:
public class LoggingList<T> implements List<T> {
private final List<T> inner;
public LoggingList(List<T> inner) {
this.inner = inner;
}
@Override
public int size() {
return inner.size();
}
@Override
public boolean isEmpty() {
return inner.isEmpty();
}
@Override
public boolean contains(Object o) {
return inner.contains(o);
}
@Override
public Iterator<T> iterator() {
return inner.iterator();
}
// ... 20+ more forwarding methods ...
@Override
public boolean add(T t) {
System.out.println("Adding: " + t);
return inner.add(t);
}
}
You write dozens of one-line methods that do nothing but call inner.whatever(). The actual behavior you care about — the logging in add() — drowns in boilerplate. IDEs can generate these forwarding methods, but generated code is still code you maintain, review, and debug.
Kotlin eliminates this entire category of boilerplate with a single language keyword.
Class Delegation with by
class LoggingList<T>(private val inner: MutableList<T>) : MutableList<T> by inner {
override fun add(element: T): Boolean {
println("Adding: $element")
return inner.add(element)
}
}
That’s it. The by inner clause tells the compiler: “For every method in MutableList<T> that I haven’t explicitly overridden, generate a forwarding call to inner.” One line replaces two dozen forwarding methods.
What the Compiler Generates
The by keyword isn’t runtime magic — it’s a compile-time code generation feature. When you write class Foo : Bar by delegate, the Kotlin compiler generates real forwarding methods in the bytecode. Decompiling the LoggingList class reveals something structurally identical to the verbose Java version above: individual methods that call through to the delegate instance.
You can verify this yourself:
// Compile and inspect
// kotlinc LoggingList.kt -include-runtime -d out.jar
// javap -c LoggingList.class
The bytecode shows concrete size(), isEmpty(), contains(), iterator() methods — each consisting of a field load and an invokeinterface call to the delegate. The performance is identical to hand-written forwarding; there’s no reflection, no proxy, no indirection layer.
Selective Override
The power of class delegation is that you override only the methods that need different behavior. The rest get forwarded automatically:
class CountingSet<T>(private val inner: MutableSet<T> = HashSet()) : MutableSet<T> by inner {
var addCount: Int = 0
private set
override fun add(element: T): Boolean {
addCount++
return inner.add(element)
}
override fun addAll(elements: Collection<T>): Boolean {
addCount += elements.size
return inner.addAll(elements)
}
}
Compare this with Java’s ForwardingSet from Guava, or worse, extending HashSet directly (where addAll calls add internally, breaking your counting logic — the classic example from Effective Java, Item 18). Kotlin’s delegation avoids this trap because your overridden addAll calls inner.addAll(), not this.add().
Multi-Interface Delegation
You can delegate multiple interfaces to different objects:
interface Printer {
fun print(message: String)
}
interface Logger {
fun log(level: String, message: String)
}
class ConsolePrinter : Printer {
override fun print(message: String) = println(message)
}
class FileLogger(private val path: String) : Logger {
override fun log(level: String, message: String) {
// write to file
}
}
class Application(
printer: Printer,
logger: Logger
) : Printer by printer, Logger by logger
The Application class composes two behaviors without inheriting from either implementation. Each interface’s methods route to the corresponding delegate.
Property Delegation
Kotlin extends the delegation concept beyond classes to individual properties. Property delegation lets you extract reusable property behavior — caching, validation, observation, persistence — into standalone objects.
var name: String by Delegates.observable("initial") { property, oldValue, newValue ->
println("${property.name} changed: $oldValue -> $newValue")
}
When you write var x: T by delegate, the compiler transforms property access into method calls on the delegate object:
- Reading
xcallsdelegate.getValue(thisRef, property) - Writing
x = valuecallsdelegate.setValue(thisRef, property, value)
These are operator functions with specific signatures:
operator fun getValue(thisRef: Any?, property: KProperty<*>): T
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)
The thisRef parameter is the object that owns the property. The property parameter is a KProperty reflection object carrying metadata — the property’s name, return type, annotations, and visibility. This metadata parameter is what makes property delegates so much more capable than plain getter/setter methods: the delegate can adapt its behavior based on which property it’s attached to.
For read-only properties (val), you only need getValue. You can formalize this by implementing ReadOnlyProperty<R, T>:
class ResourceDelegate : ReadOnlyProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return loadResource(property.name)
}
}
For mutable properties (var), implement ReadWriteProperty<R, T>:
class TrimmedString : ReadWriteProperty<Any?, String> {
private var value: String = ""
override fun getValue(thisRef: Any?, property: KProperty<*>): String = value
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
this.value = value.trim()
}
}
In Java, the closest equivalent is a combination of field + custom getter/setter logic, duplicated for every property that needs it. Or you reach for AOP frameworks like AspectJ to intercept field access — a heavyweight, runtime, classpath-dependent solution to a problem Kotlin solves at the language level.
What’s Ahead
This chapter splits into two focused sections. The first dives into practical delegation patterns — building custom property delegates, mastering the standard library delegates (lazy, observable, vetoable, map-backed), and applying class delegation to real design patterns like decorator and adapter. The second section moves beyond delegation into Kotlin’s metaprogramming capabilities: KSP (Kotlin Symbol Processing) for compile-time code generation and the landscape of Kotlin compiler plugins.
Both sections share a common theme: Kotlin provides language-level mechanisms that let you write less code while maintaining full type safety and zero runtime overhead. The question isn’t whether to use these features — it’s knowing which tool fits which problem.