company_banner

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, "<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
    • 10,4k
    • 6
    FunCorp
    284,00
    Разработка развлекательных сервисов
    Поделиться публикацией

    Похожие публикации

    Комментарии 6

      0
      Вот никогда не понимал зачем статические методы если есть функции. А отказ от функий совсем не понимал… Math.sin туды его в качель
        0

        А сеттер — метод вроде же.

        +2

        Всю статью можно свести к одному слову: @JvmStatic

          0
          В версии 1.3 появились inline классы kotlinlang.org/docs/reference/inline-classes.html. Так что статика появилась, но не та которую мы привыкли видеть.
            +1
            И какая связь между инлайн-классами и статикой?
            0
            Правда, документ этот датируется маем 2017 года, а на дворе уже конец 2018.
            Ничего страшного, в нём можно найти Subject variable in when (№19) который добавили в последнем мажорном релизе. Но также следует смотреть на количество голосов «За» и «Против», так как можно заметить что статические поля получили довольно противоречивую оценку (122 против 88 голосов) в отличие от №19 (122 против 6 голосов).

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое