How to use Kotlin 1.5 new feature Inline classes

How to use Kotlin 1.5 new feature Inline classes? If you are using Android Studio 4.2.0, IntelliJ IDEA 2020.3 or higher, you will soon receive the Kotlin 1.5 Plugin push. As a major version, 1.5 brings many new features, the most important of which is the inline class.

How to use Kotlin 1.5 new feature Inline classes
How to use Kotlin 1.5 new feature Inline classes

Kotlin 1.5 new feature Inline classes

There is an alpha version of inline class as early as Kotlin 1.3. Entered the beta at 1.4.30, and now finally ushered in the Stable version in 1.5.0. The inline keyword of the early experimental version was deprecated in 1.5 and changed to the value keyword.

//before 1.5
inline class Password(private val s: String)

//after 1.5 (For JVM backends)
@JvmInline
value class Password(private val s: String)

Kotlin 1.5 new feature Inline classes

I personally agree with the naming change from inline to value, which makes its purpose more clear:

The main purpose of inline class is to better “package” value

Sometimes in order to be more recognizable in semantics, we use custom classes to wrap some basic values. Although this improves code readability, additional packaging will bring potential performance loss. Basic values are packaged. In other classes, you can’t enjoy the optimization of jvm (from allocation on the heap to allocation on the stack). The inline class is replaced with its “wrapped” value in the final generated bytecode, thereby improving runtime performance.

// For JVM backends
@JvmInline
value class Password(private val s: String)

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

As above, there can only be one member variable in the inline class construction parameter, that is, the value that is finally inlined into the bytecode.

val securePassword = Password("Don't try this in production")

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

As above, the Password instance is replaced with String type “Don’t try this in production” in the bytecode

How to install Kotlin 1.5

First update the Kotlin Plugin of the IDE. If you do not receive the push, you can manually upgrade:

Tools> Kotlin> Configure Kotlin Plugin Updates

Configure languageVersion & apiVersion

compileKotlin {
    kotlinOptions {
        languageVersion = "1.5"
        apiVersion = "1.5"
    }
}

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

Code after inline processing

What exactly does inline classes look like after they are converted to bytecode?

fun check(password: Password) {
    //...
}

fun main() {
    val securePassword = Password("Don't try this in production")
    check(securePassword)
}

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

