Как стать автором
Обновить
0
FUNCORP
Разработка развлекательных сервисов

Kotlin: статика, которой нет

Время на прочтение 10 мин
Количество просмотров 32K

В этой статье пойдёт речь об использовании статики в 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, "<set-?>");
     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<T> {
    fun factoryMethod(): T
}


class SimpleClassKotlin1 {

    companion object : FactoryInterface<MyClass> {
        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, "<set-?>");
        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.

На этом у меня всё. Думаю, что тему разобрали довольно подробно. Вопросы пишите в комментарии.
Теги:
Хабы:
+14
Комментарии 6
Комментарии Комментарии 6

Публикации

Информация

Сайт
funcorp.dev
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Кипр
Представитель
ulanana

Истории