Kotlin - dmt7531/FAQ GitHub Wiki

Полезные ссылки:

1. Ключевые слова и операторы Kotlin

2. Темы по Kotlin: hr-vector.com

Список тем и вопросов:

Kotlin и Java

Анонимные классы и объекты, object и companion object

Null safety

Класс Any (как Object в Java)

Тип Unit (как void в Java)

Тип Nothing (нет аналога в Java)

Модификаторы доступа. Свойства. Делегаты. Конструкторы. Init блок.

Data класс

Классы и интерфейсы

  • Что такое абстрактные классы и интерфейсы?
  • Почему классы в Kotlin по умолчанию final?
  • Что нужно сделать, чтобы класс можно было наследовать? (open)
  • Как можно получить тип класса?
  • Что такое enum класс (перечислений)?
  • Что такое sealed класс (изолированный)?
  • Какая разница между sealed class и enum?
  • Что такое inner (внутренние) и nested (вложенные) классы?
  • Value (бывшие inline) классы.
  • Что такое деструктурирующее объявление? Что нужно сделать, чтобы иметь возможность использовать его для своего класса? Какие проблемы могут возникнуть с таким объявлением?
  • Какая польза от typealias? Во что он компилируется?

Коллекции и последовательности (Sequences)

  • Какие коллекции есть в Kotlin?
  • List
  • Set
  • Map
  • Sequences

Преимущества языка Kotlin перед Java ↑↑↑

Код на Kotlin компактнее на 30-40%
Меньше кода = меньше ошибок, выше скорость разработки.
Безопасная работа с обнуляемыми переменными (Null Safety)
В отличие от Java, в Kotlin по умолчанию все типы являются non-nullable, то есть не могут принимать значение null. Присвоение или возврат null приведет к ошибке компиляции. Чтобы присвоить переменной значение null, в Kotlin необходимо явно пометить эту переменную как nullable (добавив после типа знак вопроса). В Java же при использовании ссылки на объект с указанным значением null, появляется исключение в виде «NullPointerExpertion!».
Функции-расширения (Extensions)
Kotlin позволяет расширять класс путём добавления нового функционала без необходимости наследования от такого класса. Это реализовано с помощью специальных выражений, называемых расширения. Например, вы можете написать новые функции для класса из сторонней библиотеки, которую вы не можете изменить. Такие функции можно вызывать обычным способом, как если бы они были методами исходного класса. Этот механизм называется функцией расширения.
Классы данных (data classes)
Разработчику на Java приходится писать много стандартного, но часто встречающегося кода (т.н. шаблонный код или boilerplate). В Kotlin же есть возможность создания специальных классов для определения полей для хранения данных, конструктора, функций сеттеров и геттеров для каждого поля, и функций Hashcode(), toString() и equals(). Для этого достаточно добавить data в определение класса, затем компилятор сделает все сам.
Функции высшего порядка и Лямбда-выражения
Функция высшего порядка (higher-order function) — функция, которая может принимать в качестве параметра функцию или лямбда-выражение, или же может возвращать функцию. Лямбда-выражения — это функция, которая способна передаваться как выражение, хоть и не является объявленной. Лямбда-выражения + inline-функции.
Синглтоны на уровне языка (Object)
В Java все должно объявляться внутри класса. Но в Kotlin все иначе. Компоненты могут объявляться за пределами класса, и это автоматически делает их статическими. Поэтому нам не требуется ключевое слово static. В Java статические члены обрабатываются не так, как члены-объекты. Это означает, что для статических членов нам недоступны такие вещи, как реализация интерфейса, помещение экземпляра в ассоциативный список (map) или передача его в качестве параметра методу, который принимает объект. В Kotlin static не является ключевым словом и вместо статических членов используются объекты-компаньоны, позволяющие преодолеть вышеуказанные ограничения. В этом и заключается преимущество. Даже если члены объектов-компаньонов выглядят как статические члены в других языках, во время выполнения они все равно остаются членами экземпляров реальных объектов и могут, например, реализовывать интерфейсы. Синглтоны на уровне языка.
Корутины
Kotlin предоставляет возможность создавать дополнительные потоки, однако в нем также существуют т.н. корутины и сопрограммы, которые позволяют использовать меньше памяти в сравнении с обычным потоком т.к. реализованы они без стека. Корутины же в свою очередь способны выполнять интенсивные и длительные задачи методом приостановления выполнения без блокировки потока и его последующего восстановления. Что в дальнейшем позволяет сгенерировать асинхронный код без блокирования, который при его выполнении не отличить от синхронного. К тому же, они генерируют эффектные доп. стили например async или await.
Зачем это нужно? Все просто. Процессы, которые проводят длительные операции сильно загружают процессор, вследствие чего вызывающий поток вплоть до полного завершения операции — блокируется. Android является однопоточным, из-за чего в подобных ситуациях блокируется основной поток, не давая возможности использовать пользовательский интерфейс. Также это помогает сократить размер кода. Java же решает эту проблему немного иначе, создавая фоновый поток основного интенсивного потока. Но наличие нескольких потоков и управление ими будет непростым и приводит к ошибкам в коде.
Отсутствуют проверяемые (checked) исключения
Такие исключения, как IOException и FileNotFoundException, присутствуют в Java, но не поддерживаются в Kotlin. На практике такой вид исключений приводит к написанию try/catch без их должной обработки.

Дополнительно:

Умные приведения

Строковые шаблоны

Делегирование на уровне языка

Выведение типа для переменных и свойств

Более гибкая работа с generics

Разница между Exception в Java и Kotlin ↑↑↑

С точки зрения исключений компилятор Kotlin отличается тем, что не различает checked и unchecked исключения.
Все исключения — только unchecked, поэтому нет необходимости отлавливать или объявлять какие-либо исключения (вы самостоятельно принимаете решение, стоит ли их отлавливать и обрабатывать).

Если разработчик, работающий на Java, считает, что использование try / catch в коде раздражает, то упущение, сделанное Kotlin, можно считать желанным изменением. Однако противоположностью будет, если разработчик считает, что проверяемые исключения нужны, способствуя восстановлению после ошибок и созданию надежного кода. В этом случае это можно считать для Kotlin плюсом и минусом, в зависимости от подхода к разработке.

Подробнее: code.tutsplus.com, habr.com

Как перенести статичные методы из Java в Kotlin? ↑↑↑

В Kotlin нет статических методов, для этих целей обычно служит companion object.
Для того чтобы метод из Java был представлен как статический используется аннотация @JvmStatic.

В какой модификатор преобразуется internal в Java?? ↑↑↑

В Java нет эквивалента модификатору доступа internal из Kotlin. При компиляции Kotlin-кода в Java-байткод, модификатор доступа internal преобразуется в модификатор public в Java. Таким образом, все члены класса, отмеченные как internal, будут видны из любого места в том же пакете, а также из любого другого модуля, которому был разрешен доступ к этому модулю. Члены internal классов проходят через искажение имен, чтобы усложнить случайное использование их из Java и позволить перегрузку для членов с одинаковыми сигнатурами, которые не видят друг друга в соответствии с правилами Kotlin.

Подробнее: 4comprehension.com, kotlinlang.ru

Отличия в проверке на равенство == и equals() ↑↑↑

1. Проверка на равенство в Java

Структурное равенство (значение) — метод equals()

Ссылочное равенство — оператор ==:

  • примитивные типы данных: сравнивает значения переменных
  • ссылочные типы данных (объекты, массивы): сравнивает ссылки

2. Проверка на равенство в Kotlin

Структурное равенство (значение) — оператор == (проверка через equals())

Ссылочное равенство — оператор ===:

  • примитивные типы данных: сравнивает значения переменных
  • ссылочные типы данных (объекты, массивы): сравнивает ссылки

3. Разница == с Java

Структурное равенство:
Оператор == в Kotlin это equals() в Java, т.е. в Kotlin строки можно всегда сравнивать через ==.

Ссылочное равенство:
Оператор === в Kotlin это == в Java.

Подробнее: kotlinlang.ru, baeldung.com, habr.com

Чем дженерики в Kotlin отличаются от дженериков Java? ↑↑↑

