Модификатор Kotlin, которого не должно было быть

Большинство разработчиков Kotlin уверены в том, что свойство val здесь эквивалентно использующемуся в Java свойству final. А что, если я скажу, что это не совсем так? Ведь иногда бывает необходимо задействовать final val?

В отличие от Java, свойства Kotlin являются final по умолчанию. В противном случае они явно помечаются ключевым словом open! То есть выходит, что в ключевом слове final нет необходимости? Заглянем в поисковик:

JavaMentor
JavaMentor

Интернет подтвердил это, поэтому я был очень удивлен, когда в Android Studio мне было предложено добавить final к val:

И действительно, после добавления final проблема была решена:

Таким образом, ключевое слово final со свойствами все-таки используется, но почему и когда нужно добавлять к val final?

Рассмотрим это поведение на простом примере:

class FinalBlog {

    val someProperty: String = "some"

    init {
        print(someProperty + "thing")
    }
}

Здесь все работает так, как и ожидалось, и код выведет something при создании экземпляра класса.

Теперь добавим везде open:

open class FinalBlog {

    open val someProperty: String ="some"

 init {
        print(someProperty + "thing")
    }
}

После чего снова возникнет то же самое предупреждение, что уже появлялось в Android Studio выше:

На самом деле здесь все более чем очевидно. Для класса создаются подклассы, и свойство переопределяется. Это приводит к неожиданным побочным эффектам (которые мы рассмотрим в конце статьи). Чтобы избавиться от предупреждения, просто убираем модификатор open. И хотя у этого предупреждения та же причина, сценарий этот не совсем тот, о котором предупреждала Android Studio: здесь никак нельзя добавить final, так как отсутствие open уже подразумевает final по умолчанию!

Попробуем теперь кое-что другое:

interface BlogTopic {
    val someProperty: String
}

open class FinalBlog: BlogTopic {

    override val someProperty: String = "some"

    init {
        print(someProperty + "thing")
    }
}

Если свойство наследуется от интерфейса, то оно является open по умолчанию! Опять же, мы получим здесь предупреждение:

На этот раз после добавления модификатора final оно исчезнет:

open class FinalBlog: BlogTopic {

    final override val someProperty: String = "some"

init{
        print(someProperty + "thing")
    }
}

Вот мы и получили final val.

И сейчас вы наверняка скажете: «Но в исходном коде не было переопределенного val». Действительно, и к тому же класс не объявлялся с open!

Но здесь все просто объясняется. Когда я проверил класс, то вот что увидел:

@OpenForTesting
classFindPeopleToFollowViewModel

Это распространенный маркер, активирующий плагин компилятора Kotlin, чтобы открыть класс для тестирования и создания соответствующих заглушек. Но при открытии класса все поля становятся open независимо от того, входят они в тестируемую область или нет. Таким мне и предстал val, который фактически будет open, хотя я не писал его таковым.

Надеюсь, вам понравился этот небольшой экскурс с плагином компилятора. И кстати, когда вы будете использовать этот плагин, подумайте о том, чтобы задействовать mock maker inline из фреймворка Mockito. А еще лучше  —  постарайтесь использовать меньше заглушек, тогда он вам вообще не понадобится (в этом поможет TDD).

Результаты

Вернемся к примеру:

open class FinalBlog: BlogTopic {

    override val someProperty: String = "some"

init{
        print(someProperty + "thing")
    }
}

Теперь расширим этот класс:

class ChildBlog: FinalBlog() {
    override val someProperty = "another "
}

Как думаете, что будет выводиться после создания экземпляра этого класса: something или another thing?

На самом деле, будет выводиться nullthing!

Именно об этом сообщалось в предупреждении! Чтобы здесь разобраться, перейдем к байт-коду. Нажимаем на show Kotlin bytecode («Показать байт-код Kotlin»), а затем выбираем decompile («Декомпилировать») для чтения его в формате Java:

public class FinalBlog implements BlogTopic {
   @NotNull
   private final String someProperty = "some";

   @NotNull
   public String getSomeProperty() {
      return this.someProperty;
   }

Теперь свойство Kotlin здесь  —  это геттер с полем для содержимого.

То же самое для класса-потомка:

public final class ChildBlog extends FinalBlog {
   @NotNull
   private final String someProperty = "another ";

   @NotNull
   public String getSomeProperty() {
      return this.someProperty;
   }
}

Проблема в том, что мы получаем доступ к значению из конструктора родительского класса. А так как геттер переопределен, будет вызвана версия из класса-потомка. Но резервное поле не инициализировано, ведь мы все еще в вызове суперконструктора. Поэтому значение равно null (для объявленного поля, не допускающего значений null!).

Но стоит только реализовать класс без резервного поля, и все получится:

class ChildBlog: FinalBlog() {
    override val someProperty
        get() = "another "
}

Так что поведение здесь действительно непредсказуемо! Не обращайтесь к полям без final из конструктора.

И не упускайте из виду предупреждения, они будут вам в помощь!

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Danny Preussler: The Kotlin modifier that shouldn’t be there

Предыдущая статьяПрограммирование на квантовых компьютерах: какой язык учить?
Следующая статья4 типичные ошибки программиста, которые видны лишь с позиции руководителя