post-thumb

Kotlin Inline classes

Kotlin logo

Introduction

Any C++ developer knows about inline functions. Kotlin tries to provide a similar concept to classes. The following post explains the basic idea behind inline classes by providing code examples. We first start by defining what an inline class is.

Definition of an inline class in Kotlin

In Kotlin, an inline class is a data class that has a single read-only property with the advantage of having less memory footprint. In fact, when we use the inline class in code, the compiler replaces it, most of the time, with its property. They are introduced in Kotlin 1.3.

Inline classes’ main goal is to optimize wrappers. A wrapper is a class that encapsulates an existing type in order to add behavior or to provide a supplementary abstraction level.

The following section shows a code example that illustrates the benefits of inline classes.

Basic example

Suppose we define a Mail class that is a wrapper around String as follows:

data class DataMail(val mail:String) {
    fun isValid() = mail.contains("@")
    fun dns() = if(isValid()) mail.substringAfter("@") else ""
}

Since the DataMail class has a single read-only property, it can be turned into an inline class by replacing data by inline as follows:

inline class InlineMail(val mail:String) {
    fun isValid() = mail.contains("@")
    fun dns() = if(isValid()) mail.substringAfter("@") else ""
}

Both the InlineMail and the DataMail classes are used similarly. However, the InlineMail class will not be instantiated, instead, the underlying type (which is String here) will be used. This affirmation can be assessed by printing the javaClass.name property as follows:

fun main() {
    val mail2 = DataMail("toto-gmail.com")
    println("type: ${mail2.javaClass.name}, value: ${mail2.mail}, valid: ${mail2.isValid()}, dns: ${mail2.dns()}")
    val mail1 = InlineMail("toto@gmail.com")
    println("type: ${mail1.javaClass.name}, value: ${mail1.mail}, valid: ${mail1.isValid()}, dns: ${mail1.dns()}")
}
/* Output
type: DataMail, value: toto-gmail.com, valid: false, dns:
type: java.lang.String, value: toto@gmail.com, valid: true, dns: gmail.com
 */

As we can see, the InlineMail class is not created, which means less memory footprint. In addition to the memory footprint gain, we have added an abstraction level making our code less error prone.

It may be natural to compare inline classes to typealiases. The next section explains the difference with typealias.

inline vs typealias

Inline classes behave like typealiases at runtime because they are replaced with their underlying type. However, they are different at compile time because inline classes are viewed as plain classes that introduce a new type. Thus, an inline class to a Int is not compatible with neither a Int nor a typealias for a Int.

The following code illustrates the differences between a typealias and an inline class. The code that does not compile is commented.

inline class InlineMinutes(val minutes:Int)
typealias TypealiasMinutes = Int

fun main(){
    var tam:TypealiasMinutes = 10
    var i:Int = 10
    tam = i
    // var im: InlineMinutes = 10 //not possible
    // var im2: InlineMinutes = tam //not possible
    var im3: InlineMinutes = InlineMinutes(10)
    //tam = im3 //not possible
    im3 = i //not possible
}

We can conclude that inline classes allow combining the advantages of type checking while optimizing the generated code.

As we have seen, an inline class is basically considered a new class at compile time. The next section experiments on how inline classes behave with regard to inheritance and other different situations.

Inheritance and unboxing exceptions

Inline classes can only inherit from interfaces and are not allowed to be inherited from (they are final). In case we reference an inline class by its interface, the underlying type is not used. This means that there are situations where the Kotlin compiler does not use the underlying type.

Kotlin tries to use the underlying type as much as possible with the exception of interfaces, generic classes and nullable types. Hopefully, the underlying type is used when we call a function of the inline class.

The following code illustrates passing an inline class as generic, interface and a nullable type. For each case, we print the class name using a function defined in the inline class:

interface IPrintType { fun printType() }

inline class Hours(val i: Int) : IPrintType {
    override fun printType() {
        println(this.javaClass.name)
    }
}

fun asInline(hours: Hours) { hours.printType() }
fun <T> asGeneric(x: T) {
    if(x is Int){
        println("generic Int")
    }else if(x is Hours){
        print("generic Hours - ")
        x.printType()
    }else if(x is IPrintType){
        println("generic IPrintType")
    }
}
fun asInterface(i: IPrintType) { i.printType() }
fun asNullable(i: Hours?) {
    // println(i!!.javaClass.name) //this will cause a crash
    i?.printType()
}

fun <T> id(x: T): T = x

fun main() {
    val hours = Hours(42)
    val iPrintTypeHours: IPrintType = hours
    println("Interface class name: ${iPrintTypeHours.javaClass.name}")
    print("Interface call method of inline class: ")
    iPrintTypeHours.printType()

    print("asInline: ")
    asInline(hours)
    print("asGeneric: ")
    asGeneric(hours)
    print("asInterface: ")
    asInterface(hours)
    print("asNullable: ")
    asNullable(hours)
}

/* output
Interface class name: Hours
Interface call method of inline class: int
asInline: int
asGeneric: generic Hours - int
asInterface: int
asNullable: int
 */

There are some interesting things to see here and I’ll highlight two of them. The first one, the javaClass name from the interface does give the same result in iPrintTypeHours.javaClass.name and in iPrintTypeHours.printType(). This illustrates the fact that the compiler uses the underlying class when possible. This observation is valid for all the remaining code, calling printType will always use the underlying type.

The second highlight is that un-commenting this line of code will cause a crash.

fun asNullable(i: Hours?) {
    // println(i!!.javaClass.name) //this will cause a crash
    i?.printType()
}

In fact, we get a nice java.lang.ClassCastException: Hours cannot be cast to java.base/java.lang.Number. Is it a bug with Kotlin? Who knows :confused:.

The next section summarizes what have been learned about inline classes.

Conclusion

Inline classes introduced in Kotlin 1.3 provide a simple yet efficient optimization for wrappers under some conditions. To summarize them, inline classes:

  • Must have a single read-only parameter
  • Are final
  • Can implement interfaces
  • Cannot extend classes
  • Cannot be assigned to the underlying type
  • Cannot have init blocks, inner classes
  • Cannot have backing fields on properties
  • Can only have simple properties (no lateinit/delegated properties)
  • Remain boxed when references though a generic, an interface or a its nullable type

If there conditions are satisfied, the underlying type will be used at runtime instead of the class itself. So do not hesitate to use them whenever possible.