Generics — это механизм языка, позволяющий писать обобщенные алгоритмы, возлагая на компилятор ответственность за проверку типов и их приведение. Generics позволяют нам писать шаблонный код, т.е. код, в большинстве случаев, не зависящий от типа.

Дженерики в Kotlin и Java обладают некоторыми общими чертами, но также есть и некоторые отличия:

  1. В Kotlin можно определять переменные типа с помощью оператора out, который гарантирует, что тип будет использоваться только для вывода значений. Например, если определить интерфейс List<out T>, то это означает, что список может содержать элементы типа T или его подклассов. Такой подход называется variance и позволяет более точно определять типы в дженериках.

  2. В Kotlin отсутствует оператор extends, который используется в Java для указания наследования класса или интерфейса. Вместо этого используется оператор : для указания наследования класса или интерфейса в дженериках.

  3. В Kotlin есть возможность определять функции-расширения для классов, в том числе и дженериков. Такие функции могут использоваться для добавления новой функциональности к существующим классам.

  4. В Kotlin есть возможность использовать типы-функции в качестве параметров и возвращаемых значений дженериков. Например, можно определить дженерик fun <T> List<T>.filter(predicate: (T) -> Boolean): List<T>, который принимает функцию-предикат для фильтрации списка.

Использование дженериков в Kotlin позволяет писать более безопасный и читабельный код, а также упрощает работу с коллекциями и другими типами данных. При этом, как и в Java, следует помнить о возможных проблемах, связанных с обобщениями, такими как возможность появления ошибок времени выполнения и некоторые проблемы с производительностью.

Кратко про анонимные классы и объекты, object и companion object ↑↑↑

Анонимные классы — это классы, которые явно не объявлены с помощью class, наследуется от заданного класса или реализует заданный интерфейс. Анонимный класс не всегда является синглтоном. Анонимный класс создается каждый раз при вызове соответствующего конструктора и используется только в контексте, где был создан. При этом каждый экземпляр анонимного класса имеет свое уникальное состояние и может отличаться от других экземпляров того же анонимного класса. В Kotlin анонимный класс создается следующим образом:

val obj = object : SuperClassOrInterface() {
    // implementation here
}

Объекты анонимных классов полезны для одноразового использования.

Экземпляры анонимных классов называют анонимными объектами, потому что они объявляются выражением, а не именем. Анонимный объект начинается с ключевого слова object.

  • можно задавать свойства, функции, блоки инициализации

  • можно наследоваться от других классов и реализовывать интерфейсы

  • нельзя создавать конструкторы (как основные, так и вторичные)

Ключевое слово object позволяет одновременно объявить класс и создать его экземпляр (т.е. объект).
При этом применять его можно по-разному:

  • object Name — это объявление объекта (оbject declaration), реализация паттерна Singleton

  • companion object — это объект-компаньон внутри класса (также Singleton)

  • object — это объект-выражение (анонимный объект/object expression), не Singleton

Объявление объекта (object declaration), object как Singleton ↑↑↑

Объявляется объект при помощи ключевого слова object, после которого следует имя объекта.

Файл, содержащий только object представляет из себя Singleton, т.е. будет создан только один экземпляр этого класса. Пример:

object One {
	val cats = arrayListOf<Cat>()
	
	fun callCat() {
		for (cat in cats) {
			...
		}
	}
}

Можно обращаться к методам и свойствам класса через имя объекта:

One.cats.add(Cat(...))
One.callCat()

Инициализация объявления объекта потокобезопасна и выполняется при первом доступе (лениво).

Сompanion object (также Singleton) ↑↑↑

Объекты можно объявлять внутри класса, при этом нет каких-либо ограничений по их количеству.
Но лишь один объект можно пометить ключевым словом companion object в рамках одного класса.

Синглтон-свойство companion object достигается за счет того, что он создается внутри класса в качестве статического поля и инициализируется один раз при первом обращении к нему.

Такому объекту можно не указывать свое имя, и обращаться к методам и свойствам объекта через имя содержащего его класса без явного указания имени объекта.

class SomeClass {

  companion object {
    fun create()
  }
}

val someClass = SomeClass.create()

Компилируется в public static final class на Java. Работает подобно ключевому слову static в Java.

Объект-выражение (анонимный объект/object expression) ↑↑↑

Объект-выражение — это выражение, которое "на ходу" создает анонимный объект.

Для объекта-выражения не указывается имя!
Если же объекту всё-таки требуется имя, то его можно сохранить в переменной:

val tom = object {
        val name = "Tom"
        var age = 37
        fun sayHello() {
            println("Hi, my name is $name")
        }
    }
    println("Name: ${tom.name}  Age: ${tom.age}")
    tom.sayHello()

Анонимные объекты не являются синглтонами!
Каждый раз при выполнении объекта-выражения создаётся новый объект.

Анонимный объект является заменой анонимным внутренним классам в Java.

Разница между анонимным и декларируемым (объявляемым) объектом ↑↑↑

  • анонимный объект (object) инициализируется непосредственно при использовании

  • декларированный (объявляемый) объект (object Name) инициализируется лениво, в момент первого к нему доступа

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

Аннотация @JvmStatic ↑↑↑

С помощью аннотации @JvmStatic есть возможность объявить методы по настоящему статическими, ее можно добавить как к методам object, так и к методам companion object.

object ObjectWithStatic {
    @JvmStatic
    fun staticFun(): Int {
        return 5
    }
}

В этом случае метод staticFun будет действительно объявлен статическим:

public final class ObjectWithStatic {
   public static final ObjectWithStatic INSTANCE;
 
   @JvmStatic
   public static final int staticFun() {
      return 5;
   }
 
   private ObjectWithStatic() {
      INSTANCE = (ObjectWithStatic)this;
   }
 
   static {
      new ObjectWithStatic();
   }
}

Что такое Null safety, nullable и non-nullable типы? ↑↑↑

Null safety — это концепция безопасности, которая предотвращает некоторые из наиболее распространенных ошибок в программировании, связанных с использованием null-значений. В Kotlin эта концепция реализуется за счет строгой типизации и системы Nullable/Non-nullable типов данных.

Nullable типы — это типы, которые могут содержать значение null. Non-nullable типы — это типы, которые не могут содержать значение null и всегда должны иметь некоторое значение.

В Kotlin переменные по умолчанию являются non-nullable — это означает, что они не могут принимать значение null. Если переменная может принимать значение null, то ее нужно объявить с использованием знака вопроса (?). При использовании Nullable переменной в коде Kotlin не допустит обращение к ней без предварительной проверки на null-значение.

Также Kotlin предоставляет множество функций для безопасной работы с nullable-значениями, таких как операторы elvis ?:, безопасный вызов ?. и другие.

В целом, концепция Null safety помогает разработчикам избежать ошибок связанных с null-значениями, уменьшает количество ошибок в работе приложения и упрощает разработку и поддержку кода.

Способы проверки значения на null (if-else, операторы "?.", "!!.", "?:") ↑↑↑

Kotlin разграничивает типы с поддержкой и без поддержки null- значений. Это означает, что при объявлении переменной, которая может хранить null, нужно явно объявить ее как nullable при помощи символа ?.

val languageName: String? = null

Объявляя nullable переменную вы берёте на себя ответственность по проверке её значения. Иначе компилятор будет запрещать вызов функций для таких значений, ведь это может привести к NullPointerException.

Рассмотрим все доступные способы проверки значения на null.

1. Проверка с помощью if-else

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

if(languageName != null) {
    print("Name is : $languageName")
} else {
    print("Please enter a valid name")
}

Использование оператора безопасного вызова будет предпочтительнее, так как он позволяет решить проблему меньшим количеством кода. Однако, если со значением переменной производятся какие-то сложные вычисления и перед началом вычислений нужно проверить равно ли оно null, то if-else вполне подойдёт.

2. Оператор безопасного вызова "?."

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

languageName?.length

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

Если вы хотите вызвать функцию или каким-то другим способом обработать значение, отличное от null, то совместно с оператором безопасного вызова используйте функцию let. Всё, что будет указано в функции let выполнится только в том случае, если значение переменной отлично от null.

languageName?.let { println(it) }

3. Оператор "!!"

Два восклицательных знака, стоящих после nullable-значения, преобразуют его к типу без поддержки null. При этом перед преобразованием никак не проверяется, что значение действительно не содержит null. Поэтому, если в процессе выполнения программы окажется, что значение, которое пытается преобразовать оператор !!, все-таки null, то останется только один выход — выбросить исключение NullPointerException. Если оно не обрабатывается кодом, программа аварийно завершится. Несмотря на удобство этого оператора, его следует использовать только там, где вы уверены, что null быть не может.

Данный оператор понравится любителям NullPointerException. Он как бы говорит компилятору, что если значение переменной — null, то ТРЕБУЮ выбросить NullPointerException.

val languageName: String? = null
val size = languageName!!.length

Использование данного оператора крайне не рекомендуется, потому что (очевидно) это один из немногих способов словить NPE. При его использовании вы должны быть уверены, что значение переменной ни при каких обстоятельствах не может быть null. В противном случае лучше использовать оператор безопасного вызова.

4. Элвис оператор или оператор объединения по null "?:"

Оператор элвис подобен проверке на null в варианте if-else. Элвис используется для замены null каким-либо значением, принадлежащим обычно зауженному типу. В результате выражение с элвисом позволяет не увеличивать в программе количество nullable-переменных.

Оператор указывается между двумя значениями. Если значение слева от оператора равно null, то применяется значение справа.

val size: Int = languageName.length ?: 0

Если значение languageName не равно null, его длина будет присвоена переменной size.

Если languageName равно null, тогда будет присвоено значение 0.

Но в любом случае переменной size будет присвоено значение типа Int, а не Int?, то есть non-null тип.

Использование данного оператора с функцией let может полностью заменить проверку с помощью оператора if-else.

// с использованием if-else
if(languageName != null) {
    print("Name is : $languageName")
} else {
    print("Please enter a valid name")
}

// Элвис оператор и функция let
languageName?.let {
  print("Name is : $languageName")
} ?: print("Please enter a valid name")

Подробнее: bimlibik.github.io, kotlinlang.ru, younglinux.info

От какого класса унаследованы все остальные классы в Kotlin? ↑↑↑

Класс Any находится на вершине иерархии — все классы в Kotlin являются наследниками Any. Это стандартный родительский класс для всех классов, которые явно не унаследованы от другого класса. Именно в нем определены equals, hashCode и toString. Класс Any по назначению похож на Object в Java.

public open class Any {
   public open operator fun equals(other: Any?): Boolean
   public open fun hashCode(): Int
   public open fun toString(): String
}  

Подробнее: gb.ru, kotlins.org

Чем Any в Kotlin отличается от Object в Java? ↑↑↑

Any не является полным аналогом java.lang.Object.

В Object 11 методов в классе, в Any только 3 метода: equals(), hashCode() и toString(). При импорте типов Java в Kotlin все ссылки типа java.lang.Object преобразуются в Any. Поскольку Any не зависит от платформы, он объявляет только toString(), hashCode() и equals() в качестве своих членов, поэтому, чтобы сделать другие члены java.lang.Object доступными, Kotlin использует функции расширения.

Несмотря на то, что классы Object и Any имеют сходства (корневые классы иерархии классов), они также имеют и отличия, связанные с языковыми особенностями Kotlin и Java:

  1. Класс Any в Kotlin является не только базовым классом для пользовательских классов, но также и супертипом для всех не-nullable типов данных, включая примитивные. В то время как в Java, класс Object является базовым классом только для пользовательских классов.

  2. В Kotlin все классы наследуются от Any неявно, в то время как в Java, необходимо явно указывать наследование от Object.

  3. Класс Any в Kotlin также имеет nullable версию Any?, которая является супертипом для всех nullable типов данных в Kotlin. В то время как в Java, класс Object не имеет nullable версии.

Какой тип находится на вершине иерархии типов в Kotlin? ↑↑↑

Аналогично Object в Java, к чему можно привести любой тип в Kotlin? Правильным ответом будет Any?.

Сам по себе класс Any это почти аналог Object, однако, благодаря поддержке nullable и не-nullable типов в Kotlin мы получили Any?. Фактически, Any? соответствует любому типу и null, а Any только любому типу.

Если по порядку:

  1. Any является корнем иерархии не-nullable типов.

  2. Any? является корнем иерархии nullable типов.

  3. Так как Any? является супертипом Any, то Any? находится в самом верху иерархии типов в Kotlin.

Картинка для понимания: any

Сколько существует instance "Any" и сколько существует instance "Any?"? ↑↑↑

В Kotlin любой объект, включая null, имеет тип. Верхней границей типов в Kotlin является Any?. Тип Any представляет не-nullable объекты, тогда как Any?nullable объекты.

Таким образом, число возможных экземпляров Any и Any? является бесконечным, так как любой объект в Kotlin может быть null или non-null, что расширяет количество возможных комбинаций.

Кратко о Unit ↑↑↑

Тип Unit в Kotlin выполняет ту же функцию, что и void в Java.

Возвращаемый тип можно не указывать, если функция ничего не возвращает. По умолчанию там будет Unit:

fun knockKnock() {
   println("Who’s there?")
}

// то же самое, но с указанным типом Unit
fun knockKnock(): Unit = println("Who’s there?")

Подробнее: gb.ru, kotlins.org

Сколько существует instance Unit (1)? ↑↑↑

В стандартной библиотеке Kotlin Unit определён как объект, наследуемый от Any и содержащий единственный метод, переопределяющий toString():

public object Unit {
   override fun toString() = "kotlin.Unit"
}

Unit является синглтоном (ключевое слово object). Unit ничего не возвращает, а метод toString всегда будет возвращать “kotlin.Unit”. При компиляции в java-код Unit всегда будет превращаться в void.

Кратко о Nothing ↑↑↑

Nothing является типом, который полезен при объявлении функции, которая ничего не возвращает и не завершается.

Примеры:

  • функция, которая выбрасывает exception или в которой запущен бесконечный цикл

  • функция TODO()public inline fun TODO(): Nothing = throw NotImplementedError()

  • в тестах есть функция с именем fail, которая выдает исключение с определенным сообщением:

fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

Подробнее: gb.ru, kotlins.org

Сколько существует instance Nothing (0)? ↑↑↑

Nothing — класс, который является наследником любого класса в Kotlin, даже класса с модификатором final. При этом Nothing нельзя создать — у него приватный конструктор. В коде он объявлен так:

public class Nothing private constructor()

Есть ли аналог Nothing в Java (нет)? ↑↑↑

Тип Nothing является особенным, поскольку в Java ему нет аналогов.

Действительно, каждый ссылочный тип Java, включая java.lang.Void, принимает в качестве значения null, а Nothing не принимает даже этого. Таким образом, этот тип не может быть точно представлен в мире Java. Вот почему Kotlin генерирует необработанный тип, в котором используется аргумент типа Nothing:

fun emptyList(): List<Nothing> = listOf()
// is translated to
// List emptyList() { ... }

Модификаторы доступа — private, protected, internal, public ↑↑↑

Классы, объекты, интерфейсы, конструкторы, функции, свойства и их сеттеры могут иметь модификаторы доступа. Геттеры всегда имеют ту же видимость, что и свойства, к которым они относятся. Модификаторы доступа — это ключевые слова, с помощью которых можно задать область действия данных. Они позволяют регулировать уровень доступа к различным частям кода. Локальные переменные, функции и классы не могут иметь модификаторов доступа.

В Kotlin есть четыре модификатора доступа: private, protected, internal и public.
Если модификатор явно не указан, то присваивается значение по умолчанию — public.

  • Private — доступ к членам класса только в пределах самого класса. То есть, поля и методы с модификатором private недоступны из других классов и даже из наследников.

  • Protected — доступ к членам класса только в пределах класса и его наследников. То есть, поля и методы с модификатором protected доступны из класса и его наследников, но не из других классов.

  • Internal — доступ к членам модуля (module). Модуль — это набор файлов, компилирующихся вместе, поэтому все классы, объявленные внутри модуля, могут иметь доступ к членам с модификатором internal.

  • Public — не ограничивает доступ к членам класса. Поля и методы с модификатором public доступны из любого места программы, включая другие модули.

1. Модификатор private.

Private — самый строгий модификатор доступа. При его использовании данные будут доступны только в пределах конкретного класса или файла.

// Переменная видима внутри данного файла
private const val a = 20

// Класс доступен только внутри данного файла
private class Person() {

    // Переменную можно использовать только внутри класса Person
    private val b = a
}

// ERROR: переменную b нельзя использовать за пределами класса Person
private const val c = b

По сути, главное предназначение данного модификатора — реализация инкапсуляции в программе.

2. Модификатор protected.

Данные, отмеченные модификатором protected будут видны:

  • внутри класса, в котором они объявлены
  • в дочерних классах

При этом нельзя отметить модификатором protected данные высокого уровня. К таким данным относятся классы, а также переменные или функции, объявленные вне класса.

// ERROR: Нельзя использовать protected для переменных вне класса
protected const val a = 20

// ERROR: Нельзя использовать protected для класса
protected class Person() {

  // Переменная видима внутри класса Person
  protected val b = a
}

Если в дочернем классе мы переопределим метод с модификатором protected, то он унаследует модификатор доступа от родителя и будет виден только внутри дочернего класса. Несмотря на то, что модификатор не будет указан явно.

open class Person() {
    protected open fun getAge() = 20
}

private class Student : Person() {
    // модификатор явно не указан, но он такой же, как и в родительском классе
    override fun getAge() = 25
}

Помимо модификатора protected такому методу можно задать модификатор public. При использовании остальных модификаторов Kotlin ругается.

private class Student : Person() {
    override fun public getAge() = 25
}

3. Модификатор internal.

Как правило, при разработке проекта мы делим его на независимые модули. Каждый модуль состоит из файлов, компилируемых вместе. Так вот модификатор internal позволяет сделать данные видимыми для всего модуля.

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

Например, в проекте есть два модуля — Module1 и Module2. В первом модуле есть класс Person().

// Module1

// Переменная видима для всего Module1
internal const val a = 20

// Класс доступен для всего Module1
internal open class Person() {
    // Переменная видима для всего Module1
    internal val b = a
}

И еще в первом модуле есть такой файл:

// Module1

private const val c = a + b

Так как этот файл тоже находится в Module1, то мы можем получить доступ к переменным a и b. Но если попытаться к ним обратиться из Module2 — получим ошибку.

// Module2

// ERROR: переменные a и b недоступны для данного модуля
private const val d = a + b

4. Модификатор public.

Если при объявлении каких-либо данных использовать модификатор public, то они будут видны всем (даже в космосе). Еще public является модификатором по умолчанию для тех данных, которым модификатор явно не был указан.

// Переменные доступны из любого места

public const val a = 20

public open class Person() {

    public val b = a
}

public class Student() {

    public val с = a + b
}

Подробнее: bimlibik.github.io, kotlinlang.ru.

Разница между var, val, const val ↑↑↑

  1. var — это изменяемая переменная. После инициализации мы можем изменять данные, хранящиеся в переменной.

Переменные val и const val доступны только для чтения — это неизменяемые переменные.

  1. val — константа времени выполнения, т.е. значение можно назначить во время выполнения программы.

  2. const val — константа времени компиляции, т.к. значения константам присваивается при компиляции (в момент, когда программа компилируется).

В отличие от val, значение const val должно быть известно во время компиляции.

Особенности const val:

  • могут получать значение только базовых типов: Int, Double, Float, Long, Short, Byte, Char, String, Boolean.

  • объявляются в глобальной области видимости, то есть за пределами функции main() или любой другой функции.

  • нет пользовательского геттера.

Расширенный ответ — модификатор const

Чтобы объявить константу, нужно использовать модификатор const совместно с ключевым словом val. Переменные, отмеченные модификатором const, также называют константами времени компиляции. Это означает, что значения таких переменных известны во время компиляции. Отсюда следует, что они должны соответствовать следующим требованиям:

  • находиться на самом верхнем уровне (вне класса) или быть членом объекта (object или companion object)
  • тип данных должен соответствовать одному из примитивных (например, String)
  • не иметь геттера

Пример:

class SomeClass {
    companion object {    
        const val FILE_EXTENSION = ".jpg"    

        val FILENAME: String
          get() = "Img_" + System.currentTimeMillis() + FILE_EXTENSION
    }
}

Здесь мы в объекте-компаньоне объявили константу, значением которой является расширение фотографии. Помимо этого мы объявили неизменяемую переменную, которая будет хранить имя фотографии и инициализироваться с помощью get-метода.

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

Имя же фотографии, несмотря на то что оно уникально для каждого отдельного файла, заранее неизвестно. Чтобы его задать, нам потребуется вычислить время, в которое был сделан снимок. То есть значение выбирается во время выполнения программы. Поэтому используется ключевое слово val.

После компиляции кода везде, где использовалась переменная-константа будет произведено замещение: вместо имени переменной будет подставлено значение этой переменной. Переменная, которая хранит имя файла останется как есть.

Как стоит объявлять свои константы в Kotlin: при помощи companion object или вне класса?

На самом деле оба эти подхода приемлемы. Однако, использование companion object может быть излишним: компилятор Kotlin преобразует companion object во вложенный класс. Слишком много кода для простой константы.

Если вам не требуется поведение, специфичное для companion object, объявляйте константы вне класса, так как это будет способствовать более эффективному байт-коду. Да и сам синтаксис объявления констант вне класса более чистый и читабельный.

Свойства, методы get и set ↑↑↑

Свойства класса — это переменные, которые хранят состояние объекта класса. Как и любая переменная, свойство может иметь тип, имя и значение.

В классе можно объявить свойства с помощью ключевого слова var или val. Свойства, объявленные с var, могут быть изменены после их инициализации, а свойства, объявленные с val, только для чтения.

class Person {
    var name: String = ""
    val age: Int = 0
}

При создании своего класса мы хотим сами управлять его свойствами, контролируя то, какие данные могут быть предоставлены или перезаписаны. С этой целью создаются get и set методы (геттеры и сеттеры). Цель get-метода — вернуть значение, а set-метода — записать полученное значение в свойство класса.

var name: String = ""
    get() = field.toUpperCase()
    set(value) {
        field = "Name: $value"
    }

В данном примере свойство name имеет тип String и начальное значение пустой строки. Геттер возвращает значение свойства, преобразованное к верхнему регистру. Сеттер устанавливает значение свойства с добавлением префикса "Name: " перед переданным значением. Слово field используется для обращения к текущему значению свойства.

Если get и set методы не были созданы вручную, то для таких свойств Kotlin незаметно сам их генерирует. При этом для свойства, объявленного с val, генерируется get-метод, а для свойства, объявленного с varи get, и set методы.

Подробнее: metanit.com и kotlinlang.ru

В чем отличие field от property? ↑↑↑

В Kotlin свойство (property) — это абстракция над полями (fields), которая позволяет обращаться к значению переменной через методы геттера и сеттера, вместо прямого доступа к полю.

Field — это переменная, которая содержит значение и может быть доступна напрямую или через геттер/сеттер.

Пример определения свойства с геттером и сеттером в классе:

class Person {
    var name: String = ""
        get() = field.toUpperCase()  // возвращает значение поля name в верхнем регистре
        set(value) {
            field = value.trim()    // устанавливает значение поля name без начальных и конечных пробелов
        }
}

В данном примере свойство name содержит поле, которое может быть доступно напрямую только внутри класса, и методы геттера и сеттера, которые позволяют получать и изменять значение свойства через специальные методы.

Отложенная и ленивая инициализация свойств (lateinit и by lazy) ↑↑↑

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

1. lateinit

Модификатор lateinit говорит о том, что данная переменная будет инициализирована позже. При этом инициализировать свойство можно из любого места, откуда она видна.

Правила использования модификатора lateinit:

  • lateinit может использоваться только с var свойствами класса
  • lateinit может быть применен только к свойствам, объявленным внутри тела класса (но не в основном конструкторе), а также к переменным на верхнем уровне и локальным переменным
  • lateinit свойства могут иметь любой тип, кроме примитивных типов (таких как Int, Long, Double и т.д.)
  • lateinit свойства не могут быть nullable (т.е. обязательно должно быть объявлены без знака вопроса)
  • lateinit свойства не могут быть проинициализированы сразу при их объявлении
  • lateinit свойства должны быть инициализированы до первого обращения к ним, иначе будет выброшено исключение UninitializedPropertyAccessException
  • Нельзя использовать lateinit для переменных, определенных внутри локальных областей видимости (например, внутри функций)
  • При использовании модификатора lateinit у свойства не должно быть пользовательских геттеров и сеттеров

Для проверки факта инициализации переменной вызывайте метод isInitialized(). Функцию следует использовать экономно — не следует добавлять эту проверку к каждой переменной с отложенной инициализацией. Если вы используете isInitialized() слишком часто, то скорее всего вам лучше использовать тип с поддержкой null.

lateinit var catName: String

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContentView(R.layout.activity_main)
 
    catName = "Barsik"

    if (::catName.isInitialized) {
        Log.d("Kot", "Hi, $catName")
    }
}

2. by lazy

Ленивая инициализация (lazy initialization) — это подход, при котором объект инициализируется только при необходимости, а не сразу после создания. В Kotlin для ленивой инициализации свойств используется делегат lazy.

Когда свойство объявляется с использованием делегата lazy, оно не инициализируется сразу, а только тогда, когда к нему происходит первое обращение. При этом инициализация выполняется единожды, и в дальнейшем значение свойства сохраняется для всех последующих обращений к нему. Таким образом, ленивая инициализация позволяет оптимизировать использование ресурсов приложения, не инициализируя объекты, которые не понадобятся в ходе выполнения программы.

При использовании ленивой инициализации свойств с помощью by lazy в Kotlin, создается объект типа Lazy<T>, где T — это тип свойства, и этот объект используется для хранения значения свойства.

Когда код доходит до места, где используется свойство, вызывается метод getValue() этого объекта Lazy<T>. Если значение свойства еще не было проинициализировано, то вызывается лямбда-выражение, переданное в lazy { }, и ее результат используется для инициализации свойства. Значение сохраняется в объекте Lazy<T> и возвращается как результат метода getValue(). Если значение уже было проинициализировано, то просто возвращается сохраненное значение. Например, если у нас есть свойство:

val myProperty: Int by lazy { computeValue() }

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

3. Сравнение ленивой и отложенной инициализации

  • ленивая инициализация является одним из Delegate
  • отложенная инициализация требует использования модификатора свойства
  • ленивая инициализация применяется только к val
  • отложенная инициализация применяется только к var
  • у нас может быть ленивое свойство примитивного типа
  • lateinit применяется только к ссылочным типам

Самое главное, когда мы реализуем свойство как ленивый делегат, мы фактически присваиваем ему своего рода значение. Вместо фактического значения мы помещаем туда функцию для его вычисления, когда оно нам понадобится.

С другой стороны, когда мы объявляем свойство как lateinit, мы просто отключаем одну из проверок компилятора, которая гарантирует, что программа не обращается ни к одной переменной до того, как она получит значение. Вместо этого мы обещаем сделать эту проверку сами.

Подробнее: alexanderklimov.ru, bimlibik.github.io.

Что такое делегированные свойства (Delegated properties)? ↑↑↑

Делегированные свойства (Delegated properties) — это свойства, которые не хранят своё значение напрямую, а делегируют это значение другому объекту, который реализует интерфейс Delegate. При доступе к свойству, его значение запрашивается у делегата, который может выполнить какую-то дополнительную логику, а затем вернуть требуемое значение.

В Kotlin существуют несколько встроенных делегатов для работы с делегированными свойствами:

  • observable() — позволяет реагировать на изменения свойства
  • vetoable() — позволяет отклонять изменения значения свойства на основе заданного условия
  • notNull() — гарантирует, что свойство не будет иметь значение null
  • map() — позволяет хранить значения свойств в словаре (Map)
  • lazy() — позволяет создавать лениво инициализированные свойства

Кроме того, в Kotlin можно создавать свои собственные делегаты, реализуя интерфейс ReadOnlyProperty или ReadWriteProperty. Это дает возможность создавать кастомные поведения для свойств, например, кеширование значений или логирование операций чтения/записи.

Подробнее: kotlinlang.ru, tech-geek.ru

Как реализовать кастомный делегат? ↑↑↑

Чтобы написать кастомный делегат, нужно определить класс, который реализует интерфейс ReadOnlyProperty для делегата val или ReadWriteProperty для делегата var.

Классы, реализующие ReadOnlyProperty и ReadWriteProperty, содержат два метода:

  • getValue(thisRef: T, property: KProperty<*>): R, который должен возвращать значение свойства.
  • setValue(thisRef: T, property: KProperty<*>, value: R), который должен устанавливать значение свойства.

Например, рассмотрим создание кастомного делегата для логирования изменения значения свойства:

class LoggingDelegate<T>(private var value: T) : ReadWriteProperty<Any?, T> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("Getting value of ${property.name}: $value")
        return value
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        println("Setting value of ${property.name} to $value")
        this.value = value
    }
}

Здесь мы определяем класс LoggingDelegate, который реализует интерфейс ReadWriteProperty. Метод getValue выводит в консоль текущее значение свойства и возвращает его, а метод setValue выводит новое значение свойства в консоль и сохраняет его в переменной value.

Затем мы можем использовать наш кастомный делегат следующим образом:

class MyClass {
    var myProperty: Int by LoggingDelegate(0)
}

fun main() {
    val obj = MyClass()
    obj.myProperty = 42 // Setting value of myProperty to 42
    println(obj.myProperty) // Getting value of myProperty: 42
}

Здесь мы создаем экземпляр класса MyClass, который содержит свойство myProperty, использующее наш кастомный делегат LoggingDelegate. При установке значения свойства или получении его значения будут вызываться соответствующие методы нашего делегата, и мы увидим соответствующие сообщения в консоли.

Подробнее: kotlinlang.org

Конструкторы. Какие типы конструкторов вы знаете? ↑↑↑

Свойств у класса может быть столько, сколько ему нужно. Но все они должны быть инициализированы при создании экземпляра этого класса. Поэтому для удобства был придуман конструктор — это специальный блок кода, который вызывается при создании экземпляра класса. Ему передаются необходимые значения, которые потом используются для инициализации свойств.

Класс в Kotlin может иметь основной конструктор (primary constructor) и один или более вторичных конструкторов (secondary constructors). У класса может и не быть конструктора, но Kotlin всё равно автоматически сгенерирует основной конструктор по умолчанию (без параметров).

1. Основной конструктор

Объявляется он сразу после имени класса и состоит из ключевого слова constructor и круглых скобок:

class Person constructor(name: String, age: Int) {
}

Можно обойтись и без ключевого слова constructor при условии, что нет аннотаций или модификаторов доступа.

class Person(name: String, age: Int)

Параметры, переданные в конструктор, можно использовать для инициализации свойств, объявленных в теле класса.

class Person(name: String, age: Int) {
    val name = name
    var age = age
}

А можно упростить еще больше и из параметров конструктора сделать свойства класса. Для этого перед именем параметра нужно указать ключевое слово val (только для чтения) или var (для чтения и редактирования).

class Person(val name: String, var age: Int)

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

class Person(val name: String, var age: Int = 30)
...
val adam = Person("Adam")
val alice = Person("Alice", 25)
println("${adam.name}, ${adam.age}")      // Adam, 30
println("${alice.name}, ${alice.age}")    // Alice, 25

У класса может быть суперкласс. Тогда его основной конструктор должен инициализировать свойства, унаследованные от суперкласса.

open class Base(p: Int)

class Person(val name: String, var age: Int = 30, val p: Int) : Base(p)
...
val adam = Person("Adam", 30, 1000)
println(adam.p)                      // 1000

Конструктор можно сделать приватным. Тогда никто и ничто не сможет создать экземпляр этого класса.

class Person private constructor(val name: String, var age: Int)
...
val adam = Person("Adam", 30)  // вылетит ошибка

2. Вторичный конструктор

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

Объявляется вторичный конструктор внутри тела класса при помощи ключевого слова constructor.

class Person {
  constructor(id: Int) {
    ...
  }
}

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

