Источник: Nuances of Programming
Большинство разработчиков Kotlin уверены в том, что свойство val здесь эквивалентно использующемуся в Java свойству final. А что, если я скажу, что это не совсем так? Ведь иногда бывает необходимо задействовать final val?
В отличие от Java, свойства Kotlin являются final по умолчанию. В противном случае они явно помечаются ключевым словом open! То есть выходит, что в ключевом слове final нет необходимости? Заглянем в поисковик:
Интернет подтвердил это, поэтому я был очень удивлен, когда в Android Studio мне было предложено добавить final к val:
И действительно, после добавления final проблема была решена:
Таким образом, ключевое слово final со свойствами все-таки используется, но почему и когда нужно добавлять к val final?
Рассмотрим это поведение на простом примере:
Здесь все работает так, как и ожидалось, и код выведет something при создании экземпляра класса.
Теперь добавим везде open:
После чего снова возникнет то же самое предупреждение, что уже появлялось в Android Studio выше:
На самом деле здесь все более чем очевидно. Для класса создаются подклассы, и свойство переопределяется. Это приводит к неожиданным побочным эффектам (которые мы рассмотрим в конце статьи). Чтобы избавиться от предупреждения, просто убираем модификатор open. И хотя у этого предупреждения та же причина, сценарий этот не совсем тот, о котором предупреждала Android Studio: здесь никак нельзя добавить final, так как отсутствие open уже подразумевает final по умолчанию!
Попробуем теперь кое-что другое:
Если свойство наследуется от интерфейса, то оно является open по умолчанию! Опять же, мы получим здесь предупреждение:
На этот раз после добавления модификатора final оно исчезнет:
Вот мы и получили final val.
И сейчас вы наверняка скажете: «Но в исходном коде не было переопределенного val». Действительно, и к тому же класс не объявлялся с open!
Но здесь все просто объясняется. Когда я проверил класс, то вот что увидел:
@OpenForTesting
class FindPeopleToFollowViewModel
Это распространенный маркер, активирующий плагин компилятора Kotlin, чтобы открыть класс для тестирования и создания соответствующих заглушек. Но при открытии класса все поля становятся open независимо от того, входят они в тестируемую область или нет. Таким мне и предстал val, который фактически будет open, хотя я не писал его таковым.
Надеюсь, вам понравился этот небольшой экскурс с плагином компилятора. И кстати, когда вы будете использовать этот плагин, подумайте о том, чтобы задействовать mock maker inline из фреймворка Mockito. А еще лучше — постарайтесь использовать меньше заглушек, тогда он вам вообще не понадобится (в этом поможет TDD).
Результаты
Вернемся к примеру:
Теперь расширим этот класс:
class ChildBlog: FinalBlog() {
override val someProperty = "another "
}
Как думаете, что будет выводиться после создания экземпляра этого класса: something или another thing?
На самом деле, будет выводиться nullthing!
Именно об этом сообщалось в предупреждении! Чтобы здесь разобраться, перейдем к байт-коду. Нажимаем на show Kotlin bytecode («Показать байт-код Kotlin»), а затем выбираем decompile («Декомпилировать») для чтения его в формате Java:
Теперь свойство Kotlin здесь — это геттер с полем для содержимого.
То же самое для класса-потомка:
Проблема в том, что мы получаем доступ к значению из конструктора родительского класса. А так как геттер переопределен, будет вызвана версия из класса-потомка. Но резервное поле не инициализировано, ведь мы все еще в вызове суперконструктора. Поэтому значение равно null (для объявленного поля, не допускающего значений null!).
Но стоит только реализовать класс без резервного поля, и все получится:
class ChildBlog: FinalBlog() {
override val someProperty
get() = "another "
}
Так что поведение здесь действительно непредсказуемо! Не обращайтесь к полям без final из конструктора.
И не упускайте из виду предупреждения, они будут вам в помощь!
Читайте также:
Перевод статьи Danny Preussler: The Kotlin modifier that shouldn’t be there