Kotlin Value Classes
Introduction
Kotlin Value Classes (previously known as inline classes) are a powerful feature that lets you create type-safe wrappers around existing types without the runtime overhead of creating actual object instances. They help you design more type-safe APIs while maintaining the performance characteristics of the underlying primitive types.
In normal object-oriented programming, wrapping a primitive value in a class would incur memory allocation overhead. Value classes solve this problem by "inlining" the underlying value, eliminating the wrapper object at runtime in most cases.
Understanding Value Classes
Basic Syntax
To create a value class in Kotlin, use the value class
declaration and specify exactly one property in the primary constructor:
value class Password(val value: String)
This creates a type-safe wrapper around a String that represents a password. At compile time, you work with the Password
type, but at runtime, in most cases, the compiler uses the underlying String
directly.
How It Works
Let's see how value classes behave compared to regular classes:
// Regular class
class RegularPassword(val value: String)
// Value class
value class ValuePassword(val value: String)
fun main() {
// Using regular class
val regularPassword1 = RegularPassword("secret1")
val regularPassword2 = RegularPassword("secret1")
// Using value class
val valuePassword1 = ValuePassword("secret1")
val valuePassword2 = ValuePassword("secret1")
// Equality checks
println(regularPassword1 == regularPassword2) // false (different instances)
println(valuePassword1 == valuePassword2) // true (compared by value)
// Type information
println(regularPassword1.javaClass) // RegularPassword
println(valuePassword1.javaClass) // Usually optimized away
}
The key difference is that regular classes create distinct objects in memory, while value classes typically don't create any object at runtime - they're "inlined" to their underlying type.
Benefits of Value Classes
1. Type Safety Without Overhead
Value classes allow you to create domain-specific types without performance penalties:
value class UserId(val id: Int)
value class ProductId(val id: Int)
// Functions specific to each type
fun fetchUser(userId: UserId) {
// Works with UserId only
println("Fetching user ${userId.id}")
}
fun fetchProduct(productId: ProductId) {
// Works with ProductId only
println("Fetching product ${productId.id}")
}
fun main() {
val userId = UserId(1001)
val productId = ProductId(2002)
fetchUser(userId) // Works
// fetchUser(productId) // Compile error: Type mismatch
// fetchUser(1001) // Compile error: Type mismatch
fetchProduct(productId) // Works
}
Without value classes, you would either:
- Use primitive types (losing type safety)
- Create full classes (adding runtime overhead)
2. Clearer API Design
Value classes make your code's intent more obvious:
// Without value classes - which parameter means what?
fun updateUserSettings(timeout: Int, retries: Int, maxConnections: Int) {
// Implementation
}
// With value classes - much clearer intent
value class Timeout(val seconds: Int)
value class RetryCount(val count: Int)
value class ConnectionLimit(val max: Int)
fun updateUserSettings(timeout: Timeout, retries: RetryCount, maxConnections: ConnectionLimit) {
// Implementation is self-documenting
println("Setting timeout to ${timeout.seconds}s")
println("Setting retries to ${retries.count}")
println("Setting max connections to ${maxConnections.max}")
}
fun main() {
// No confusion about parameter meaning
updateUserSettings(
Timeout(30),
RetryCount(3),
ConnectionLimit(5)
)
}
Limitations and Edge Cases
Value classes have some limitations you should be aware of:
1. Boxing in Some Cases
Value classes don't always eliminate the wrapper object. Boxing (creating an actual instance) occurs in these situations:
value class Name(val value: String) {
fun greet() {
println("Hello, $value!")
}
}
fun main() {
val name = Name("Alice")
// These use cases will cause boxing (creating actual objects):
// 1. Using as a nullable type
val nullableName: Name? = name
// 2. Using as a generic type parameter
val namesList = listOf(name)
// 3. Using the value class as a receiver for extension function
name.greet()
}
2. Property and Function Restrictions
Value classes can only have one primary property, but they can have additional properties if they're derived from the primary one:
value class Email(val address: String) {
// Additional computed properties are OK
val domain: String
get() = address.substringAfter('@')
// Member functions are OK too
fun isValid(): Boolean {
return address.contains('@')
}
// But you cannot have secondary properties with backing fields
// (This would not compile):
// var usageCount: Int = 0
}
fun main() {
val email = Email("[email protected]")
println("Domain: ${email.domain}") // Domain: example.com
println("Is valid: ${email.isValid()}") // Is valid: true
}
Real-World Applications
Example 1: Money Handling
Using value classes for representing money ensures type safety and prevents errors:
value class Dollars(val amount: BigDecimal) {
operator fun plus(other: Dollars): Dollars = Dollars(amount + other.amount)
operator fun minus(other: Dollars): Dollars = Dollars(amount - other.amount)
}
value class Euros(val amount: BigDecimal) {
operator fun plus(other: Euros): Euros = Euros(amount + other.amount)
operator fun minus(other: Euros): Euros = Euros(amount - other.amount)
}
fun processDollarPayment(payment: Dollars) {
println("Processing $${payment.amount} payment")
}
fun main() {
val salary = Dollars(BigDecimal("5000.00"))
val bonus = Dollars(BigDecimal("1000.00"))
val total = salary + bonus
val euroPayment = Euros(BigDecimal("500.00"))
processDollarPayment(total) // Works fine
// processDollarPayment(euroPayment) // Compile error: prevents mixing currencies
}
Example 2: Secure API Design
Value classes can help prevent security vulnerabilities by ensuring proper content validation:
value class SanitizedHtml private constructor(val value: String) {
companion object {
fun create(input: String): SanitizedHtml {
// Imagine this does proper HTML sanitization
val sanitized = input.replace("<script>", "<script>")
return SanitizedHtml(sanitized)
}
}
}
// API that only accepts sanitized HTML
fun renderUserContent(content: SanitizedHtml) {
println("Rendering HTML safely: ${content.value}")
}
fun main() {
val userInput = "<div>Hello</div><script>alert('XSS attack');</script>"
// This won't compile - enforcing sanitization
// renderUserContent(SanitizedHtml(userInput))
// This works - proper sanitization is ensured
val sanitized = SanitizedHtml.create(userInput)
renderUserContent(sanitized)
}
Migrating from Inline Classes
If you've used Kotlin's inline classes before, they're now called value classes. The migration is straightforward:
// Old syntax (still works but deprecated)
inline class OldSyntax(val value: String)
// New syntax (preferred)
value class NewSyntax(val value: String)
Both declarations work similarly, but value class
more accurately reflects the concept and aligns with similar features in other languages.
Best Practices
-
Use for domain-specific types: Create value classes for domain concepts like
UserId
,Email
, orTemperature
. -
Add validation: Consider adding validation in constructors or companion objects.
-
Keep them immutable: Value classes should typically represent immutable values.
-
Be aware of boxing: Remember that value classes can still be boxed in some cases.
-
Document interfaces carefully: When a value class implements an interface, document that this may lead to boxing.
Summary
Kotlin Value Classes provide an elegant solution to a common problem in strongly-typed languages: how to create domain-specific types without incurring runtime penalties. They're perfect for creating type-safe wrappers around primitive types, ensuring both safety and performance.
By using value classes, you can:
- Improve type safety with zero overhead
- Make your API intentions clearer
- Prevent common errors like mixing incompatible values
- Create domain-specific types that are both expressive and efficient
Additional Resources
Exercises
-
Create a value class
Temperature
that wraps aDouble
value and includes methods to convert between Celsius and Fahrenheit. -
Design a small library for handling HTTP requests using value classes for URL, Headers, and RequestBody to ensure type safety.
-
Implement a money handling system with different currencies as value classes and a safe conversion mechanism between them.
-
Create a value class for email addresses with validation and domain extraction functionality.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)