For the inline class Password, the products of bytecode decompilation are as follows:

   public static final void check_XYhEtbk/* $FF was: check-XYhEtbk*/(@NotNull String password) {
      Intrinsics.checkNotNullParameter(password, "password");
   }

   public static final void main() {
      String securePassword = Password.constructor-impl("Don't try this in production");
      check-XYhEtbk(securePassword);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
   

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

  1. The type of securePassword is replaced by Password with String
  2. The check method is renamed check_XYhEtbk, and the signature type also has Password instead of String.

It can be seen that, whether it is a variable type or a function parameter type, all inline classes are replaced with their packaged types.

The name is obfuscated (check_XYhEtbk) for two main purposes

  • Prevent the parameters of overloaded functions from appearing with the same signature after inline
  • Prevent the method from calling from the Java side to the parameter after the inline

Member of Inline class

Inline class has all the characteristics of ordinary classes, such as having member variables, methods, initialization blocks, etc.

@JvmInline
value class Name(val s: String) {
     init {
         require(s.length> 0) {}
     }

     val length: Int
         get() = s.length

     fun greet() {
         println("Hello, $s")
     }
}

fun main() {
     val name = Name("Kotlin")
     name.greet() // `greet()` is called as a static method
     println(name.length) // property getter is also a static method
}

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

However, the members of the inline class cannot have their own behind-the-scenes attributes and can only be used as agents. The object created by the inline class will be eliminated in the bytecode, so this instance cannot have its own state and behavior. The method call to the inline class instance will become a static method call in actual operation.

Inline class inheritance

interface Printable {
     fun prettyPrint(): String
}

@JvmInline
value class Name(val s: String): Printable {
     override fun prettyPrint(): String = "Let's $s!"
}

fun main() {
     val name = Name("Kotlin")
     println(name.prettyPrint()) // prettyPrint() is also a static method call
}

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

Inline class can implement any inteface, but it cannot inherit from class. Because there will be nowhere to put the attributes or states of its parent class at runtime. If you try to inherit another Class, the IDE will prompt an error: Inline class cannot extend classes.

Automatic unpacking

Inline class is not always eliminated in bytecode, and sometimes it needs to exist. For example, when it appears in a generic type or as a Nullable type, it will automatically convert with the packaged type according to the situation, and realize automatic unpacking and unpacking like Integer and int.

@JvmInline
value class WrappedInt(val value: Int)

fun take(w: WrappedInt?) {
    if (w != null) println(w.value)
}

fun main() {
    take(WrappedInt(5))
}

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

As above, take receives a Nulable WrappedInt and performs print processing

public static final void take_G1XIRLQ(@Nullable WrappedInt w) {
    if (Intrinsics.areEqual(w, (Object)null) ^ true) {
        int var1 = w.unbox_impl();
        System.out.println(var1);
    }
}

public static final void main() {
    take_G1XIRLQ(WrappedInt.box_impl(WrappedInt.constructor_impl(5)));
}

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

In the bytecode, the take parameter has not changed to Int, but is still the original type WrappedInt. Therefore, at the call of take, box_impl needs to be used for boxing, and in the implementation of take, unboxing is performed through unbox_impl before printing

In the same way, when using inline class in a generic method or generic container, it needs to be boxed to ensure that its original type is passed in:

genericFunc(color)         // boxed
val list = listOf(color)   // boxed
val first = list.first()   // unboxed back to primitive

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

Conversely, when getting the item from the container, it needs to be unboxed as the packaged type.

There is no need to pay too much attention to the automatic unpacking and packing in the development, as long as you know that this feature exists.

Compare with other types Kotlin Inline classes

What is the difference with type aliases?

Inline class and type aliases are similar in concept, they will be replaced by the proxy (wrapper) type after compilation. The difference is

  • The inline class itself is the actual Class, but it is eliminated in the bytecode and replaced with the wrapped type
  • Type aliases are just aliases, and their type is the type of the proxy class.
typealias NameTypeAlias = String

@JvmInline
value class NameInlineClass(val s: String)

fun acceptString(s: String) {}
fun acceptNameTypeAlias(n: NameTypeAlias) {}
fun acceptNameInlineClass(p: NameInlineClass) {}

fun main() {
     val nameAlias: NameTypeAlias = ""
     val nameInlineClass: NameInlineClass = NameInlineClass("")
     val string: String = ""

     acceptString(nameAlias) // OK: NameTypeAlias is equivalent to String and can be passed
     acceptString(nameInlineClass) // Not OK: NameInlineClass and String are two classes and cannot be equal

     // vice versa:
     acceptNameTypeAlias(string) // OK: It is also possible to pass in String
     acceptNameInlineClass(string) // Not OK: String is not equivalent to NameInlineClass
}

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

The difference with data class?

Inline class and data class are also very similar in concept, both are packaging of some data, but the difference is obvious

  • An inline class can only have one member attribute, and its main purpose is to make the code easier to use through an additional type of packaging
  • Data clas can have multiple member attributes, and its main purpose is to more efficiently process a collection of related data

Scenes to be used

As mentioned above, the purpose of inline class is to make the code easier to use through packaging. This ease of use is reflected in many aspects:

Scenario 1: Improve readability

fun auth(userName: String, password: String) { println("authenticating $userName.") }

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

As above, the two parameters of auth are both String, which lacks recognition. It is difficult to detect even if the transmission is wrong like the following

auth("12345", "user1") //Error
@JvmInline value class Password(val value: String)
@JvmInline value class UserName(val value: String)

fun auth(userName: UserName, password: Password) { println("authenticating $userName.")}

fun main() {
    auth(UserName("user1"), Password("12345"))
    //does not compile due to type mismatch
    auth(Password("12345"), UserName("user1"))
}

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

Use inline class to make the parameters more recognizable and avoid errors

Scenario 2: Type safety (shrink the scope of the extension function)

inline fun <reified T> String.asJson() = jacksonObjectMapper().readValue<T>(this)

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

The extension method asJson of the String type can be converted to the specified type T

val jsonString = """{ "x":200, "y":300 }"""
val data: JsonData = jsonString.asJson()

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

Since the extension function is top-level, all String types can be accessed, causing pollution

"whatever".asJson<JsonData> //will fail

Through the inline class, the Receiver type can be reduced to the specified type to avoid pollution

@JvmInline value class JsonString(val value: String)

inline fun <reified T> JsonString.asJson() = jacksonObjectMapper().readValue<T>(this.value)

As above, define JsonString and define extension methods for it.

Scenario 3: Carrying additional information

/**
 * parses string number into BigDecimal with a scale of 2
 */
fun parseNumber(number: String): BigDecimal {
    return number.toBigDecimal().setScale(2, RoundingMode.HALF_UP)
}

fun main() {
    println(parseNumber("100.12212"))
}

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

As above, the function of parseNumber is to parse any string into a number and retain two decimal places.

If we want to save the values before and after parsing through a type and print them separately, we may first think of using Pair or data class. But when there is a conversion relationship between these two values, it can actually be achieved with inline class. as follows

@JvmInine value class ParsableNumber(val original: String) {
    val parsed: BigDecimal
        get() = original.toBigDecimal().setScale(2, RoundingMode.HALF_UP)
}

fun getParsableNumber(number: String): ParsableNumber {
    return ParsableNumber(number)
}

fun main() {
    val parsableNumber = getParsableNumber("100.12212")
    println(parsableNumber.parsed)
    println(parsableNumber.original)
}

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

The packaging type of ParsableNumber is String, and the parsed value is carried through parsed. As mentioned earlier, in the bytecode, the parsed getter will exist in the form of a static method, so although it carries more information, there is actually no such a wrapper class instance:

@NotNull
public static final String getParsableNumber(@NotNull String number) {
    Intrinsics.checkParameterIsNotNull(number, "number");
    return ParsableNumber.constructor_impl(number);
}

public static final void main() {
    String parsableNumber = getParsableNumber("100.12212");
    BigDecimal var1 = ParsableNumber.getParsed_impl(parsableNumber);
    System.out.println(var1);
    System.out.println(parsableNumber);
}

Kotlin 1.5 new feature Inline classes – Kotlin Inline classes

Conclusion

Inline class is a good tool that will not cause performance loss while improving the readability and ease of use of the code. In the early days, it has been in a state of experimentation and has not been known to everyone. With the current conversion in Kotlin 1.5, I believe that it will be used more widely and explore more application scenarios in the future.

reference:
https://kotlinlang.org/docs/inline-classes.html
https://blog.jetbrains.com/kotlin/2021/02/new-language-features-preview-in-kotlin-1-4-30/ https://blog.csdn.net/vitaviva/article/details/116488136

Leave a Comment