Avatar ·

Константы в Kotlin: правильные способы объявления и использования

Всем привет! Я только начал изучать Kotlin после Java, и меня смущает вопрос объявления констант. В Java я привык писать `public static final String MY_CONST = "value";`, а в Kotlin вижу несколько разных подходов и не понимаю, какой из них правильный. Например, я вижу, что можно писать: ```kotlin class MyClass { companion object { val MY_CONST = "value" } } ``` Или даже просто: ```kotlin val MY_CONST = "value" ``` Но в некоторых гайдах пишут про `const val`, а в других — что `const` работает только с примитивами. Еще я запутался с `object` и `companion object` — когда что использовать? И нужно ли вообще использовать `companion object` как аналог `static final` из Java, или это антипаттерн? Подскажите, пожалуйста, какой подход считается правильным и идиоматичным для Kotlin? Хочется сразу писать "правильно", чтобы потом не переделывать. Особенно интересует разница между `val` и `const val`, и где лучше хранить константы — в классе, в отдельном файле или в объекте? Заранее спасибо за помощь! <<>>

Основные способы объявления констант в Kotlin

В Kotlin есть два основных подхода: val и const val. Они отличаются тем, когда и как вычисляются значения. Давайте разберёмся по порядку.

1. const val — настоящие константы времени компиляции

const val работает только с примитивами (Int, Long, Double, Boolean, Char) и строками (String). Значение должно быть известно на этапе компиляции. Такие константы встраиваются прямо в байт-код — как в Java static final.

// Верхнеуровневая константа (рекомендуется)
const val DEFAULT_TIMEOUT_MS = 5000L
const val APP_NAME = "MyApp"

// Внутри object (аналог статического класса)
object AppConstants {
    const val BASE_URL = "https://api.example.com"
    const val MAX_RETRIES = 3
}

// Внутри companion object (если константа логически связана с классом)
class NetworkClient {
    companion object {
        const val DEFAULT_PORT = 8080
    }
}

2. val — константы времени выполнения (ленивые или вычисляемые)

val — это read-only переменная. Она может быть инициализирована в рантайме, например, результатом функции или сложным выражением.

// Вычисляется при первом обращении
val currentTime: Long get() = System.currentTimeMillis()

// Ленивая инициализация
val heavyConfig: Config by lazy {
    loadConfigFromFile()
}

// Константа, которая не встраивается в код (без const)
val PI_APPROX = 3.14159  // не const — не будет встроена компилятором

3. Где хранить константы — лучшие практики

  • Верхнеуровневые константы (в отдельном файле Constants.kt) — самый идиоматичный способ для Kotlin. Просто пишите const val вне класса.
  • Внутри object — если констант много и они логически сгруппированы (например, ApiEndpoints, DatabaseKeys).
  • Внутри companion object — если константы тесно связаны с классом (например, порт по умолчанию для NetworkClient).
  • Избегайте хранения констант прямо внутри класса без companion — тогда каждая копия класса будет иметь свою копию переменной.

4. Пример сравнения Java vs Kotlin

// Java
public class Config {
    public static final int MAX_CONNECTIONS = 10;
    public static final String API_KEY = "12345";
}

// Kotlin — эквивалент
object Config {
    const val MAX_CONNECTIONS = 10
    const val API_KEY = "12345"
}

// Или ещё более идиоматично — на верхнем уровне
const val MAX_CONNECTIONS = 10
const val API_KEY = "12345"

5. Когда использовать const val, а когда просто val?

  • const val — когда значение известно на этапе компиляции И это примитив/строка. Используйте всегда, когда возможно — это даёт производительность (встраивание) и безопасность.
  • val — когда значение вычисляется в рантайме (например, val uuid = UUID.randomUUID()), или когда это не примитив/строка (например, список, объект).
  • const val НЕЛЬЗЯ использовать для: val list = listOf(1,2,3), val date = Date(), val config = MyConfig().

Итог: рекомендованный подход

// Файл Constants.kt
const val DEFAULT_TIMEOUT = 30_000L
const val API_VERSION = "v2"

// Файл NetworkConfig.kt (если нужно сгруппировать)
object NetworkConfig {
    const val BASE_URL = "https://api.example.com"
    const val RETRY_COUNT = 3
}

// В классе — только если логически принадлежит классу
class UserRepository {
    companion object {
        const val TABLE_NAME = "users"
    }
}

Этот подход считается идиоматичным для Kotlin, не требует создания лишних объектов и даёт максимальную производительность.

<<>>

Альтернативный подход: использование аннотации @JvmField и константы в интерфейсах

Если вы работаете в гибридном проекте Java + Kotlin или хотите сохранить совместимость с Java-кодом, можно использовать аннотацию @JvmField для создания статических полей:

// Kotlin — создаст статическое поле в Java
class MyClass {
    companion object {
        @JvmField
        val MY_CONST = "value"  // Без const, но @JvmField делает его static final
    }
}

// Java-код будет обращаться: MyClass.MY_CONST

Ещё один вариант — использовать interface для хранения констант (это старый Java-паттерн, но в Kotlin тоже работает):

interface AppConstants {
    companion object {
        const val MAX_SIZE = 100
        const val DEFAULT_NAME = "default"
    }
}

// Использование
class MyActivity : AppConstants {
    fun show() {
        println(MAX_SIZE)  // доступно через интерфейс
    }
}

Минусы: такой подход считается устаревшим и неидиоматичным для чистого Kotlin. Лучше использовать верхнеуровневые константы или объекты. Однако если вам нужно, чтобы константы были видны из Java как static final поля, @JvmField может быть полезен.

Важно: без const val и без @JvmField поля в companion object будут доступны через геттер, что чуть медленнее и не является настоящей константой времени компиляции.