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.
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
- The type of securePassword is replaced by Password with String
- 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