Exploring Kotlin’s hidden costs — Part 3

Delegated properties and ranges

Following the overwhelming positive feedback I received after publishing the first two parts of this series about the Kotlin programming language, including a mention by Jake Wharton himself, I’m happy to continue the investigation. Don’t miss part 1 and part 2.

In this part 3, we’ll reveal more secrets of the Kotlin compiler and provide new tips to write more efficient code.

Delegated properties

A delegated property is a property whose getter and optional setter implementations are provided by an external object called the delegate. This allows reusable custom property implementations.

class Example {

var p: String by Delegate()

}

The delegate object has to implement an operator getValue() function, as well as a setValue() function for read/write properties. These functions will receive as extra arguments the containing object instance as well as the property metadata (like its name).

When a class declares a delegated property, code matching the following Java representation is generated by the compiler:

public final class Example {

@NotNull

private final Delegate p$delegate = new Delegate();

// $FF: synthetic field

static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Example.class), "p", "getP()Ljava/lang/String;"))};



@NotNull

public final String getP() {

return this.p$delegate.getValue(this, $$delegatedProperties[0]);

}



public final void setP(@NotNull String var1) {

Intrinsics.checkParameterIsNotNull(var1, "<set-?>");

this.p$delegate.setValue(this, $$delegatedProperties[0], var1);

}

}

Some static property metadata is added to the class. The delegate is initialized in the class constructor and is then invoked every time the property is read or written to.

Delegate instances

In the above example, a new delegate instance is created to implement the property. This is required when the delegate implementation is stateful, for example if it caches the computed value of the property locally:

class StringDelegate {

private var cache: String? = null



operator fun getValue(thisRef: Any?, property: KProperty<*>): String {

var result = cache

if (result == null) {

result = someOperation()

cache = result

}

return result

}

}

It’s also required to create a new delegate instance if it requires extra parameters, passed through its constructor:

class Example {

private val nameView by BindViewDelegate<TextView>(R.id.name)

}

But there are some cases where only a single delegate instance is required to implement any property: when the delegate is stateless and the only variables it needs to do its job are the containing object instance and the property name, which are already provided. In that case, you can make the delegate a singleton by declaring it as object instead of class .

For example, the following singleton delegate retrieves the Fragment whose tag name matches the property name inside an Android Activity :

object FragmentDelegate {

operator fun getValue(thisRef: Activity, property: KProperty<*>): Fragment? {

return thisRef.fragmentManager.findFragmentByTag(property.name)

}

}

Similarly, any existing object can be extended to become a delegate. getValue() and setValue() can also be declared as extension functions. Kotlin already provides built-in extension functions to allow using Map and MutableMap instances as delegates, using the property names as keys.

If you choose to reuse the same local delegate instance to implement multiple properties in the same class, you need to initialize this instance in the class constructor.

Note: since Kotlin 1.1, it’s also possible to declare a local variable in a function as a delegated property. In that case, the delegate can be initialized later, up to the point where the variable is declared in the function.

Each delegated property declared in a class also involves the overhead of its associated delegate object, and adds some metadata to the class.

Try to reuse delegates for different properties when it makes sense.

Also consider whether delegated properties are your best option in cases where you would need to declare a large number of them.

Generic delegates

The delegate functions can be declared in a generic way, so the same delegate class can be used with various property types.

private var maxDelay: Long by SharedPreferencesDelegate<Long>()

However, if you use a generic delegate with a primitive type property like in the above example, boxing and unboxing will occur each time the property is read or written to, even if the declared primitive type is non-null.

For delegated properties of non-null primitive types, prefer using specialized delegate classes created for that specific value type rather than a generic delegate, to avoid boxing overhead during each access to the property.

Standard delegates: lazy()

Kotlin provides a few standard delegates to cover common cases, like Delegates.notNull() , Delegates.observable() and lazy() .

lazy() is a function returning a delegate for read-only properties that will evaluate the provided lambda to initialize the property value when first read.

private val dateFormat: DateFormat by lazy {

SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())

}

This is a neat way to defer an expensive initialization until it’s actually needed, improving performance while keeping the code readable.

It should be noted that lazy() is not an inline function and the lambda passed as argument will be compiled to a separate Function class and will not be inlined inside the returned delegate object.

What is often overlooked is that lazy() has an optional mode argument to determine which of 3 different types of delegates will be returned:

public fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer) public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =

when (mode) {

LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)

LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)

LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)

}

The default mode, LazyThreadSafetyMode.SYNCHRONIZED , will perform a relatively expensive double-checked lock, which is required to guarantee that the initialization block will run safely once when the property can be read from multiple threads.

If you know that a property is only going to be accessed from a single thread (like the main thread), then locking can be avoided entirely by explicitly using LazyThreadSafetyMode.NONE instead:

val dateFormat: DateFormat by lazy(LazyThreadSafetyMode.NONE) {

SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())

}