Blog Infos
Author
Published
Topics
Published
Topics
Learn how to use the value keyword to create inline classes and apply them in 4 different scenarios

Kotlin introduced inline classes with version 1.3 as an experimental feature. In the meantime, a few things have changed. Kotlin changed the original inline keyword to be value instead. In addition, on the JVM you have to add an annotation to your value class to make it work as expected. The terminology is still valid so this article will be referring to “inline classes” although the keywords are named a bit differently. Inline classes add a simple tool we can use to add a wrapper around some other type without adding runtime overhead through additional heap allocations. In this article, we want to see how inline classes in Kotlin work and when it makes sense to use them.

At the end of this article, I will be going through 4 different scenarios at that benefit from this construct.

A first glance at inline classes

Inline classes are not super complicated to get started with. In fact, you just add the value keyword to your class and apply the @JvmInline annotation:

@JvmInline
value class WrappedInt(val value: Int)

Inline classes are required to specify exactly one property in the primary constructor, as shown with value. You can’t wrap multiple values in one inline class. Also, you cannot have properties with backing fields and property delegation isn’t supported. However, inline classes can have simple computable properties which we will see later in this article.

At runtime, the wrapped type of an inline class will be used without its wrapper whenever possible. Looking at the example above, this means that the compiler will try to use the value: Int whenever it can do so. This is similar to Java’s boxed types like Integer or Boolean, which will be represented as their corresponding primitive type whenever the compiler is able to. That exactly is the great selling point for inline classes in Kotlin: When you make a class an inline class, the class itself won’t be used in the byte code unless it’s absolutely necessary. Inlining classes drastically reduces space overhead at runtime.

Runtime representation

At runtime, an inline class can be represented as both, the wrapper type and the underlying type. As mentioned in the previous paragraph, the compiler prefers using the underlying (wrapped) type of an inline class to optimize the code as much as it can. This is similar to boxing between int and Integer. In certain situations, however, the compiler needs to use the wrapper itself, so it will be generated during compilation:

https://gist.github.com/53e05dbebb1d5d95c14d285fbc20d187

This snippet shows the simplified byte code represented as Java code to show how an inline class looks like. Along with some obvious stuff like the value field and its getter, the constructor is private, and new objects will instead be created through constructor_impl which does not actually use the wrapper type but only returns the passed in underlying type. Finally, you can see box_impl and unbox_impl functions which are used for boxing purposes. Now let’s see how this inline class wrapper is being utilized when we use the inline class in our code.

Using inline classes
fun take(w: WrappedInt) {
println(w.value)
}
fun main() {
val inlined = WrappedInt(5)
take(inlined)
}
view raw call-inline.kt hosted with ❤ by GitHub

In this snippet, we create aWrappedInt and pass it to a function that prints its wrapped value. The corresponding byte code, again as Java code, looks as follows:

public static final void take_wIOJKEE(int w) {
System.out.println(w);
}
public static final void main() {
int inlined = WrappedInt.constructor-impl(5);
take-wIOJKEE(inlined);
}

In the compiled code, no instance of WrappedInt is created. Although the static constructor_impl is used, it just returns an int which is then passed to the take function that also does not know anything about the type of the inline class which we originally had in our source code. Note that the names of functions accepting inline class parameters are extended with a generated hash code in the byte code. This way, they can stay distinguishable from overloaded functions accepting the underlying type as a parameter:

fun take(w: WrappedInt) = println(w.value)
fun take(v: Int) = println(v.value)
view raw overload-fun.kt hosted with ❤ by GitHub

To make both take methods available in the JVM byte code and avoid signature clashes, the compiler renames the first one to something like take-wIOJKEE. This technique is called mangling. Note that the Java bytecode representation above shows a “_” rather than a “-” since Java does not allow method names to contain the dash. You can still call those functions from Java but it has a quirk; you explicitly have to give the function a name and with that disable automatic mangling:

@JvmName("takeWrappedInt")
fun take(w: WrappedInt) {
println(w.value)
}
Boxing of inline classes

We saw earlier that box_impl and unbox_impl functions are created for inline classes, so when do we need them? The Kotlin docs cite a rule of thumb which says:

Inline classes are boxed whenever they are used as another type.

Boxing happens, for instance, when you use your inline class as a generic or nullable type:

inline class WrappedInt(val value: Int)
fun take(w: WrappedInt?) {
if (w != null) println(w.value)
}
fun main() {
take(WrappedInt(5))
}

In this code, we modified the take function to take a nullable WrappedInt and print the underlying type if the argument is not null.

public static final void take_KR2Ufmw(@Nullable WrappedInt w) {
if (w != null) {
int var1 = w.unbox-impl();
System.out.println(var1);
}
}
public static final void main() {
take-KR2Ufmw(WrappedInt.box-impl(WrappedInt.constructor-impl(5)));
}

In the byte code, take now does not accept the underlying type directly anymore. It has to work with the wrapper type instead. When printing its content, unbox_impl is invoked. On the caller site, we can see that box_impl is used to create a boxed instance of WrappedInt.

It should be evident that we want to avoid boxing whenever possible. Keep in mind that specific usages of inline classes and also primitive types, in general, rely on this technique and might have to be reconsidered.

Use Cases for inline classes

We saw that inline classes have a huge advantage: In the best case, they reduce runtime overhead drastically since additional heap allocations are avoided. But when do we want to use wrapper types anyway?

