Классы без лишнего веса: инлайн-классы в Kotlin
Сегодня поговорим о Kotlin и его инлайн‑классах. Честно говоря, когда я впервые услышал об этой фиче, подумал: «Опять что‑то выдумали, чтобы жизнь медом не казалась». Но, разобравшись, понял, что это очень даже полезная штука.
Зачем нам инлайн-классы?
Начнем с простого. Представьте, что у вас есть идентификаторы пользователей, заказов или каких-нибудь транзакций. Все это, как правило, обычные числа или строки. Но вот беда: легко перепутать userId
с orderId
, ведь оба — Int
или String
. Ошибки из-за этого могут быть самыми неприятными.
И вот тут на хороши инлайн-классы. Они позволяют создать типобезопасные обертки над существующими типами данных без дополнительного накладного расхода на производительность.
Создаем первый инлайн-класс
Посмотрим, как это выглядит на практике.
@JvmInline
value class UserId(val id: Int)
Обратите внимание на аннотацию @JvmInline
и ключевое слово value
. Это говорит компилятору, что мы хотим создать инлайн-класс. Теперь мы можем использовать UserId
как отдельный тип:
fun getUserName(userId: UserId): String {
// какая-то логика получения имени пользователя
return "User_${userId.id}"
}
val userId = UserId(42)
println(getUserName(userId)) // Output: User_42
Теперь, если вы случайно попытаетесь передать OrderId
вместо UserId
, компилятор вас остановит:
@JvmInline
value class OrderId(val id: Int)
val orderId = OrderId(100)
// Ошибка компиляции!
// println(getUserName(orderId))
Как это работает?
Инлайн-классы компилируются таким образом, что их экземпляры инлайнятся в байт-коде. Это означает, что при использовании UserId
фактически передается просто Int
, без дополнительных объектов.
Например, функция:
fun processUserId(userId: UserId) {
println("Processing user ID: ${userId.id}")
}
val userId = UserId(123)
processUserId(userId)
Компилируется в байт-код, эквивалентный:
fun process(id: Int) {
println(id)
}
Таким образом, мы получаем типобезопасность на уровне исходного кода и отсутствие накладных расходов на уровне выполнения.
Добавляем методы и свойства
Инлайн-классы могут содержать методы и вычисляемые свойства:
@JvmInline
value class Email(val address: String) {
val domain: String
get() = address.substringAfter('@')
fun isValid(): Boolean {
return address.contains("@") && address.contains(".")
}
}
val email = Email("test@example.com")
println(email.domain) // Output: example.com
println(email.isValid()) // Output: true
Можно даже перегружать операторы:
@JvmInline
value class Dollars(val amount: Int) {
operator fun plus(other: Dollars) = Dollars(this.amount + other.amount)
}
val wallet1 = Dollars(50)
val wallet2 = Dollars(70)
val total = wallet1 + wallet2
println(total.amount) // Output: 120
Инлайн-классы прекрасно работают с коллекциями:
val userIds = listOf(UserId(1), UserId(2), UserId(3))
userIds.forEach { println(it.id) }
И даже с обобщениями:
fun getFirstElement(list: List): T {
return list.first()
}
val firstUserId = getFirstElement(userIds)
println(firstUserId.id) // Output: 1
Взаимодействие с Java
Если вы используете Kotlin вместе с Java, будьте внимательны. Инлайн-классы в Kotlin выглядят как их базовые типы в Java:
// Kotlin
@JvmInline
value class Token(val value: String)
// Java
public class Main {
public static void main(String[] args) {
Token token = new Token("abc123"); // Ошибка компиляции!
}
}
Чтобы Java могла использовать ваш инлайн-класс, нужно предоставить вспомогательный метод-фабрику:
// Kotlin
@JvmInline
value class Token(val value: String) {
companion object {
@JvmStatic
fun create(value: String) = Token(value)
}
}
// Java
public class Main {
public static void main(String[] args) {
Token token = Token.create("abc123"); // Теперь все ок
}
}
Ограничения инлайн-классов
Не все так радужно. Есть некоторые ограничения:
Наследование: инлайн-классы не могут наследоваться и не могут быть родителями других классов.
// Ошибка компиляции! value class MyInt(val value: Int) : Number()
Свойства только val: внутри инлайн-класса все свойства должны быть
val
.// Ошибка компиляции! value class MutableValue(var value: Int)
Инициализатор: нельзя иметь дополнительные свойства или инициализацию вне главного конструктора.
// Ошибка компиляции! value class Invalid(val x: Int) { val y = x * 2 }
Использование с nullable типами
Инлайн-классы могут быть nullable:
val nullableUserId: UserId? = null
fun printUserId(userId: UserId?) {
if (userId != null) {
println("User ID: ${userId.id}")
} else {
println("User ID is null")
}
}
printUserId(nullableUserId) // Output: User ID is null
Но есть нюанс: nullable инлайн-классы не инлайнятся и представляют собой полноценные объекты.
Инлайн-классы и сериализация
С библиотекой kotlinx.serialization можно работать с инлайн-классами:
@Serializable
@JvmInline
value class ProductCode(val code: String)
val productCode = ProductCode("ABC123")
val json = Json.encodeToString(productCode)
println(json) // Output: "ABC123"
val decoded = Json.decodeFromString(json)
println(decoded.code) // Output: ABC123
Рефлексия
С рефлексией все немного сложнее. Инлайн-классы могут вести себя неожиданно при использовании рефлексии:
@JvmInline
value class Tag(val value: String)
fun main() {
val tag = Tag("Kotlin")
val kClass = tag::class
println(kClass.simpleName) // Output: String, а не Tag!
}
Инлайн-классы против типалиасов
Можно задаться вопросом:, а почему бы не использовать typealias
?
@Serializable
@JvmInline
value class ProductCode(val code: String)
val productCode = ProductCode("ABC123")
val json = Json.encodeToString(productCode)
println(json) // Output: "ABC123"
val decoded = Json.decodeFromString(json)
println(decoded.code) // Output: ABC123
Но typealias
не создают новый тип, это просто псевдоним. Компилятор не поможет, если вы перепутаете UserId
и OrderId
. Инлайн-классы же создают новый тип с полной типобезопасностью.
Если остались вопросы или хотите поделиться своим опытом — пишите, обсудим!
А завтра вечером (12 ноября) в Otus пройдет открытый урок, на котором участники рассмотрят ключевые отличия между Kotlin и Java.
Первая часть занятия будет посвящена таким концепциями, как null-безопасность, сокращение шаблонного кода, лямбда-выражения, и другим преимуществам Kotlin. Во второй части участники напишут веб-сервис с CRUD операциями на Java, а затем преобразуют его в Kotlin, чтобы на практике увидеть, как синтаксис Kotlin упрощает код.
Если заинтересовало, записывайтесь на занятие по ссылке.