Kotlin: классы и объекты

Eugene Saturov
5 min readApr 18, 2016

--

В Kotlin классы обозначаются ключевым словом class:

Объявление класса состоит из имени класса, заголовка класса (первичного конструктора, параметров и т.д.) и тела, заключённого в фигурные скобки. Наличие и заголовка, и тела — опционально. Если в классе отсутствует тело, фигурные скобки разрешается опустить.

Конструкторы

Класс в Kotlin может иметь один первичный конструктор и несколько вторичных конструкторов. Первичный конструктор является частью заголовка класса: он идёт сразу после имени класса.

Если первичный конструктор не имеет никаких аннотаций или модификаторов доступа, ключевое слово constructor разрешается опустить:

Первичный конструктор не может содержать код. Инициализационный код должен быть обёрнут в блок инициализации, обозначаемый ключевым словом init:

Обратите внимание, что в блоке инициализации допускается использование параметров первичного конструктора. Разумеется, эти параметры также можно использовать для инициализации переменных прямо в теле класса.

На самом деле, для объявления переменных и их инициализации параметрами первичного конструктора в Kotlin существует специальный краткий синтаксис:

Точно также, как и обычные переменные, переменные, объявляемые в первичном конструкторе могут быть изменяемыми (var) и неизменяемыми (val).

Если конструктор снабжён аннотациями или модификаторами доступа, ключевое слово constructor необходимо, а модификаторы и аннотации предшествуют ему:

Вторичные конструкторы

Класс может также содержать несколько вторичных конструкторов, объявление которых обязательно начинается с ключевого слова constructor:

Если класс содержит первичный конструктор, каждый вторичный конструктор должен быть делегирован к первичному явно или неявно, через другой вторичный конструктор. Делегирование к другому конструктору того же класса осуществляется через ключевое слово this:

Если в неабстрактном классе не будет объявлено ни одного конструктора (первичного или вторичного), для него будет сгенерирован первичный конструктор без аргументов. Этот сгенерированный конструктор будет иметь модификатор доступа public. Если вы не хотите, чтобы ваш класс имел публичный конструктор, вам придётся объявить пустой первичный конструктор с модификатором доступа private:

Важно: на JVM, если все параметры первичного конструктора имеют значения по умолчанию, компилятор автоматически сгенерирует дополнительный пустой конструктор, использующий стандартные значения параметров. Это облегчает использование Kotlin с библиотеками типа Jackson или JPA, для создания экземпляра класса которым требуется конструктор без параметров.

Создание экземпляров классов

Для создания экземпляра класса требуется вызвать конструктор так, будто это обычный метод.

Обратите внимание, что в Kotlin отсутствует ключевое слово new.

Члены класса

Классы могут содержать в себе:

  1. Конструкторы и блоки инициализации;
  2. Методы;
  3. Переменные;
  4. Вложенные классы;
  5. Объявление объекта.

Наследование

Все классы в Kotlin унаследованы от одного суперкласса Any. Это стандартный родительский класс для всех классов, которые явно не унаследованы от другого класса:

Any не является полным аналогом java.lang.Object. В частности, в классе Any нет ничего, кроме методов equals(), hashCode() и toString().

Для того, чтобы явно унаследовать один класс от другого, необходимо указать имя родительского класса через двоеточие в заголовке класа:

Если у класс есть первичный конструктор, базовый тип может (и должен) быть проинициализирован прямо в нём, с использованием параметров первичного конструктора.

Если же у класса отсутствует первичный конструктор, тогда каждый вторичный конструктор должен самостоятельно инициализировать базовый тип через ключевое слово super, или делегировать это другому вторичному конструктору. Обратите внимание, что в этом случае, разные вторичные конструкторы могут обращаться к разным конструкторам базового класса:

Аннотация open обратна по смыслу модификатору final из Java — она делает класс доступным для наследования. По умолчанию, все классы в Kotlin финализированные, т.е. закрыты для наследования.

Переопределение членов

Как вы уже поняли, некоторые вещи, будучи неявными в Java, в Kotlin стали явными. В отличие от Java, в Kotlin необходимо явно аннотировать члены класса, которые должны быть доступны для переопределения:

Для переопределения метода v() в классе Derived необходима аннотация override. Если аннотации не будет — компилятор будет на это ругаться. Если исходный метод базового класса не помечен аннотацией open, как метод nv() в классе Base, то объявлять метод с идентичной сигнатурой в дочернем методе запрещается, не имеет значения, будет он снабжён аннотацией override или нет. В финализированном классе (т.е. в классе не определённом как open) открывать члены для наследования не допускается.

Член класса, помеченный аннотацией override является открытым, т.е. может быть переопределён. Если вы хотите запретить повторное переопределение, используйте модификатор final:

Погодите! Как же теперь хакать сторонние библиотеки?

Да, действительно, с новой политикой переопределения (классы и его члены финализированы по умолчанию) возникают проблемы по переопределению библиотечных компонентов, в том случае, если они явно не открыты для переопределения разработчиком библиотеки.

Мы не считаем это недостатком по следующим причинам:

  1. Согласно лучшим практикам, внедрять свои хаки в библиотеки вообще не следует;
  2. Разработчики успешно используют другие языки (C++, C#), в которых используется аналогичный подход;
  3. Если хакнуть библиотеку вам всё же необходимо, вы всегда можете написать свой хак на Java и вызвать его из Kotlin.

Правила переопределения

В Kotlin наследование подчиняется следующему правилу: если класс наследует множество реализаций одного и того же компонента из своего непосредственного суперкласса, он должен переопределить этот компонент и снабдить его собственной реализацией (возможно, позаимствованной из одного из суперклассов). Для обозначения суперкласса, из которого наследуется реализация компонента, используется модификатор вида super<ИмяСуперкласса>, например super<Base>.

Kotlin позволяет аследоваться и от A, и от B. С методами a() и b() при этом не возникнет никаких проблем, так как класс C унаследует лишь по одной реализации каждого метода из всех своих суперклассов. Но так как оба суперкласса имеют свою реализацию метода f(), в классе C приходится принудительно переопределять метод с целью устранения коллизии.

Абстрактные классы

Класс и некоторые его члены могут быть помечены ключевым словом abstract. Абстрактный компонент не может иметь реализации в том классе, в котором он объявлен. Обратите внимание, что помечать абстрактный класс или другой компонент аннотацией open не требуется — он будет открытым по-умолчанию.

Также, мы можем переопределять не абстрактные открытые компоненты абстрактными.

Объекты-компаньоны

В отличие от Java и C#, в Kotlin отсутствуют статические методы. В большинстве случаев, рекомендуется использовать методы с пакетным уровнем доступа.

Если вам требуется реализовать метод, который можно будет вызывать без привязки к конкретному экземпляру класса, но имея доступ к компонентам класса (например, фабричный метод), вы можете реализовать его как объявление объекта внутри класса.

Ещё немного конкретики: если вы объявляете объект-компаньон в теле класса, вы можете обращаться к его компонентам, используя тот же синтаксис, что используется для вызова статических методов в Java/C#.

Запечатанные классы (Sealed classes)

Запечатанные классы используются для представления ограниченной иерархии классов, когда значение может быть одного типа из ограниченного набора, и никакого другого. Эти классы, в некотором смысле, являются логическим дополнением enum-классов: набор значений enum-типов также ограничен, но каждая из enum-констант существует только в виде единственного экземпляра, тогда как подклассы запечатанных классов могут иметь несколько экземпляров, хранящих своё состояние.

Чтобы пометить класс запечатанным, требуется поместить ключевое слово sealed перед заголовком класса. От запечатанных классов можно наследоваться, но все эти унаследованные классы должны быть вложенными в родительский запечатанный класс.

Обратите внимание, что классы, которые расширяют подклассы запечатанного класса могут располагаться где угодно, а не обязательно внутри запечатанного класса.

Основные сильные стороны запакованных классов выявляются в связке с выражениями when. Если вы уверены, что все ветки выражения покрывают все возможные сценарии, можно обойтись без ветки else.

--

--