Kotlin: статика, которой нет
В этой статье пойдёт речь об использовании статики в Kotlin.
Начнём.
В Kotlin нет статики!
Об этом говорится в официальной документации.
И вроде бы на этом можно было бы и закончить статью. Но позвольте, как же так? Ведь если в Android Studio вставить код на Java в файл на Kotlin, то умный конвертер сделает магию, превратит всё в код на нужном языке и всё заработает! А как же полная совместимость с Java?
В этом месте любой разработчик, узнав про отсутствие статики в Kotlin, полезет в документацию и форумы разбираться с этим вопросом. Давайте разбираться вместе, вдумчиво и кропотливо. Постараюсь, чтобы к концу этой статьи вопросов по этой теме осталось как можно меньше.
В чём проявляет себя статика в Java? Бывают:
- статические поля класса
- статические методы класса
- статические вложенные классы
Проведём эксперимент (это первое, что приходит на ум).
Создадим простой Java-класс:
public class SimpleClassJava1 {
public static String staticField = "Hello, static!";
public static void setStaticValue (String value){
staticField = value;
}
}
Здесь всё легко: в классе создаём статическое поле и статический метод. Всё делаем публичным для экспериментов с доступом извне. Связываем поле и метод логически.
Теперь создадим пустой Kotlin-класс и попробуем скопировать в него всё содержимое класса SimpleClassJava1. На образовавшийся вопрос про конвертацию отвечаем «да» и смотрим что получилось:
class SimpleClassKotlin1 {
var staticField = "Hello, static!"
fun setStaticValue(value: String) {
staticField = value
}
}
Кажется, это не совсем то, что нам надо… Чтобы удостовериться в этом, преобразуем байт-код этого класса в Java-код и смотрим, что вышло:
public final class SimpleClassKotlin1 {
@NotNull
private String staticField = "Hello, static!";
@NotNull
public final String getStaticField() {
return this.staticField;
}
public final void setStaticField(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "");
this.staticField = var1;
}
public final void setStaticValue(@NotNull String value) {
Intrinsics.checkParameterIsNotNull(value, "value");
this.staticField = value;
}
}
Да. Всё именно так, как и показалось. Никакой статикой здесь и не пахнет. Конвертер просто обрубил в сигнатуре модификатор static, как будто его и не было. На всякий случай сразу cделаем вывод: не стоит слепо доверять конвертеру, иногда он может преподнести неприятные сюрпризы.
К слову сказать, примерно полгода назад конвертация того же Java-кода в Kotlin показала бы несколько иной результат. Так что ещё раз: осторожнее с автоматической конвертацией!
Экспериментируем дальше.
Идём в любой класс на Kotlin и пробуем вызвать в нём статические элементы Java-класса:
SimpleClassJava1.setStaticValue("hi!")
SimpleClassJava1.staticField = "hello!!!"
Вот как! Всё прекрасно вызывается, даже автозаполнение кода нам всё подсказывает! Довольно любопытно.
Теперь перейдём к более содержательной части. Действительно, создатели Kotlin решили уйти от статики в том виде, в котором мы привыкли её использовать. Зачем было сделано именно так и не иначе рассуждать не будем — споров и мнений по этому поводу в сети предостаточно. Мы же просто будем выяснять как с этим жить. Естественно, нас не просто так лишили статики. Kotlin даёт нам набор инструментов, которыми мы можем компенсировать утерянное. Они подходят для внутреннего использования. И обещанную полную совместимость с Java-кодом. Поехали!
Самое быстрое и простое, что можно осознать и начать использовать, — ту альтернативу, которую нам предлагают вместо статических методов, — функции уровня пакета. Что это такое? Это функция, не принадлежащая какому-либо классу. То есть эта некая логика, находящаяся в вакууме где-то в пространстве пакета. Мы можем описать её в любом файле внутри интересующего нас пакета. Например, назовём этот файл JustFun.kt и расположим его в пакете com.example.mytestapplication
package com.example.mytestapplication
fun testFun(){
// some code
}
Преобразуем байт-код этого файла в Java и заглянем внутрь:
public final class JustFunKt {
public static final void testFun() {
// some code
}
}
Видим, что в Java создаётся класс, имя которого учитывает название файла, в котором описана функция, а сама функция превращается в статический метод.
Теперь если мы хотим в Kotlin вызвать функцию testFun
из класса (или такой же функции), находящемся в пакете package com.example.mytestapplication
(то есть том же пакете, что и функция), то мы можем просто без дополнительных фокусов обратиться к ней. Если же мы вызываем её из другого пакета, то мы должны произвести импорт, привычный нам и обычно применимый к классам:
import com.example.pavka.mytestapplication.testFun
Если говорить про вызов функции testFun
из Java-кода, то импорт функции нужно производить всегда, независимо от того из какого пакета мы её вызываем:
import static com.example.pavka.mytestapplication.ForFunKt.testFun;
В документации говорится, что в большинстве случаев вместо статических методов нам достаточно использовать функции уровня пакета. Однако, по моему личному мнению (которое не обязательно должно совпадать с мнением всех остальных), данный способ реализации статики подходит только для небольших проектов.
Получается, что эти функции не принадлежат явно какому-либо классу. Визуально их вызов выглядит как вызов метода класса (или его родителя), в котором мы находимся, что иногда может сбить с толку. Ну и главное — функция с таким названием может быть в пакете только одна. Даже если мы попробуем создать одноимённую функцию в другом файле, система выдаст нам ошибку. Если говорить про большие проекты, то у нас довольно часто бывают, например, разные фабрики, имеющие одноименные статические методы.
Посмотрим на другие альтернативы реализации статических методов и полей.
Вспомним, что такое статическое поле класса. Это поле класса, принадлежащее классу, в котором оно объявлено, но не принадлежащее конкретному инстансу класса, то есть создаётся в единственном экземпляре на весь класс.
Kotlin предлагает нам для этих целей использовать некую дополнительную сущность, которая так же существует в единственном экземпляре. Иначе говоря — синглтон.
Для объявления синглтонов в Kotlin имеется ключевое слово object.
object MySingltoneClass {
// some code
}
Инициализируются такие объекты лениво, то есть в момент первого обращения к ним.
Ок, в Java тоже есть синглтоны, причём здесь статика?
Для любого класса в Kotlin мы можем создать сопутствующий объект, или объект-компаньон. Некий синглтон, привязанный к конкретному классу. Это можно сделать, используя совместно 2 ключевых слова companion и object
:
class SimpleClassKotlin1 {
companion object{
var companionField = "Hello!"
fun companionFun (vaue: String){
// some code
}
}
}
Здесь мы имеем класс SimpleClassKotlin1
, внутри которого мы объявляем синглтон с помощью ключевого слова object и привязываем его к объекту, внутри которого он объявляется ключевым словом companion. Здесь можно обратить внимание на то, что в отличие от предыдущего объявления синглтона (MySingltoneClass) не указывается имя класса-синглтона. В случае, если объект объявлен компаньоном, допускается не указывать его имя. Тогда ему автоматически присвоится имя Companion
. Если нужно, мы можем получить инстанс класса-компаньона таким образом:
val companionInstance = SimpleClassKotlin1.Companion
Однако, обращение к свойствам и методам класса-компаньона можно делать напрямую, через обращение класса, к которому он привязан:
SimpleClassKotlin1.companionField
SimpleClassKotlin1.companionFun("Hi!")
Это уже сильно похоже на вызов статических полей и классов, не так ли?
Если нужно, мы можем присвоить классу-компаньону имя, но на практике это делается очень редко. Из интересных особенностей сопутствующих классов можно отметить то, что он, как и любой обычный класс может реализовывать интерфейсы, что может помочь нам иногда внести в код чуть больше порядка:
interface FactoryInterface {
fun factoryMethod(): T
}
class SimpleClassKotlin1 {
companion object : FactoryInterface {
override fun factoryMethod(): MyClass = MyClass()
}
}
Класс-компаньон у класса может быть только один. Однако никто не запрещает нам объявлять внутри класса сколько угодно объектов-синглтонов, но в этом случае мы должны явно указать имя этого класса и, соответственно, указывать это имя при обращении к полям и методом этого класса.
Говоря ещё о классах, объявленных как object, можно сказать, что мы также можем в них же объявлять вложенные object, но не можем объявлять в них companion object.
Пора заглянуть «под капот». Возьмём наш простенький класс:
class SimpleClassKotlin1 {
companion object{
var companionField = "Hello!"
fun companionFun (vaue: String){
}
}
object OneMoreObject {
var value = 1
fun function(){
}
}
Теперь декомпилируем его байт-код в Java:
public final class SimpleClassKotlin1 {
@NotNull
private static String companionField = "Hello!";
public static final SimpleClassKotlin1.Companion Companion = new SimpleClassKotlin1.Companion((DefaultConstructorMarker)null);
public static final class OneMoreObject {
private static int value;
public static final SimpleClassKotlin1.OneMoreObject INSTANCE;
public final int getValue() {
return value;
}
public final void setValue(int var1) {
value = var1;
}
public final void function() {
}
static {
SimpleClassKotlin1.OneMoreObject var0 = new SimpleClassKotlin1.OneMoreObject();
INSTANCE = var0;
value = 1;
}
}
public static final class Companion {
@NotNull
public final String getCompanionField() {
return SimpleClassKotlin1.companionField;
}
public final void setCompanionField(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "");
SimpleClassKotlin1.companionField = var1;
}
public final void companionFun(@NotNull String vaue) {
Intrinsics.checkParameterIsNotNull(vaue, "vaue");
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
Смотрим, что же получилось.
Свойство объекта-компаньона представлено в виде статического поля нашего класса:
private static String companionField = "Hello!";
Похоже, что это именно то, чего мы хотели. Однако это поле приватное и доступ к нему осуществляется через геттер и сеттер нашего класса компаньона, который здесь представлен в виде public static final class
, а его инстанс представлен в виде константы:
public static final SimpleClassKotlin1.Companion Companion = new SimpleClassKotlin1.Companion((DefaultConstructorMarker)null);
Функция companionFun не стала статическим методом нашего класса (наверное, и не должна была). Она так и осталась функцией синглтона, инициализированного в классе SimpleClassKotlin1. Однако, если вдуматься, то логически это примерно одно и то же.
С классом OneMoreObject
ситуация очень похожая. Стоит отметить только то, что здесь, в отличии от компаньона поле класса value не переехало в класс SimpleClassKotlin1
, а осталось в OneMoreObject
, но также стало статическим и получило сгенерированные геттер и сеттер.
Попробуем осмыслить всё вышеописанное.
Если мы хотим реализовать статические поля или методы класса в Kotlin, то для этого следует воспользоваться companion object, объявленным внутри этого класса.
Вызов этих полей и функций из Kotlin будет выглядеть совершенно аналогично вызову статики в Java. А что будет, если мы попробуем вызвать эти поля и функции в Java?
Автозаполнение подсказывает нам, что доступны следующие вызовы:
SimpleClassKotlin1.Companion.companionFun("hello!");
SimpleClassKotlin1.Companion.setCompanionField("hello!");
SimpleClassKotlin1.Companion.getCompanionField();
То есть здесь мы никуда не денемся от прямого указания имени компаньона. Соответственно, здесь используется имя, которое присвоилось объекту-компаньону по умолчанию. Не очень удобно, так ведь?
Тем не менее, создатели Kotlin дали возможность сделать так, чтобы в Java это выглядело более привычно. И для этого есть несколько способов.
@JvmField
var companionField = "Hello!"
Если применить эту аннотацию к полю companionField
нашего объекта-компаньона, то при преобразовании байт-кода в Java увидим, что статическое поле companionField
SimpleClassKotlin1 уже не private, а public, а в статическом классе Companion
пропали геттер и сеттер для companionField. Теперь мы можем обращаться из Java-кода к companionField
привычным образом.
Второй способ — это указать для свойства объекта компаньона модификатор lateinit
, свойства с поздней инициализацией. Не забываем, что это применимо только к var-свойствам, а его тип должен быть non-null и не должен быть примитивным. Ну и не забываем, про правила обращения с такими свойствами.
lateinit var lateinitField: String
И ещё один способ: мы можем объявить свойство объекта-компаньона константой, указав ему модификатор const. Несложно догадаться, что это должно быть val-свойство.
const val myConstant = "CONSTANT"
В каждом из этих случаев сгенерированный Java-код будет содержать привычное нам public static поле, в случае с const это поле будет ещё и final. Конечно, стоит понимать, что у каждого из 3х этих случаев есть своё логическое назначение, и только первый из них предназначен специально для удобства использования с Java, остальные получают эту «плюшку» как бы в нагрузку.
Отдельно следует отметить, что модификатор const можно использовать для свойств объектов, объектов-компаньонов и для свойств уровня пакета. В последнем случае мы получим то же, что и при использовании функций уровня пакета и с теми же ограничениями. Сгенерируется Java-код со статическим публичным полем в классе, имя которого учитывает имя файла, в котором мы описали константу. В пакете может быть только одна константа с указанным именем.
Если мы хотим, чтобы функция объекта-компаньона также преобразовалась в статический метод при генерации Java-кода, то для этого нам надо применить к этой функции аннотацию @JvmStatic
.
Также допустимо применять аннотацию @JvmStatic
к свойствам объектов-компаньонов (и просто объектов — синглтонов). В этом случае свойство не превратится в статическое поле, но будут сгенерированы статический геттер и сеттер к этому свойству. Для лучшего понимания посмотрим на вот этот Kotlin-класс:
class SimpleClassKotlin1 {
companion object{
@JvmStatic
fun companionFun (vaue: String){
}
@JvmStatic
var staticField = 1
}
}
В данном случае из Java валидны следующие обращения:
int x;
SimpleClassKotlin1.companionFun("hello!");
x = SimpleClassKotlin1.getStaticField();
SimpleClassKotlin1.setStaticField(10);
SimpleClassKotlin1.Companion.companionFun("hello");
x = SimpleClassKotlin1.Companion.getStaticField();
SimpleClassKotlin1.Companion.setStaticField(10);
Из Kotlin валидны такие вызовы:
SimpleClassKotlin1.companionFun("hello!")
SimpleClassKotlin1.staticField
SimpleClassKotlin1.Companion.companionFun("hello!")
SimpleClassKotlin1.Companion.staticField
Понятно, что для Java следует использовать первые 3, а для Kotlin первые 2. Остальные вызовы всего лишь допустимы.
Теперь осталось прояснить последнее. Как быть со статическим вложенными классами? Тут всё просто — аналогом такого класса в Kotlin является обычный вложенный класс без модификаторов:
class SimpleClassKotlin1 {
class LooksLikeNestedStatic {
}
}
После преобразования байт-кода в Java видим:
public final class SimpleClassKotlin1 {
public static final class LooksLikeNestedStatic {
}
}
Действительно, это то, что нам нужно. Если мы не хотим, чтобы класс был final, то в Kotlin-коде указываем ему модификатор open. Вспомнил об этом на всякий случай.
Думаю, можно подвести итог. Действительно, в самом Kotlin, как и говорилось, нет статики в том виде, в котором мы привыкли её видеть. Но предлагаемый набор инструментов позволяет нам реализовать все типы статики в сгенерированном Java-коде. Также обеспечена полная совместимость с Java, и мы можем напрямую вызывать из Kotlin статические поля и методы Java-классов.
В большинстве случаев, реализация статики в Kotlin требует несколько больше строк кода. Возможно, это один из немногих, а может даже единственный случай, когда в Kotlin нужно писать больше. Тем не менее, к этому быстро привыкаешь.
Думаю, что в проектах, где совместно используется Kotlin и Java-код, можно гибко подходить к выбору используемого языка. Например, для хранения констант, как лично мне кажется, всё же больше подходит Java. Но тут, как и во многом другом стоит руководствоваться ещё и здравым смыслом, и регламентом написания кода в проекте.
И в завершении статьи вот ещё такая информация. Возможно, в будущем в Kotlin всё же появится модификатор static, устраняющий много вопросов и делающий жизнь разработчиков проще, а код короче. Такое предположение я сделал, обнаружив соответствующий текст в пункте 17 документа Kotlin feature descriptions.
Правда, документ этот датируется маем 2017 года, а на дворе уже конец 2018.
На этом у меня всё. Думаю, что тему разобрали довольно подробно. Вопросы пишите в комментарии.