Большинство разработчиков 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 из конструктора.
И не упускайте из виду предупреждения, они будут вам в помощь!
Читайте также:
- Абстракции с нулевой стоимостью* в Kotlin
- REST API для приложения со Spring Boot, Kotlin и Gradle
- Корутины Kotlin: как работать асинхронно в Android
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Danny Preussler: The Kotlin modifier that shouldn’t be there