Super kotlin что это
Классы
Kotlin пытается сократить количество кода при создании новых классов. В Kotlin решили реализовать другой подход к классам. Например, объявления по умолчанию получают модификаторы final и public. Вложенные классы по умолчанию не являются внутренними и другие особенности.
В Kotlin нет жёсткой привязки классов и файлов. Вы можете создать несколько классов в одном файле и выбрать любое имя для этого файла. При этом также не важно размещение файлов с классами на диске, можно даже не обращать внимание на имена пакетов и соблюдать точную иерархию вложенности. Но лучше придерживаться правил Java, особенно, если у вас уже большой опыт и вы привыкли к стандартному расположению.
Для класса используется ключевое слово class. С ним могут использоваться также ключевые слова: data, open, internal, abstract, public.
Начнём с самых основ. Класс представляет собой шаблон, определяющий свойства и функции, связанные с объектами этого типа. Android уже содержит множество готовых классов, но всё предусмотреть невозможно и вам придётся создавать собственные классы. Например, мы можем создать класс для кота. Класс!
Если ваше приложение предназначено для хранения информации о котах, вы можете определить класс Cat для создания ваших собственных объектов Cat. В таких объектах скорее всего вам понадобится хранить имя кота (name), его вес (weight) и порода (breed). Вы можете придумать свои варианты для ваших конкретных задач.
В большинстве случаев класс содержит свойства и функции. Хотя Kotlin позволяет создать пустой бессмысленный класс.
Создадим простейший класс для понимания.
В Kotlin ключевое слово public при создании класса не требуется, по умолчанию класс имеет данный модификатор и указывать его не нужно.
Вызываем класс в коде.
Никаких new. Одна строчка кода для создания класса, одна строчка для вызова.
То, что объект может сделать, — это его функции (в Java методы). Они определяют поведение объекта и могут использовать свойства объекта. Класс Cat может содержать функцию sleep().
Создадим полноценный класс.
Мы сейчас не только создали класс, но и задали ему три свойства, используя ключевые слова val и var (это важно).
Функции пишутся внутри фигурных скобок. Создадим функцию sleep(). Если это ещё маленький котёнок, то во сне он сопит, а солидный кот уже храпит.
Создаём объект Барсик, используя все свойства класса.
Доступ к любому свойству достигается через оператор . (точка):
Если свойство задано ключевым словом var, то мы можем не только прочитать свойство, но и установить новое значение. В нашем случае таким свойством является weight. Давайте накормим кота, чтобы увеличить его вес.
Если вы захотите изменить свойства с ключевым словом val, то у вас ничего не получится. Компилятор сообщит об ошибке (error: val cannot be reassigned). Попробуйте это сделать самостоятельно, чтобы увидеть своими глазами.
Функция вызывается также через оператор «точка», но не забывайте про круглые скобки.
Мы можем создать массив объектов из нашего класса.
Так легче различать, где свойство и где параметр.
Если вы привыкли использовать одно имя, то добавьте this.
Определение свойств в теле класса обеспечивает большую гибкость, чем простое добавление их в конструктор, так как в этом случае не нужно инициализировать каждое свойство значением параметра конструктора.
Предположим, вы хотите определить для нового свойства значение по умолчанию, не включая его в конструктор. Добавим в класс свойство activities и инициализируем его массивом, который по умолчанию содержит значение «Play».
Обратите внимание, что мы убрали из конструктора свойство breed (нет ключевого слова val) и заменили параметром breed_param. В теле класса создали свойство breed, которое принимает параметр конструктора и преобразовывает его.
Блок инициализации init
Указанный способ инициализации свойств хорошо работает, если вы хотите присвоить простое значение или выражение. А если понадобится сделать что-то более сложное? Или нужно выполнить дополнительный код, который сложно добавить в конструктор?
Здесь нам поможет блоки инициализации, которые выполняются при инициализации объекта сразу же после вызова конструктора и снабжаются префиксом init. Блоков init может быть несколько. Они выполняются в том порядке, в котором определяются в теле класса, чередуясь с инициализаторами свойств.
Фактически при создании свойства и его инициализации, вы назначаете свойствам значения по умолчанию. Например, для строковых свойств можно использовать пустую строку, для чисел 0 и т.п.
Конструкторы
Мы уже ранее сталкивались с конструктором. Познакомимся с ним поближе.
Когда вы определяете класс без конструктора, компилятор генерирует его за вас. Он добавляет пустой конструктор (конструктор без параметров) в откомпилированный код, который ничего не делает. Ваш код будет равносилен коду:
И создавать объект вам всё-равно придётся с использованием круглых скобок.
Если вы будете наследоваться от подобного класса, не создавая своих конструкторов, то следует явно вызвать конструктор суперкласса, несмотря на то, что он не имеет параметров.
Можно обойтись без фигурных скобок.
Подобный конструктор считается основным или первичным (primary) в Kotlin. Он объявляется вне тела класса. Дополнительные вторичные конструкторы объявляются уже в теле класса.
Предыдущий пример создания класс можно было переписать чуть длиннее.
Напомню, что знак подчёркивания используют, чтобы отличить параметр конструктора от свойства. Если вы привыкли использовать одно имя, то добавьте this.
Параметрам конструктора можно назначать значения по умолчанию, как в параметрах функций.
Тогда этот параметр можно не указывать при создании экземпляра класса.
Если класс имеет суперкласс, то основной конструктор должен инициализировать свойства от суперкласса.
Вторичные конструкторы
При создании дополнительных конструкторов используется ключевое слово constructor (впрочем для первичного конструктора тоже можно). Вторичный конструктор делегирует первичный конструктор с помощью ключевого слова this. Добавим в конструктор новое свойство email:
Если требуется несколько конструкторов, то можно объявить столько вторичных конструкторов, сколько вам требуется.
Геттеры/сеттеры
Для решения проблемы существуют get- и set-методы (иногда их называют геттеры и сеттеры). Get- и set-методы предназначены для чтения и записи значений свойств. Цель get-метода — вернуть значение, запрошенное для данного свойства. А set-методы получают значение аргумента и используют его для записи значения в свойство. Таким образом get- и set-методы позволяют защитить значения свойств и управлять тем, какие значения читаются или записываются в свойства.
В очередной раз переделаем класс. Добавим в класс новое свойство weightInGramms и напишем для него пользовательский get-метод, который будет возвращать соответствующее значение.
Сразу после объявления свойства мы создаём get-метод, который представляет собой функцию без параметров. Тип возвращаемого значения должен совпадать с типом свойства, значение которого должен возвращать get-метод, в противном случае код не будет компилироваться.
Проверим, как работает свойство.
Дополним свойство weight пользовательским set-методом, чтобы свойство могло обновляться только значениями больше 0. Для этого необходимо переместить определение свойства weight из конструктора в тело класса, а затем добавить set-метод к свойству.
Set-метод представляет собой функцию с именем set, которая записывается под объявлением свойства. Set-метод имеет один параметр (обычно с именем value), который содержит новое значение свойства. В нашем случае значение свойства weight обновляется только в том случае, если значение параметра value больше 0. Если попытаться обновить свойство weight значением, меньшим либо равным 0, set-метод проигнорирует обновление свойства. Внимание! Для обновления свойства weight set-метод использует идентификатор field, обозначающий поле данных для свойства. Очень важно использовать именно field вместо имени свойства, потому что так вы предотвращаете зацикливание. Просто поверьте на слово.
Мы создавали get- и set- методы вручную, чтобы контролировать данные для свойства. Но вдобавок компилятор незаметно генерирует get- и set-методы для всех свойств, у которых пользовательских методов нет. Если свойство определяется с ключевым словом val, то компилятор добавляет get-метод, а если свойство определяется с ключевым словом var, то компилятор добавляет и get-, и set-метод.
Рассмотрим примеры. По ключевому слову val можно догадаться, что у name есть только геттер, так как этот тип переменной отвечает за неизменяемые данные. У isMale есть и геттер и сеттер.
Вызываем экземпляр класса, присваиваем коту имя и пол. Результат выводим на экран.
Если вы хотите открыть доступ к свойству для чтения и закрыть для записи, то объявите область видимости записи приватным.
Теперь изменять свойство может только сам экземпляр класса. А предыдущий пример не станет работать. Это полезно, если требуется запретить изменение свойства другими частями вашего приложения.
Вызываем объект класса и проверяем.
Мы получили вычисляемое свойство, у которого значение меняется. У него нет начального значения, значения по умолчанию и нет поля, которое могло бы хранить значение.
Импорт Java-классов
Можно импортировать стандартный Java-класс и вызывать его методы.
Получить имя класса
Вместо стандартного метода Java getClass() можно вызвать javaClass.
Иногда в логах используют имя текущего класса. Проверим на стандартной активности
В Kotlin есть новое ключевое слово internal, применимое к пакетам. Позволяет указать, что классы доступны внутри модуля.
Переопределяем метод toString()
Можно переопределять метод toString():
Расширение классов
Можно расширять классы не изменяя код самого класса при помощи функций-расширений. Добавим функцию isBlack() к классу Cat вне самого класса:
Таким удобным образом вы можете даже расширить классы, которые не принадлежат к коду вашего проекта.
Абстрактные классы
Как и в Java, класс можно объявить абстрактным, добавив ключевое слово abstract. Создать экземпляр такого класса нельзя. Абстрактные методы всегда открыты, поэтому использование модификатора open необязательно.
Абстрактный класс может содержать абстрактные свойства и функции. Абстрактный класс также может содержать и неабстрактные свойства и функции. А ещё абстрактный класс может не содержать ни одного абстрактного свойства или функции. Если класс содержит какие-либо свойства и функции, помеченные как абстрактные, весь класс должен быть абстрактным.
Свойства в абстрактных свойствах нельзя инициализировать.
Абстрактные функции не могут иметь блок с фигурными скобками.
Расширяемся от абстрактного класса при помощи двоеточия, как и с с суперклассом.
В абстрактных подклассах у вас есть выбор: либо реализовать абстрактные свойства и функции, либо передать эстафету их подклассам.
Модификаторы видимости
По умолчанию класс имеет модификатор public, который можно не указывать. Члены класса и объявления верхнего уровня доступны повсюду.
Члены класса с модификатором protected доступны в подклассах.
Члены класса с модификатором private доступны в самом классе, а объявления верхнего уровня с этим модификатором доступны в файле.
Внутренние (inner) и вложенные классы
Можно объявлять один класс внутри другого. Но в Kotlin вложенные классы не имеют доступа к экземпляру внешнего класса, если не запросить его явно.
В Kotlin вложенный класс без модификаторов является аналогом статического вложенного класса в Java (фактически независимый класс).
Чтобы создать внутренний класс со ссылкой на внешний класс, нужно добавить модификатор inner.
Класс Any является предком (суперклассом) всех классов. Каждый класс, который вы определяете, является подклассом Any, и вам не нужно указывать на это в программе. Когда вы создаёте класс следующим образом:
Компилятор незаметно для вас преобразует ваш в класс в подкласс Any.
Подобный подход гарантирует, что каждый класс наследует общее поведение. В частности, любой класс будет содержать функцию equals().
Также вы можете создать функцию с параметрами Any или возвращаемый тип Any, который будет работать с объектами любых типов. Например, можно создать массив для хранения объектов любого типа:
Класс содержит несколько функций, наследуемых каждым классом.
Вы можете переопределить реализации всех этих функций, чтобы изменить поведение по умолчанию.
Не забывайте, что переменная с типом Any не может хранить значения null. Если вам нужна переменная, которая должна хранить объекты любых типов и null, то используйте Any?.
Kotlin: копаем глубже. Конструкторы и инициализаторы
В уже далёком мае 2017 года Google объявила о том, что Kotlin стал официальным языком для разработки под Android. Кто-то тогда впервые услышал название этого языка, кто-то на нём уже продолжительное время писал, но с того момента стало понятно, что все, кто близок к Android-разработке, теперь просто обязаны познакомиться с ним. Далее последовали как восторженные отклики «Наконец-то!», так и жуткое негодование «Зачем нам нам новый язык? Чем Java не угодила?» и т.д. и т.п.
С тех пор прошло достаточно времени, и хоть споры о том, хороший Kotlin или плохой, до сих пор не утихли, всё больше кода под Android пишется именно на нём. И даже вполне консервативные разработчики тоже переходят на него. Кроме того, в сети можно наткнуться на информацию, что скорость разработки после освоения этого языка увеличивается на 30% по сравнению с Java.
Сегодня Kotlin уже успел вылечиться от нескольких детских болезней, оброс большим количеством вопросов и ответов на Stack Overflow. Невооружённым взглядом стали видны как его плюсы, так и слабые места.
И вот на этой волне мне пришла в голову идея подробно разобрать отдельные элементы молодого, но популярного языка. Обратить внимание на сложные моменты и сравнить их с Java для наглядности и лучшего понимания. Разобраться в вопросе несколько глубже, чем это можно сделать, прочитав документацию. Если эта статья вызовет интерес, то, скорее всего, она положит начало целому циклу статей. А пока начну с довольно базовых вещей, которые, тем не менее, скрывают массу подводных камней. Поговорим о конструкторах и инициализаторах в Kotlin.
Как и в Java, в Kotlin создание новых объектов 一 сущностей определённого типа 一 происходит при помощи вызова конструктора класса. В конструктор также можно передавать аргументы, а конструкторов может быть несколько. Если смотреть на этот процесс как бы снаружи, то здесь единственное отличие от Java — отсутствие ключевого слова new при вызове конструктора. Теперь заглянем глубже и разберёмся, что же происходит внутри класса.
У класса могут быть первичный (primary) и дополнительные (secondary) конструкторы.
Конструктор объявляется с помощью ключевого слова constructor. В случае если у первичного конструктора нет модификаторов доступа и аннотаций, ключевое слово можно опустить.
У класса может не быть конструкторов, объявленных явно. В этом случае после объявления класса нет никаких конструкций, мы сразу переходим к телу класса. Если проводить аналогию с Java, то это равнозначно отсутствию явного объявления конструкторов, в результате чего конструктор по умолчанию (без параметров) будет сгенерирован автоматически на этапе компиляции. Выглядит это, ожидаемо, так:
Это равносильно такой записи:
Но если вы напишите именно так, то вам будет вежливо предложено удалить первичный конструктор без параметров.
Первичный конструктор — тот, который всегда вызывается при создании объекта в случае, если он есть. Пока примем это к сведению, а подробнее разберём позже, когда перейдём к вторичным конструкторам. Соответственно, помним, что если конструкторов нет совсем, то на самом деле один (первичный) есть, но мы его не видим.
Главная особенность первичного конструктора в том, что он не имеет тела, т.е. не может содержать в себе исполняемого кода. Он просто принимает в себя параметры и передаёт их вглубь класса для дальнейшего использования. На уровне синтаксиса это выглядит так:
Параметры, переданные таким образом, могут быть использованы для различных инициализаций, но не более. В чистом виде эти аргументы в рабочем коде класса мы использовать не можем. Однако мы можем проинициализировать поля класса прямо здесь. Это выглядит таким образом:
Здесь param1 и param2 можно использовать в коде в качестве полей класса, что равносильно следующему:
Ну и если сравнивать с Java, то это выглядело бы вот так (и кстати, на этом примере можно оценить, как сильно Kotlin может сократить количество кода):
Т.е. дополнительный конструктор как бы является наследником первичного.
Теперь, если мы создадим объект вызовом дополнительного конструктора, произойдёт следующее:
вызов дополнительного конструктора;
вызов основного конструктора;
инициализация поля класса p1 в основном конструкторе;
выполнение кода в теле дополнительного конструктора.
Это похоже на такую конструкцию в Java:
Вспомним, что в Java мы можем вызвать один конструктор из другого с помощью ключевого слова this только в начале тела конструктора. В Kotlin этот вопрос решили кардинально — сделали такой вызов частью сигнатуры конструктора. На всякий случай отмечу, что вызывать любой (первичный или дополнительный) конструктор прямо из тела дополнительного запрещено.
Дополнительный конструктор всегда должен ссылаться на главный (при его наличии), но может делать это косвенно, ссылаясь на другой дополнительный конструктор. Суть в том, чтобы в конце цепочки мы всё же добрались до главного. Срабатывание конструкторов, очевидно, будет происходить в порядке, обратном обращению конструкторов друг к другу:
Теперь последовательность такая:
Бывает так, что класс не имеет первичного конструктора, при этом может иметь один или несколько дополнительных. Тогда дополнительные конструкторы не обязаны ссылаться на кого-то, но при этом могут ссылаться на другие дополнительные конструкторы этого класса. Ранее мы выяснили, что основной конструктор, не указанный явно, генерируется автоматически, но это касается случаев, когда в классе нет вообще никаких конструкторов. Если есть хотя бы один дополнительный конструктор, первичный конструктор без параметров не создаётся:
Можем создать объект класса вызовом:
Можем создать объект только таким вызовом:
В этом плане в Kotlin по сравнению с Java ничего нового нет.
Кстати, как и первичный конструктор, дополнительный может и не иметь тела в том случае, если его задачей является лишь передача параметров в другие конструкторы.
Также стоит обратить внимание на то, что в отличие от первичного конструктора, инициализация полей класса в списке аргументов дополнительного конструктора запрещена.
Т.е. такая запись будет невалидной:
Отдельно стоит отметить, что дополнительный конструктор, как и первичный, вполне может быть без параметров:
Говоря про конструкторы, нельзя не упомянуть одну из удобных фич Kotlin — возможности назначать для аргументов значения по умолчанию.
Теперь допустим, что мы имеем класс с несколькими конструкторами, имеющими разное количество аргументов. Приведу пример на Java:
Как показывает практика, подобные конструкции встречаются довольно часто. Давайте посмотрим, как то же самое можно написать на Kotlin:
Теперь давайте дружно похлопаем Kotlin за то, как сильно он сократил код. Кстати, кроме уменьшения количества строк мы получаем больше порядка. Вспомните, наверняка вы не раз видели что-то такое:
Когда видишь подобное, хочется найти человека, который это написал, взять за пуговицу, подвести к экрану и грустным голосом спросить: «Зачем?»
Хотя можно повторить такой подвиг и на Kotlin, но не надо.
Есть, правда, одна деталь, которую в случае такой сокращённой записи на Kotlin необходимо учесть: если мы хотим вызывать конструктор со значениями по умолчанию из Java, то мы должны добавить к нему аннотацию @JvmOverloads :
В противном случае мы получим ошибку.
Теперь поговорим про инициализаторы.
В Java также есть инициализирующие блоки, но это не одно и то же. В них мы не можем, как в Kotlin, передать значение извне (аргументы первичного конструктора). Инициализатор очень похож на тело первичного конструктора, вынесенного в отдельный блок. Но это с первого взгляда. На самом деле это не совсем так. Давайте разбираться.
Приведу несколько интересных случаев работы с инициализаторами.
Здесь мы видим, что первое обращение к полю в ходе инициализации (в блоке init или вне его) равносильно обычной его инициализации в Java. Все остальные действия, связанные с присвоением значения в процессе инициализации, кроме первого (первое присвоение значение объединяется с объявлением поля), выносятся в конструктор.
Если проводить эксперименты с декомпиляцией, то выясняется, что если конструктора нет, то генерируется первичный конструктор, и вся магия происходит в нём. Если есть несколько дополнительных конструкторов, не ссылающихся друг на друга, и при этом нет первичного, то в Java-коде этого класса все последующие присвоения значения полю testParam дублируются во всех дополнительных конструкторах. Если же первичный конструктор при этом есть, то только в первичном. Фуф…
И на закуску самое интересное: поменяем сигнатуру testParam с var на val :
А где-то в коде вызовем:
Всё скомпилировалось без ошибок, запустилось, и вот мы видим вывод логов:
in showTestParam testParam = some string
in constructor testParam = after
Делая выводы из приведённых выше случаев, можно только посоветовать не плодить блоки инициализации и не разбрасывать их по классу, избегать повторных присвоений значений в процессе инициализации, вызывать из init-блоков только чистые функции. Всё это делается во избежании возможной путаницы.
Итак. Инициализаторы — это некий блок кода, обязательно выполняемый при создании объекта независимо от того, с помощью какого конструктора этот объект создаётся.
Вроде разобрались. Рассмотрим взаимодействие конструкторов и инициализаторов. В рамках одного класса всё довольно просто, но надо запомнить:
Стоит отметить, что как Object является базовым для всех классов в Java, так Any является таковым в Kotlin. При этом Any и Object 一 это не одно и то же.
Для начала о том, как происходит наследование. Класс-наследник, как и родительский класс, может иметь или не иметь первичного конструктор, но при этом должен ссылаться на определённый конструктор родительского класса.
Если класс-наследник имеет первичный конструктор, то этот конструктор должен указывать на конкретный конструктор базового класса. При этом все дополнительные конструкторы класса-наследника должны ссылаться на основной конструктор своего класса.
Также не забываем про возможность косвенного вызова конструктора родительского класса через другие конструкторы класса-наследника:
Если же класс-наследник не имеет никаких конструкторов, то просто добавляем вызов конструктора родительского класса после имени класса-наследника:
При этом всё же есть вариант с наследованием, при котором ссылка на конструктор родительского класса не требуется. Такая запись является валидной:
Но только в том случае, если у родительского класса есть конструктор без параметров, который является конструктором по умолчанию (первичный или дополнительный — не важно).
Теперь рассмотрим порядок вызова инициализаторов и конструкторов при наследовании:
На этом этапе будем считать, что конструкторы и инициализаторы мы изучили довольно глубоко и теперь знаем про них почти всё. Немножко отдохнём и будем копать в другую сторону!
Inheritance
By default, Kotlin classes are final – they can’t be inherited. To make a class inheritable, mark it with the open keyword:
To declare an explicit supertype, place the type after a colon in the class header:
If the derived class has a primary constructor, the base class can (and must) be initialized in that primary constructor according to its parameters.
If the derived class has no primary constructor, then each secondary constructor has to initialize the base type using the super keyword or it has to delegate to another constructor which does. Note that in this case different secondary constructors can call different constructors of the base type:
Overriding methods
Kotlin requires explicit modifiers for overridable members and overrides:
A member marked override is itself open, so it may be overridden in subclasses. If you want to prohibit re-overriding, use final :
Overriding properties
You can also override a val property with a var property, but not vice versa. This is allowed because a val property essentially declares a get method, and overriding it as a var additionally declares a set method in the derived class.
Note that you can use the override keyword as part of the property declaration in a primary constructor:
Derived class initialization order
During the construction of a new instance of a derived class, the base class initialization is done as the first step (preceded only by evaluation of the arguments for the base class constructor), which means that it happens before the initialization logic of the derived class is run.
This means that when the base class constructor is executed, the properties declared or overridden in the derived class have not yet been initialized. Using any of those properties in the base class initialization logic (either directly or indirectly through another overridden open member implementation) may lead to incorrect behavior or a runtime failure. When designing a base class, you should therefore avoid using open members in the constructors, property initializers, or init blocks.
Calling the superclass implementation
Code in a derived class can call its superclass functions and property accessor implementations using the super keyword:
Inside an inner class, accessing the superclass of the outer class is done using the super keyword qualified with the outer class name: super@Outer :
Overriding rules
In Kotlin, implementation inheritance is regulated by the following rule: if a class inherits multiple implementations of the same member from its immediate superclasses, it must override this member and provide its own implementation (perhaps, using one of the inherited ones).
To denote the supertype from which the inherited implementation is taken, use super qualified by the supertype name in angle brackets, such as super :
.draw() // call to Polygon.draw() > >