1. Introduction
Kotlin is a modern programming language that has gained a lot of popularity in recent years. One of its newest features is generic inline classes. In this tutorial, we’ll explore what generic inline classes are, how to use them, and their advantages and limitations.
2. What Are Generic Inline Classes?
Inline classes are lightweight wrapper classes introduced in Kotlin 1.1 that enforce type constraints at compile-time without runtime overhead. They can only hold a single value and their purpose is mainly to provide a runtime-optimized wrapper for primitives.
Generic classes, on the other hand, are parameterized classes that allow for type-safe operations on different types. The combination of inline classes and generic classes in Kotlin 1.7.20 provides several benefits.
3. How to Use Generic Inline Classes
To define a generic inline class in Kotlin, use the value modifier in combination with the class keyword and one or more type parameters. The type parameters are specified using angle brackets
value class Wrapper<T>(val value: T)
In this example, Wrapper is a generic inline class that encapsulates a value of type T.
4. Comparison Between Regular Classes and Generic Inline Classes
These special classes do not incur any runtime overhead as the type parameter is erased at runtime and the generated bytecode uses the underlying type directly. This allows for efficient and type-safe code without runtime performance penalties. Let’s write an example to illustrate the difference:
value class Wrapper<T>(val value: T)
fun main() {
val wrapper = Wrapper<Int>(42)
val value: Int = wrapper.value // No runtime overhead
val list = listOf(42)
val item: Int = list[0] // Boxing and unboxing overhead
}
In this example, the Wrapper class is a regular class that takes a type parameter T. Accessing the value property incurs no runtime overhead. However, accessing an item in the list incurs boxing and unboxing overhead.
5. Advantages of Using Generic Inline Classes
In contrast to normal classes, generic inline classes offer improved performance, memory efficiency, type safety, and code readability. They also simplify code and reduce boilerplate.
5.1. Improved Performance and Memory Efficiency
As mentioned earlier, generic inline classes do not incur any runtime overhead, making them highly efficient in terms of performance and memory usage. The type parameter is erased at runtime, and the generated bytecode uses the underlying type directly, resulting in optimal performance.
5.2. Type Safety and Code Readability
Strong type safety at compile-time ensures the correct types are used at runtime. This helps catch type-related errors early in the development process and leads to more robust and reliable code. Additionally, the use of generic inline classes can improve code readability by providing meaningful type constraints and reducing the need for explicit type casting.
5.3. Simplified Code and Reduced Boilerplate
Code can be simplified using generic inline classes by encapsulating complex data types or operations on different types within a single class, reducing the need for repetitive boilerplate code. This can lead to more concise and maintainable code, improving developer productivity. Let’s prepare two sample entities: User and Wallet. As we’ll see, our Id
value class Id<T>(val value: T)
interface Identifiable<T> {
val id: Id<T>
}
data class User(override val id: Id<String>, val name: String) : Identifiable<String>
data class Wallet(override val id: Id<Int>, val currency: String) : Identifiable<Int>
fun <ID, T : Identifiable<ID>> findById(entities: List<T>, id: Id<ID>): T? {
return entities.firstOrNull { it.id == id }
}
In this example, we define a generic inline class Id
We then define a findById() function that takes a list of Identifiable entities and an Id
Notice how it’s possible to use multiple implementations of Identifiable and still leverage our generic inline class Id:
@Test
fun `Find User with Generic Inline Classes`() {
val user1 = User(Id("1"), "Alice")
val user2 = User(Id("2"), "Bob")
val wallet = Wallet(Id(1), "Bitcoin")
val foundUser = findById(listOf(user1, user2, wallet), Id("2"))
assertEquals(user2, foundUser)
}
6. Limitations of Generic Inline Classes
There are some limitations to be aware of when using generic inline classes, such as restrictions on class hierarchy, inability to implement interfaces, and limited usage in some scenarios:
- Restrictions on Class Hierarchy – Generic inline classes cannot extend other classes or be extended by other classes.
- Inability to Implement Interfaces – Generic inline classes also cannot implement interfaces.
- Limited Usability – While powerful and efficient, operations such as complex type transformations or dynamic type checking won’t have generic inline classes as their best choice of implementation.
7. Use Cases for Generic Inline Classes
Generic inline classes have practical applications in various scenarios, including:
- Wrapping primitive data types to provide type safety and enforce constraints
- Creating generic collections or containers that operate on different types
- Encapsulating operations on different types, such as mathematical calculations or data manipulations
7.1. Type-Safe Data Encapsulation
Our inline classes can be used to wrap primitive data types, enforcing type safety and constraints at compile time. For example, we can use an inline class to encapsulate a Percentage value to ensure it falls within the valid range of 0 to 100:
value class Percentage(val value: Double) {
init {
require(value in 0.0..100.0) { "Percentage value must be between 0 and 100" }
}
}
7.2. Generic Collections or Containers
We can also leverage the benefits of generic inline classes when dealing with generic collections or containers that can operate on different types. This can provide flexibility and re-usability in handling data of different types:
inline class Box<T>(val value: T)
val intBox: Box<Int> = Box(42)
val stringBox: Box<String> = Box("Hello, Baeldung!")
7.3. Encapsulating Operations on Different Types
Generic inline classes can be used to encapsulate operations on different types, such as mathematical calculations or data manipulations. This can provide a clean and concise way to perform common operations on various types:
value class Name<T : CharSequence>(val value: T) {
operator fun plus(other: Name<T>): Name<CharSequence> = Name("$value ${other.value}")
}
@Test
fun `Calculate full name`() {
val firstName = Name("Leonardo")
val lastName = Name("Colman")
val fullName = firstName + lastName
assertEquals("Leonardo Colman", fullName.value)
}
8. Conclusion
Kotlin 1.7.20 introduces the powerful feature of generic inline classes, offering type-safe and efficient encapsulation of data and operations on different types with improved performance, memory efficiency, and simplified code.
Generic inline classes are a valuable addition to Kotlin. However, it’s important to be aware of their limitations, such as class hierarchy restrictions and limited usage scenarios.
Overall, generic inline classes provide flexibility and expressiveness in Kotlin code, allowing developers to write more efficient, type-safe, and readable applications. As always, the code used in this article is available over on GitHub.