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



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

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


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



Интернет подтвердил это, поэтому я был очень удивлен, когда в 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 из конструктора.


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


943   0  

Comments

    Ничего не найдено.