class Person(val name: String, var age: Int) {

    constructor(name: String, age: Int, id: Int) : this(name, age) {
        ...
    }
}

Если основного конструктора нет, то и обращаться к нему не надо.

Во вторичном конструкторе нельзя объявлять свойства класса. Все передаваемые ему параметры можно использовать либо для передачи основному конструктору, либо для инициализации свойств, объявленных в теле класса.

class Person(val name: String, var age: Int) {
    var id: Int = 0

    constructor(name: String, age: Int, id: Int) : this(name, age) {
        this.id = id
    }
}

Также во вторичный конструктор можно добавить какую-либо логику.

class Person(val name: String, var age: Int) {
    var id: Int = 0

    constructor(name: String, age: Int, id: Int) : this(name, age) {
        if (id > 0) this.id = id * 2
    }
}

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

open class Base(val p: Int)

class Person : Base {
    constructor(name: String, age: Int, p: Int) : super(p)
}
...
val adam = Person("Adam", 30, 1)
println(adam.p)                   // 1.

Подробнее: kotlinlang.ru, bimlibik.github.io

Блок инициализации (init блок) ↑↑↑

Основной конструктор не может в себе содержать какую-либо логику по инициализации свойств (исполняемый код). Он предназначен исключительно для объявления свойств и присвоения им полученных значений. Поэтому вся логика может быть помещена в блок инициализации — блок кода, обязательно выполняемый при создании объекта независимо от того, с помощью какого конструктора этот объект создаётся. Помечается он словом init.

class Person(val name: String, var age: Int) {
    var id: Int = 0

    // require выдает ошибку с указанным текстом, если условие в левой части false
    init {
        require(name.isNotBlank(), { "У человека должно быть имя!" })
        require(age > -1, { "Возраст не может быть отрицательным." })
    }

    constructor(name: String, age: Int, id: Int) : this(name, age) {
        if (id > 0) this.id = id * 2
    }
}

По сути блок инициализации — это способ настроить переменные или значения, а также проверить, что были переданы допустимые параметры. Код в блоке инициализации выполняется сразу после создания экземпляра класса, т.е. сразу после вызова основного конструктора. В классе может быть один или несколько блоков инициализации и выполняться они будут последовательно.

class Person(val name: String, var age: Int) {
    // сначала вызывается основной конструктор и создаются свойства класса
    // далее вызывается первый блок инициализации
    init {
        ...
    }

    // после первого вызывается второй блок инициализации
    init {
        ...
    }

    // и т.д.
}

Блок инициализации может быть добавлен, даже если у класса нет основного конструктора. В этом случае его код будет выполнен раньше кода вторичных конструкторов. image

Подробнее: bimlibik.github.io, kotlinlang.ru.

Расскажите о Data классах. Какие преимущества они имеют? ↑↑↑

Data класс предназначен исключительно для хранения каких-либо данных.

Основное преимущество: для параметров, переданных в основном конструкторе автоматически будут переопределены методы toString(), equals(), hashCode(), copy().

Также для каждой переменной, объявленной в основном конструкторе, автоматически генерируются функции componentN(), где N — номер позиции переменной в конструкторе.

Благодаря наличию вышеперечисленных функций внутри data класса мы исключаем написание шаблонного кода.

Подробнее: kotlinlang.ru и bimlibik.github.io

Что такое мульти-декларации (destructuring declarations)? ↑↑↑

Мульти-декларации (destructuring declarations или деструктуризирующее присваивание) — это способ извлечения значений из объекта и присвоения их сразу нескольким переменным. В Kotlin этот механизм поддерживается с помощью оператора распаковки (destructuring operator) — componentN(), где N — номер компонента.

При создании data класса Kotlin автоматически создает функции componentN() для каждого свойства класса, где N — номер позиции переменной в конструкторе. Функции componentN() возвращают значения свойств в порядке их объявления в конструкторе. Это позволяет использовать мульти-декларации для распаковки значений свойств и присваивания их отдельным переменным.

Например, если у нас есть data класс Person с двумя свойствами name и age, мы можем использовать мульти-декларации, чтобы извлечь эти свойства и присвоить их двум переменным:

data class Person(val name: String, val age: Int)

val person = Person("Alice", 29)
val (name, age) = person

println(name) // Alice
println(age) // 29

Также можно использовать мульти-декларации в циклах, чтобы итерироваться по спискам объектов и распаковывать значения свойств:

val people = listOf(Person("Alice", 30), Person("Bob", 40))
for ((name, age) in people) {
    println("$name is $age years old")
}

// Alice is 30 years old
// Bob is 40 years old

Мульти-декларации также могут быть использованы с массивами и другими коллекциями:

val list = listOf("apple", "banana", "orange")
val (first, second, third) = list

println(first) // apple
println(second) // banana
println(third) // orange

Подробнее: kotlinlang.ru

Что делает функция componentN()? ↑↑↑

Функция componentN() возвращает значение переменной и позволяет обращаться к свойствам объекта класса по их порядковому номеру. Генерируется автоматически только для data классов.

Также функцию componentN() можно создать самому для класса, который не является data классом.

class Person(val firstName: String, val lastName: String, val age: Int) {
    operator fun component1() = firstName
    operator fun component2() = lastName
    operator fun component3() = age
}

Теперь можно использовать мульти-декларации для класса Person:

val person = Person("John", "Doe", 30)
val (firstName, lastName, age) = person
println("$firstName $lastName is $age years old.")

В данном примере мы определили функции component1(), component2() и component3() как операторы с ключевым словом operator. Они возвращают значения свойств firstName, lastName и age соответственно. После этого мы можем использовать мульти-декларации для разбивки объекта Person на отдельные переменные.

Какие требования должны быть соблюдены для создания data класса? ↑↑↑

  • Класс должен иметь хотя бы одно свойство, объявленное в основном конструкторе.

  • Все параметры основного конструктора должны быть отмечены val или var (рекомендуется val).

  • Классы данных не могут быть abstract, open, sealed или inner.

Можно ли наследоваться от data класса? ↑↑↑

От data класса нельзя наследоваться т.к. он является final классом, но он может наследоваться от других классов.

Что такое абстрактные классы и интерфейсы? ↑↑↑

Абстрактные классы и интерфейсы используются для описания абстрактных концепций, не имеющих реализации.

1. Абстрактный класс — это класс, представляющий из себя "заготовку" для целого семейства классов, который описывает для них общий шаблон поведения. Экземпляр такого класса не может быть создан. Абстрактному классу не нужен модификатор open, потому что он "открыт" для наследования по умолчанию.

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

abstract class Tree {
  abstract val name: String
  abstract val description: String
  abstract fun info()
}

Каждый наследник обязан переопределять их все.

class Pine : Tree() {
  override val name = "Сосна"
  override val description = "Хвойное дерево с длинными иглами и округлыми шишками"
  override fun info() = "$name - ${description.toLowerCase()}."  
}

Свойства и функции необязательно должны быть абстрактными. У них может быть обобщенная реализация, которая будет с пользой наследоваться всеми подклассами. В этом случае для них в абстрактном классе объявляется конкретная реализация, к которой имеют доступ все наследники.

abstract class Tree {
  abstract val name: String
  abstract val description: String
  fun info(): String = "$name - ${description.toLowerCase()}."
}

...

class Pine : Tree() {
  override val name = "Сосна"
  override val description = "Хвойное дерево с длинными иглами и округлыми шишками"
}

...

val pine = Pine()
println(pine.info())

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

class Pine : Tree() {
  override val name = "Сосна"
  override val description = "Хвойное дерево с длинными иглами и округлыми шишками"

  // ошибка: функция "info" является "final" и не может быть переопределена
  override fun info() = description  
}

Чтобы это исправить, нужно явно задать модификатор open для функции с конкретной реализацией. Тогда у наследников появляется выбор: либо не переопределять функцию и использовать реализацию суперкласса, либо переопределить и указать свою собственную реализацию.

abstract class Tree {
  abstract val name: String
  abstract val description: String

  open fun info(): String = "$name - ${description.toLowerCase()}."
}

У абстрактного класса может быть конструктор.

abstract class Tree(val name: String, val description: String) {
  open fun info(): String = "$name - ${description.toLowerCase()}."
}

Тогда каждый наследник должен предоставить для него значения.

class Pine(name: String, description: String) : Tree(name, description)

...

val pine = Pine("Сосна", "Хвойное дерево с длинными иглами и округлыми шишками")
println(pine.info())

2. Интерфейс — это совокупность методов и правил, которые определяют поведение класса или общее поведение для группы независимых друг от друга классов. Интерфейсы похожи на абстрактные классы тем, что нельзя создать их экземпляры и они могут определять абстрактные или конкретные функции и свойства. Отличие в том, что интерфейсу не важна связь "родитель-наследник", он задаёт лишь правила поведения.

Интерфейсы в Kotlin могут содержать объявления абстрактных методов, а также методы с реализацией. Главное отличие интерфейсов от абстрактных классов заключается в невозможности хранения переменных экземпляров. Они могут иметь свойства, но те должны быть либо абстрактными, либо предоставлять реализацию методов доступа.

В теле интерфейса можно определять абстрактные свойства и функции. Для этого не требуется использовать ключевое слово abstract, так как Kotlin способен сам понять, что свойство и функция без реализации должны быть абстрактными. Также обратите внимание, что единственный способ определить свойство — это определить его в теле интерфейса, так как у интерфейса не бывает конструкторов.

interface Cultivable {
  val bloom: Boolean
  fun startPhotosynthesis()
}

Класс должен реализовывать все абстрактные свойства и функции, определённые в интерфейсе.

abstract class Tree : Cultivable {
  abstract val name: String
  abstract val description: String
  open fun info(): String = "$name - ${description.toLowerCase()}."

  override val bloom = false
  override fun startPhotosynthesis() {
    ...
  }
}

При этом если интерфейс реализовывается в абстрактном классе, то свойства и функции интерфейса могут быть в нём опущены. Тогда все наследники абстрактного класса должны будут их переопределять.

abstract class Tree : Cultivable {
  abstract val name: String
  abstract val description: String
  open fun info(): String = "$name - ${description.toLowerCase()}."

  override fun startPhotosynthesis() {
    ...
  }
}

class Pine : Tree() {
  override val name = "Сосна"
  override val description = "Хвойное дерево с длинными иглами и округлыми шишками"

  override val bloom = false
}

В интерфейсе можно определять свойства и функции с конкретной реализацией (по умолчанию). Классы, реализующие этот интерфейс, могут использовать реализацию по умолчанию или определить свою. При этом реализация свойств осуществляется с помощью метода доступа get().

interface Cultivable {
  val bloom: Boolean
    get() = false

  fun startPhotosynthesis() {
    ...
  }
}

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

interface Fruitable {
  val fruit: String
    get() = "неплодоносный"
}

interface Cultivable : Fruitable {
  ...

  fun isFruitable() : Boolean {
    if(fruit == "неплодоносный") return false
    return true
  }
}

Каждый класс, реализующий интерфейс Cultivable может использовать свойства и функции интерфейса Fruitable, если в этом есть необходимость.

class AppleTree() : Tree() {
  override val name = "Яблоня"
  override val description = "Фруктовое дерево"
  override val fruit = "яблоко"
}

...

val appleTree = AppleTree()
if(appleTree.isFruitable()) {
  println("Плод - ${appleTree.fruit}.")
} else {
  println("${appleTree.name} не плодоносит.")
}

3. Как выбрать, что применять — абстрактный класс или интерфейс?

  • У вас есть семейство классов, из которых можно выделить общую сущность? Определите эту сущность в качестве абстрактного класса и она будет “заготовкой” для всего семейства.
  • Вам нужно создать более конкретную версию класса? Создайте подкласс этого класса и добавьте недостающее поведение.
  • Требуется определить общее поведение для группы независимых друг от друга классов? Создайте интерфейс и реализуйте его теми классами, которым необходимо это поведение.

4. Ключевые моменты:

  • Абстрактный класс — это "заготовка" для целого семейства классов.
  • Нельзя создать экземпляр абстрактного класса.
  • Абстрактный класс может содержать как абстрактные, так и конкретные реализации свойств и функций.
  • Класс, который содержит абстрактное свойство или функцию, должен быть объявлен абстрактным.
  • Абстрактный класс может быть без единого абстрактного свойства или функции.
  • У класса может быть только один суперкласс.
  • Наследники абстрактного класса должны переопределять все его абстрактные свойства и функции.
  • Чтобы наследники могли переопределять конкретные реализации свойств и функций, для них в абстрактном классе должен быть явно указан модификатор open.
  • У абстрактного класса может быть конструктор.
  • Интерфейс определяет поведение класса или общее поведение для группы независимых друг от друга классов.
  • Нельзя создать экземпляр интерфейса.
  • Интерфейс может содержать как абстрактные, так и конкретные реализации функций.
  • Свойства интерфейсов могут быть абстрактными, а могут иметь get() методы.
  • Класс может реализовывать несколько интерфейсов.
  • Класс должен реализовывать все абстрактные свойства и функции, определённые в интерфейсе.
  • Если интерфейс реализовывается абстрактным классом, то переопределение его абстрактных свойств и функций может быть передано наследникам абстрактного класса.
  • Интерфейс может реализовывать другой интерфейс.

Подробнее: kotlinlang.ru и bimlibik.github.io

Почему классы в Kotlin по умолчанию final? ↑↑↑

Классы в Kotlin по умолчанию являются final для того, чтобы избежать случайного наследования и переопределения методов. Это сделано для повышения безопасности кода и уменьшения сложности программы, так как ограничение наследования помогает избежать ошибок, связанных с неожиданным изменением поведения унаследованных методов.

В Kotlin рекомендуется использовать композицию вместо наследования для повторного использования кода и расширения функциональности.

Что нужно сделать, чтобы класс можно было наследовать? (open) ↑↑↑

По умолчанию, классы в Kotlin объявляются как final, то есть их нельзя наследовать. Если мы всё же попытаемся наследоваться от такого класса, то получим ошибку: “This type is final, so it cannot be inherited from”.

Чтобы класс можно было наследовать, его нужно объявить с модификатором open.

open class Fraction {
  ...
}

Не только классы, но и функции в Kotlin по умолчанию имеют статус final. Поэтому те функции, которые находятся в родительском классе и которые вы хотите переопределить в дочерних классах, также должны быть отмечены open.

open class Fraction {

  open fun toAttack() {
    ...
  }

}

Свойства класса также по умолчанию являются final. Для возможности переопределения таких свойств в дочерних классах, не забудьте и их отметить ключевым словом open.

open class Fraction {

  open val name: String = "default"

  open fun toAttack() {
    ...
  }

}

При этом, если в открытом классе будут присутствовать функции и свойства, которые не отмечены словом open, то переопределяться они не будут. Но дочерний класс сможет к ним обращаться.

open class Fraction {

  open val name: String = "default"

  fun toAttack() {
    ...
  }

}

class Horde : Fraction() {
  override val name = "Horde"
}

class SomeClass() {
  val horde = Horde()
  horde.toAttack()
}

Подробнее: bimlibik.github.io, kotlinlang.ru.

Как можно получить тип класса? ↑↑↑

1. Получение типа класса через функцию ::class

Функция ::class возвращает объект KClass, который содержит информацию о типе класса во время выполнения.

class Person(val name: String, val age: Int)

fun main() {
    val person = Person("John", 30)
    println(person::class) // выводит "class Person"
}

2. Получение типа класса через функцию javaClass

Функция javaClass возвращает объект Class, который содержит информацию о типе класса во время выполнения.

class Person(val name: String, val age: Int)

fun main() {
    val person = Person("John", 30)
    println(person.javaClass) // выводит "class Person"
}

3. Получение типа класса через функцию ::class.java

Вызов функции ::class.java на объекте типа KClass возвращает объект Class, который содержит информацию о типе класса во время выполнения.

class Person(val name: String, val age: Int)

fun main() {
    val person = Person("John", 30)
    println(person::class.java) // выводит "class Person"
}

Подробнее: kotlinlang.ru

Что такое enum класс (перечислений)? ↑↑↑

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

enum class ColorType {
  RED,
  BLUE,
  GREEN
}

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

var color: ColorType
color = ColorType.RED

Помимо самих констант в класс перечислений можно добавить свойства и функции. Их необходимо отделять от констант точкой с запятой. Это единственное место в Kotlin, где используется точка с запятой.

enum class ColorType {
  RED,
  BLUE,
  GREEN;

  fun names() = "Красный, Голубой, Зелёный"
  val rgb = "0xFFFFFF"
}

При этом каждая константа сможет обращаться к этому свойству или функции.

var color: ColorType = ColorType.RED
println(color.names()) // выведет "Красный, Голубой, Зелёный"
println(color.rgb) // выведет "0xFFFFFF"

Классы перечислений как и обычные классы также могут иметь конструктор. Так как константы являются экземплярами enum-класса, они могут быть инициализированы.

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}

Enum-константы также могут объявлять свои собственные анонимные классы как с их собственными методами, так и с перегруженными методами базового класса. Напоминаю, что при объявлении в enum-классе каких-либо членов, необходимо отделять их от объявления констант точкой с запятой.

enum class ProtocolState {
    WAITING {
        override fun signal() = TALKING
    },

    TALKING {
        override fun signal() = WAITING
    };

    abstract fun signal(): ProtocolState
}

Подробнее: kotlinlang.ru и metanit.com

Что такое sealed класс (изолированный)? ↑↑↑

Sealed class (изолированный класс) — это класс, который является абстрактным и используется в Kotlin для ограничения классов, которые могут наследоваться от него.

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

  • Конструктор изолированного класса всегда приватен, и это нельзя изменить.

  • У sealed класса могут быть наследники, но все они должны находиться в одном файле с изолированным классом. Изолированный класс "открыт" для наследования по умолчанию, указывать слово open не требуется.

  • Наследники sealed класса могут быть классами любого типа: data class, объектом, обычным классом, другим sealed классом. Классы, которые расширяют наследников sealed класса могут находиться где угодно.

  • Изолированные классы абстрактны и могут содержать в себе абстрактные компоненты.

  • Изолированные классы нельзя инициализировать.

  • При использовании when, все подклассы, которые не были проверены в конструкции, будут подсвечены IDE.

  • Не объявляется с ключевым словом inner.

Пример sealed класса:

sealed class Shape {
    class Circle(val radius: Double) : Shape()
    class Rectangle(val width: Double, val height: Double) : Shape()
    class Triangle(val base: Double, val height: Double) : Shape()
}

fun calculateArea(shape: Shape): Double {
    return when (shape) {
        is Shape.Circle -> Math.PI * shape.radius * shape.radius
        is Shape.Rectangle -> shape.width * shape.height
        is Shape.Triangle -> 0.5 * shape.base * shape.height
    }
}

fun main() {
    val circle = Shape.Circle(5.0)
    val rectangle = Shape.Rectangle(2.0, 3.0)
    val triangle = Shape.Triangle(4.0, 5.0)

    println(calculateArea(circle))     // Output: 78.53981633974483
    println(calculateArea(rectangle))  // Output: 6.0
    println(calculateArea(triangle))   // Output: 10.0
}

В этом примере мы определили sealed class Shape, который содержит три класса: Circle, Rectangle и Triangle.
Эти классы наследуются от Shape. Это означает, что мы можем создавать объекты этих классов и использовать их, как объекты типа Shape.

В функции calculateArea мы используем выражение when, чтобы определить тип фигуры и вернуть ее площадь. Таким образом, если мы передадим Shape.Circle в calculateArea, то будет вычислена площадь круга.

В функции main мы создали объекты Circle, Rectangle и Triangle и передали их в calculateArea, чтобы вычислить их площади.

Подробнее: kotlinlang.ru и bimlibik.github.io

Какая разница между sealed class и enum? ↑↑↑

Sealed class и Enum это два разных концепта в Kotlin, хотя их часто используют для ограничения набора возможных значений. Основная разница между ними:

  • enum представляет собой конечный список значений, которые объявляются заранее в момент компиляции, и не могут быть расширены или изменены во время выполнения программы
  • sealed class позволяет определять ограниченный набор значений, но эти значения могут быть расширены в будущем

В общем, enum class используется для представления конечного списка опций или состояний, тогда как sealed class используется для определения ограниченного набора значений, которые могут быть произвольными объектами.

Что такое inner (внутренние) и nested (вложенные) классы? ↑↑↑

В Kotlin можно объявить один класс внутри другого. Это может быть полезно в тех случаях, когда вам нужно организовать код и логически связать классы между собой. Подобные классы разделяются на внутренние (inner) и вложенные (nested).

1. Внутренние классы (inner classes) имеют доступ к членам внешнего класса, даже если они объявлены как private. Внутренний класс является частью внешнего класса и имеет доступ к его свойствам и методам. В Kotlin внутренний класс объявляется с помощью ключевого слова inner. Например:

class Outer {
    private val outerProperty = "Outer Property"
    
    inner class Inner {
        fun innerMethod() {
            println("Accessing outer property: $outerProperty")
        }
    }
}

В этом примере Inner является внутренним классом, а Outer является внешним классом. Inner имеет доступ к членам Outer, в том числе к приватным свойствам и методам, таким как outerProperty.

2. Вложенные классы (nested classes) не имеют доступа к членам внешнего класса по умолчанию. Они имеют свои собственные члены, которые могут быть использованы только внутри класса. Вложенный класс объявляется внутри внешнего класса, но не имеет доступа к его членам, если не является явно объявлен внутри. Например:

class Outer {
    private val outerProperty = "Outer Property"
    
    class Nested {
        fun nestedMethod() {
            println("Accessing nested property")
        }
    }
}

Здесь Nested является вложенным классом. Он не имеет доступа к свойству outerProperty, но может использовать свои собственные члены, такие как nestedMethod.

3. Ключевое отличие: внутренний (inner) класс — это вложенный (nested) класс, который может обращаться к компонентам внешнего класса.

Подробнее: kotlinlang.ru и bimlibik.github.io

Value (бывшие inline) классы. ↑↑↑

Тем, кто хочет подробно узнать историю создания inline классов в Kotlin и почему было принято решение переименовать модификатор inline в value — лучше прочитать текст от первоисточника Романа Елизарова.

Кратко: в Kotlin версии 1.2.30 была добавлена функциональность inline (встраиваемых) классов. Это позволило создавать классы, которые компилируются в обычные примитивы (Int, Long и другие), но при этом могли содержать дополнительные методы и свойства.

В Kotlin 1.5 была добавлены value классы (классы значений), которые заменили inline классы. Классы значений предоставляют те же преимущества, что и inline классы, но с улучшенным синтаксисом и дополнительными возможностями.

Преимущества Value классов в Kotlin:

  1. Экономия памяти за счет уменьшения количества объектов, которые создаются в программе.
  2. Улучшение производительности за счет уменьшения количества операций копирования объектов.
  3. Улучшение безопасности за счет возможности установки ограничений на значения свойств value класса.

При использовании value классов необходимо учитывать следующие ограничения и условия:

  1. Класс должен быть помечен аннотацией @JvmInline, чтобы быть оптимизированным компилятором.

  2. Value класс не может иметь перегруженных конструкторов или конструкторов без параметров.

  3. Свойства value класса должны быть объявлены как val и должны быть установлены в конструкторе.

  4. Value класс не может быть наследником или наследоваться от другого класса.

  5. Value класс может наследоваться от интерфейсов.

  6. Value класс не может быть аннотирован как open, abstract, inner или sealed.

Пример Value класса:

Подробнее: kotlinlang.ru, tech-geek.ru, habr.com.

Что такое деструктурирующее объявление? Что нужно сделать, чтобы иметь возможность использовать его для своего класса? Какие проблемы могут возникнуть с таким объявлением? ↑↑↑

Какая польза от typealias? Во что он компилируется? ↑↑↑

Можно ли использовать алиасы для функциональных типов?

  • Подменяется на базовый тип
  • Не вводят новые типы
  • Эквивалентны соответствующим базовым типам
  • Можно использовать для функциональных типов

Какие коллекции есть в Kotlin?

List

Set

Map

Sequences

⚠️ **GitHub.com Fallback** ⚠️