Better typing with inline classes

Imagine an authentication method in an API that looks as follows:

fun auth(userName: String, password: String) {
println("authenticating $userName.")
}
view raw auth_wrap.kt hosted with ❤ by GitHub

If we were naïv, we could think of course, every client is going to pass sane values here, i.e. a user name and the password. However, it isn’t too far-fetched to assume the scenario that specific users will invoke this method differently:

auth("12345", "user1")

Since both parameters are of type String, you may mess up their order which gets even more likely with an increasing number of arguments. Wrappers around these types can help you mitigate that risk, and therefore inline classes are an awesome tool:

@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"))
}
view raw auth-inline.kt hosted with ❤ by GitHub

The parameter list has become less confusing and, on the caller site, the compiler will not allow a mismatch. Inline classes give us simple, type-safe wrappers without introducing additional heap allocations. For these situations, inline classes should be preferred whenever possible. Nevertheless, inline classes can be even smarter, which the next use case demonstrates.

Handling state without additional space

Let’s consider a method that takes a numeric string and parses it into a BigDecimal while also adjusting its scale:

/**
* 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"))
}
view raw bd_parsing.kt hosted with ❤ by GitHub

The code is rather straightforward and would work just fine, but a requirement could be that you need to keep track of the original string that was used to parse the number. To solve that, you may create a wrapper type or just use the existing Pair class to return a pair of values from that function. Those approaches would be valid although it obviously allocates additional space, which, in particular situation, should be avoided. Inline classes can help you with that. We already noticed that inline classes can’t have multiple properties with backing fields. However, they can have simple calculated members in the form of properties and functions. We can create an inline class for our use case that wraps the original String and provides a method or a property that, on demand, parses our value. For the user, this will look like a normal data wrapper around two types while it does not add any runtime overhead in the best case:

@JvmInline
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)
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

As you can see, the getParsableNumber function returns an instance of our inline class which provides two properties original (the underlying type) and parsed (the calculated parsed number). That’s an interesting use case that is worth observing on a byte code level again:

More byte code
public final class ParsableNumber {
@NotNull
private final String original;
@NotNull
public final String getOriginal() { return this.original; }
// $FF: synthetic method
private ParsableNumber(@NotNull String original) {
Intrinsics.checkParameterIsNotNull(original, "original");
super();
this.original = original;
}
@NotNull
public static final BigDecimal getParsed_impl(String $this) {
BigDecimal var10000 = (new BigDecimal($this)).setScale(2, RoundingMode.HALF_UP);
Intrinsics.checkExpressionValueIsNotNull(var10000, "original.toBigDecimal().…(2, RoundingMode.HALF_UP)");
return var10000;
}
@NotNull
public static String constructor_impl(@NotNull String original) {
Intrinsics.checkParameterIsNotNull(original, "original");
return original;
}
// $FF: synthetic method
@NotNull
public static final ParsableNumber box_impl(@NotNull String v) {
Intrinsics.checkParameterIsNotNull(v, "v");
return new ParsableNumber(v);
}
// $FF: synthetic method
@NotNull
public final String unbox_impl() { return this.original; }
//more Object related implementations
}

The generated wrapper class ParsableNumber pretty much looks like the earlier shown WrappedInt class. One important difference, however, is the getParsed_impl function, which represents our computable property parsed. As you can see, the function is implemented as a static function that takes a string and returns a BigDecimal. So how is this used in the caller code?

@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);
}

As expected, getParsableNumber does not have any reference to our wrapper type. It simply returns the String without introducing any new type. In the main, we see that the static getParsed_impl is used to parse the given String into a BigDecimal. Again, no usage of ParsableNumber.

Reducing the scope of extension functions

A common issue with extension functions is that they may pollute your namespace if defined on general types like String. As an example, you may want to have an extension function that converts a JSON string into a corresponding type:

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

To convert a given string into some data holder JsonData, you would then do:

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

However, the extension function is available on strings that represent other data as well although it might not make much sense:

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

This code will fail since the String does not contain valid JSON data. What can we do to make the extension shown above only available for certain strings? Yep, inline classes can help with that:

Narrow down extension scope with inline class
@JvmInline
value class JsonString(val value: String)
inline fun <reified T> JsonString.asJson() =
jacksonObjectMapper().readValue<T>(this.value)
view raw json-inline.kt hosted with ❤ by GitHub

When we introduce a wrapper for strings that hold JSON data and change the extension to use a JsonString receiver accordingly, the issue described above has been solved. The extension won’t appear on any arbitrary String anymore and instead only extend the ones we consciously wrapped in a JsonString.

Unsigned Types

Another great use case of inline classes becomes apparent when looking at the unsigned integer types that were added to the language with version 1.3:

@JvmInline
public value class UInt @PublishedApi internal constructor(@PublishedApi internal val data: Int) : Comparable<UInt>
view raw unit_def.kt hosted with ❤ by GitHub

As you can see, the UInt class is defined as an unsigned class that wraps a regular signed integer data. You can learn more about this feature in the corresponding KEEP.

Conclusion

Inline classes are a great tool we can use to reduce heap allocations for wrapper types and which helps us solve different kinds of problems. However, be aware that certain scenarios such as using inline classes as nullable types require boxing. Nevertheless, it’s good to know this little but powerful tool and have it in mind the next time you come across one of the use cases discussed.

This article was originally published on proandroiddev.com on September 21, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu