2099 просмотров
от 9 февраля 2024
Kotlin

Вопросы и ответы с собеседований по Kotlin

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

1

Преимущества 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 в определение класса, затем компилятор сделает все сам. Синглтоны на уровне языка (Object) В Java все должно объявляться внутри класса. Но в Kotlin все иначе. Компоненты могут объявляться за пределами класса, и это автоматически делает их статическими. Поэтому нам не требуется ключевое слово static. В Java статические члены обрабатываются не так, как члены-объекты. Это означает, что для статических членов нам недоступны такие вещи, как реализация интерфейса, помещение экземпляра в ассоциативный список (map) или передача его в качестве параметра методу, который принимает объект. В Kotlin static не является ключевым словом и вместо статических членов используются объекты-компаньоны, позволяющие преодолеть вышеуказанные ограничения. В этом и заключается преимущество. Даже если члены объектов-компаньонов выглядят как статические члены в других языках, во время выполнения они все равно остаются членами экземпляров реальных объектов и могут, например, реализовывать интерфейсы. Корутины Kotlin предоставляет возможность создавать дополнительные потоки, однако в нем также существуют т.н. корутины (сопрограммы), которые позволяют использовать меньше памяти в сравнении с обычным потоком, т.к. реализованы они без стека. Корутины же в свою очередь способны выполнять интенсивные и длительные задачи методом приостановления выполнения без блокировки потока и его последующего восстановления. Что в дальнейшем позволяет сгенерировать асинхронный код без блокирования, который при его выполнении не отличить от синхронного. К тому же, они генерируют эффектные доп. стили например async или await.

Комментарии
0/3000
2

Подход к исключениям (exception) в Java и Kotlin

Одним из ключевых отличий между Java и Kotlin является подход к исключениям. В Java есть два типа исключений: checked и unchecked. Checked исключения это те, которые должны быть обработаны в коде, иначе компилятор не позволит коду скомпилироваться. Unchecked исключения не требуют обработки в коде. С точки зрения исключений компилятор Kotlin отличается тем, что не различает checked и unchecked исключения. Все исключения — только unchecked, поэтому нет необходимости отлавливать или объявлять какие-либо исключения (вы самостоятельно принимаете решение, стоит ли их отлавливать и обрабатывать). Такой подход был выбран разработчиками Kotlin, чтобы упростить и ускорить процесс разработки, сократив количество бойлерплейта и улучшив читаемость кода. Однако, это может привести к тому, что некоторые ошибки могут быть упущены при компиляции и проявиться только во время выполнения программы. Некоторые разработчики считают, что отказ от checked исключений является недостатком Kotlin, поскольку это может привести к ошибкам, которые могут быть предотвращены на этапе компиляции в Java. Однако, другие разработчики утверждают, что этот подход снижает количество шаблонного кода и упрощает написание программ.

Комментарии
0/3000
3

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

В Kotlin нет статических методов, для этих целей обычно служит companion object. Для того чтобы метод из Java был представлен как статический используется аннотация @JvmStatic. Эта аннотация говорит компилятору Kotlin создать статический метод в байт-коде, что позволяет использовать методы так же, как в Java. Например, если у нас есть статический метод в Java: public class MyClass { public static int sum(int a, int b) { return a + b; } } Мы можем использовать этот метод в Kotlin, добавив аннотацию @JvmStatic: object MyClass { @JvmStatic fun sum(a: Int, b: Int): Int { return a + b } }

Комментарии
0/3000
4

Во что преобразуется internal в Java?

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

Комментарии
0/3000
5

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

Коллекция — это объект, содержащий в себе набор значений одного или различных типов, а также позволяющий к этим значениям обращаться и извлекать. Другими словами — это контейнер, в который вы можете помещать то, что вам нужно, а затем каким-либо образом с ним взаимодействовать. В Kotlin есть три типа коллекций: - List (список). Упорядоченная коллекция, в которой к элементам можно обращаться по их индексам. Идентичные элементы (дубликаты) могут встречаться в списке более одного раза. Примером списка является предложение: это группа слов, их порядок важен, и они могут повторяться. - Set (множество/набор). Неупорядоченная коллекция без повторяющихся значений. Примером множества является алфавит. - Map (словарь/ассоциативный список). Набор из пар "ключ-значение". Ключи уникальны и каждый из них соответствует ровно одному значению. В коллекции могут присутствовать повторяющиеся значения, но не повторяющиеся ключи. Пример — ID сотрудников и их должностей. Map не является наследником интерфейса Collection. Два типа интерфейсов, на основе которых создаются коллекции: - Неизменяемый (read-only) — дают доступ только для чтения (Set, List, Map, Collection). - Изменяемый (mutable) — расширяет предыдущий интерфейс и дополнительно даёт доступ к операциям добавления, удаления и обновления элементов коллекции (MutableSet, MutableList, MutableMap, MutableCollection). List Список — это упорядоченная коллекция. Каждое значение, помещённое в List, называется элементом, к которому можно обращаться по индексу. Индексы начинаются с "0" и заканчиваются индексом последнего элемента в списке — (list.size - 1). Список может содержать сколько угодно одинаковых элементов — дублей (в том числе null). val trees = listOf("Сосна", "Берёза", "Дуб") // неизменяемый список trees.add("Ясень") // ошибка val mutableTrees = mutableListOf("Сосна", "Берёза", "Дуб") // изменяемый список mutableTrees.add("Ясень") // всё ок По умолчанию в Kotlin реализацией List является ArrayList, его можно создать напрямую: val mutableTrees = ArrayList<String>() mutableTrees.add("Ясень") Set Множество — это коллекция уникальных элементов. Это означает, что Set не может содержать дублей. Обратите внимание, что null — это тоже уникальный элемент. val trees = setOf("Сосна", "Берёза", "Дуб") // неизменяемый сет trees.add("Ясень") // ошибка val mutableTrees = mutableSetOf("Сосна", "Берёза", "Дуб") // изменяемый сет mutableTrees.add("Сосна") // проигнорируется В отличие от списка, множество не заботится о порядке элементов. Это означает, что при использовании функций, зависящих от порядка элементов, вы можете получить непредсказуемый результат. Но это зависит от реализации сета. Например, по умолчанию реализацией Set является LinkedHashSet, который сохраняет порядок вставки элементов. val numbers = setOf(1, 2, 3, 4) // по умолчанию LinkedHashSet val numbersBackwards = setOf(4, 3, 2, 1) println(numbers.first() == numbersBackwards.first()) // false println(numbers.first() == numbersBackwards.last()) // true Но также существует HashSet, который не сохраняет порядок вставки элементов. И LinkedHashSet, и HashSet можно создать напрямую. val linkedHashSet = LinkedHashSet<String>() linkedHashSet.add("Дуб") val hashSet = HashSet<String>() hashSet.add("Ясень") Map Ассоциативные списки с уникальными ключами и любыми значениями (дубликаты ключей не допускаются, значения могут быть одинаковыми). Связь между ключами и значениями происходит через специальную форму вызова метода (инфиксный вызов) to. // числа - это ключи, деревья - значения val map = mapOf(1 to "Сосна", 2 to "Берёза", 3 to "Дуб") // неизменяемая мапа map.put(4, "Ясень") // ошибка val mutableMap = mutableMapOf(1 to "Сосна", 2 to "Берёза", 3 to "Дуб") // изменяемая мапа mutableMap.put(4, "Ясень") По умолчанию реализацией мапы является LinkedHashMap, который сохраняет порядок вставки записей. Есть ещё HashMap, которая не сохраняет порядок вставки записей. Обе реализации можно создать напрямую. val linkedHashMap = LinkedHashMap<Int, String>() linkedHashMap.put(1, "Дуб") val hashMap = HashMap<Int, String>() hashMap.put(1, "Ясень")

Комментарии
0/3000
6

Какая из коллекций не является имплементацией Collection?

Интерфейс Map не является наследником интерфейса Collection. Технически — это не коллекция, так как Map не наследуется от Collection. Но это также структура для хранения данных и ее всегда изучают и рассматривают вместе с коллекциями. В разговоре вполне нормально называть Map коллекцией.

Комментарии
0/3000
7

Что такое sequences и чем они отличаются от коллекций?

Sequences или последовательности — ещё один тип контейнера в Kotlin, но он не является коллекцией. Последовательности очень похожи на коллекции, они предоставляют те же функции. Ключевая разница в том, что они применяют другой подход с многоэтапной обработкой элементов (например, когда вы последовательно вызываете некую цепочку вызовов к коллекции). Последовательность — это итерируемый тип, с которым можно работать, не создавая ненужных промежуточных коллекций, выполняя все применимые операции над каждым элементом перед переходом к следующему. Отличия коллекции от последовательности: Если обработка Iterable состоит из нескольких шагов, то они выполняются немедленно: при завершении обработки каждый шаг возвращает свой результат — промежуточную коллекцию. Следующий шаг выполняется для этой промежуточной коллекции. Sequence же по возможности выполняет обработку "лениво" — фактически вычисления происходят только тогда, когда запрашивается результат выполнения всех шагов. Iterable завершает каждый шаг для всей коллекции, а затем переходит к следующему шагу. Sequence выполняет все шаги один за другим для каждого отдельного элемента. Iterable могут занимать больше памяти, чем последовательности, так как они вычисляют все элементы сразу и хранят их в памяти. Sequence вычисляют элементы при необходимости и не хранят все элементы в памяти. Зачем вообще нужны Sequences? Для оптимизации производительности в работе с большими коллекциями (от 1000). Фишка в том, что значения в таких коллекциях создаются только по мере необходимости, не инициализируя их заранее. Из-за этого нет доступа к содержимому по индексу, а также не контролируется размер. Последовательности позволяют избежать создания промежуточных результатов для каждого шага, тем самым повышая производительность всей цепочки вызовов. Однако "ленивый" характер последовательностей добавляет некоторые накладные расходы, которые могут быть значительными при обработке небольших коллекций или при выполнении более простых вычислений. Следовательно, вы должны рассмотреть, а затем самостоятельно решить, что вам подходит больше — Sequence или Iterable.

Комментарии
0/3000
8

Расскажите про проверку на равенство == и equals()

1. Проверка на равенство в Java Структурное равенство (значение) — метод equals(). Ссылочное равенство — оператор ==: — примитивные типы данных: сравнивает значения переменных — ссылочные типы данных (объекты, массивы): сравнивает ссылки 2. Проверка на равенство в Kotlin Структурное равенство (значение) — оператор == (проверка через equals()) Ссылочное равенство — оператор ===: — примитивные типы данных: сравнивает значения переменных — ссылочные типы данных (объекты, массивы): сравнивает ссылки 3. Разница == с Java Структурное равенство (значение) — оператор == в Kotlin это equals() в Java, т.е. в Kotlin строки можно всегда сравнивать через ==. Ссылочное равенство — оператор === в Kotlin это == в Java.

Комментарии
0/3000
9

Что такое анонимные классы и объекты?

Анонимный класс — это класс, которые явно не объявлен с помощью class, наследуется от заданного класса или реализует заданный интерфейс. Анонимный класс не всегда является синглтоном. Анонимный класс создается каждый раз при вызове соответствующего конструктора и используется только в контексте, где был создан. При этом каждый экземпляр анонимного класса имеет свое уникальное состояние и может отличаться от других экземпляров того же анонимного класса. В Kotlin анонимный класс создается следующим образом: val obj = object : SuperClassOrInterface() { // implementation here } Объекты анонимных классов полезны для одноразового использования. Экземпляры анонимных классов называют анонимными объектами, потому что они объявляются выражением, а не именем. Анонимный объект начинается с ключевого слова object. - можно задавать свойства, функции, блоки инициализации; - можно наследоваться от других классов и реализовывать интерфейсы; - нельзя создавать конструкторы (как основные, так и вторичные). Ключевое слово object позволяет одновременно объявить класс и создать его экземпляр (т.е. объект). При этом применять его можно по-разному: - object Name — это объявление объекта (оbject declaration), реализация паттерна Singleton; - companion object — это объект-компаньон внутри класса (также Singleton); - object — это объект-выражение (анонимный объект/object expression), не Singleton.

Комментарии
0/3000
10

Расскажите про объявление объекта

Объявляется объект при помощи ключевого слова object, после которого следует имя объекта. Файл, содержащий только object представляет из себя Singleton, т.е. будет создан только один экземпляр этого класса. Пример: object One { val cats = arrayListOf<Cat>() fun callCat() { for (cat in cats) { ... } } } Можно обращаться к методам и свойствам класса через имя объекта: One.cats.add(Cat(...)) One.callCat() Инициализация объявления объекта потокобезопасна и выполняется при первом доступе (лениво).

Комментарии
0/3000
11

Что такое companion object?

Объекты можно объявлять внутри класса, при этом нет каких-либо ограничений по их количеству. Но только один объект можно пометить ключевым словом companion object в рамках одного класса. Синглтон-свойство companion object достигается за счет того, что он создается внутри класса в качестве статического поля. Он будет инициализирован при первом обращении к нему или при создании первого экземпляра класса, в котором он объявлен. Важно отметить, что companion object будет инициализирован первым, а затем уже будет создан экземпляр класса: class MyClass { init { // Выполняется всегда после инициализации companion object } companion object { init { // Выполняется всегда перед блоком init содержащего класса } } } val myClass = MyClass() Такому объекту можно не указывать свое имя, и обращаться к методам и свойствам объекта через имя содержащего его класса без явного указания имени объекта. class SomeClass { companion object { fun create() } } val someClass = SomeClass.create() Компилируется в public static final class на Java. Работает подобно ключевому слову static в Java.

Комментарии
0/3000
12

Что такое объект-выражение?

Объект-выражение — это выражение, которое "на ходу" создает анонимный объект. Для объекта-выражения не указывается имя! Если же объекту всё-таки требуется имя, то его можно сохранить в переменной: 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.

Комментарии
0/3000
13

В чем разница между анонимным и объявляемым объектом?

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

Комментарии
0/3000
14

Зачем нужна аннотация @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(); } }

Комментарии
0/3000
15

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

Комментарии
0/3000
16

Способы проверки значения на null

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")

Комментарии
0/3000
17

От какого класса унаследованы все классы в 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 }

Комментарии
0/3000
18

Чем 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: Класс Any в Kotlin является не только базовым классом для пользовательских классов, но также и супертипом для всех не-nullable типов данных, включая примитивные. В то время как в Java, класс Object является базовым классом только для пользовательских классов. Класс Any в Kotlin также имеет nullable версию Any?, которая является супертипом для всех nullable типов данных в Kotlin. В то время как в Java, класс Object не имеет nullable версии.

Комментарии
0/3000
19

Что такое Unit?

Тип Unit в Kotlin выполняет ту же функцию, что и void в Java. Возвращаемый тип можно не указывать, если функция ничего не возвращает. По умолчанию там будет Unit: fun knockKnock() { println("Who’s there?") } // то же самое, но с указанным типом Unit fun knockKnock(): Unit = println("Who’s there?")

Комментарии
0/3000
20

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

В стандартной библиотеке Kotlin Unit определён как объект, наследуемый от Any и содержащий единственный метод, переопределяющий toString(): public object Unit { override fun toString() = "kotlin.Unit" } Unit является синглтоном (ключевое слово object). Unit ничего не возвращает, а метод toString всегда будет возвращать kotlin.Unit. При компиляции в java-код Unit всегда будет превращаться в void.

Комментарии
0/3000
21

Что такое Nothing?

Nothing является типом, который полезен при объявлении функции, которая ничего не возвращает и не завершается. Примеры: - функция, которая выбрасывает exception или в которой запущен бесконечный цикл; - функция TODO()public inline fun TODO(): Nothing = throw NotImplementedError(); - в тестах есть функция с именем fail, которая выдает исключение с определенным сообщением: fun fail(message: String): Nothing { throw IllegalStateException(message) }

Комментарии
0/3000
22

Подтип всех типов в Kotlin

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

Комментарии
0/3000
23

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

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

Комментарии
0/3000
24

Есть ли аналог Nothing в Java?

Тип Nothing является особенным, поскольку в Java ему нет аналогов. Действительно, каждый ссылочный тип Java, включая java.lang.Void, принимает в качестве значения null, а Nothing не принимает даже этого. Таким образом, этот тип не может быть точно представлен в мире Java. Вот почему Kotlin генерирует необработанный тип, в котором используется аргумент типа Nothing: fun emptyList(): List<Nothing> = listOf() // is translated to // List emptyList() { ... }

Комментарии
0/3000
25

Модификаторы доступа в Kotlin

Классы, объекты, интерфейсы, конструкторы, функции, свойства и их сеттеры могут иметь модификаторы доступа. Геттеры всегда имеют ту же видимость, что и свойства, к которым они относятся. Модификаторы доступа — это ключевые слова, с помощью которых можно задать область действия данных. Они позволяют регулировать уровень доступа к различным частям кода. Локальные переменные, функции и классы не могут иметь модификаторов доступа. В Kotlin есть четыре модификатора доступа: private, protected, internal и public. Если модификатор явно не указан, то присваивается значение по умолчанию — public. Private — доступ к членам класса только в пределах самого класса. То есть, поля и методы с модификатором private недоступны из других классов и даже из наследников. Protected — доступ к членам класса только в пределах класса и его наследников. То есть, поля и методы с модификатором protected доступны из класса и его наследников, но не из других классов. Internal — доступ к членам модуля (module). Модуль — это набор файлов, компилирующихся вместе, поэтому все классы, объявленные внутри модуля, могут иметь доступ к членам с модификатором internal. Public — не ограничивает доступ к членам класса. Поля и методы с модификатором public доступны из любого места программы, включая другие модули.

Комментарии
0/3000
26

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

var — это изменяемая переменная. После инициализации мы можем изменять данные, хранящиеся в переменной. Переменные val и const val доступны только для чтения — это неизменяемые переменные. val — константа времени выполнения, т.е. значение можно назначить во время выполнения программы. const val — константа времени компиляции, т.к. значения константам присваивается при компиляции (в момент, когда программа компилируется). В отличие от val, значение const val должно быть известно во время компиляции. Особенности const val: - могут получать значение только базовых типов: Int, Double, Float, Long, Short, Byte, Char, String, Boolean. - объявляются в глобальной области видимости, то есть за пределами функции main() или любой другой функции. - нет пользовательского геттера. Как стоит объявлять свои константы в Kotlin — при помощи companion object или вне класса? На самом деле оба эти подхода приемлемы. Однако, использование companion object может быть излишним: компилятор Kotlin преобразует companion object во вложенный класс. Слишком много кода для простой константы. Если вам не требуется поведение, специфичное для companion object, объявляйте константы вне класса, так как это будет способствовать более эффективному байт-коду. Да и сам синтаксис объявления констант вне класса более чистый и читабельный.

Комментарии
0/3000
27

Расскажите про свойства, методы 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 методы.

Комментарии
0/3000
28

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

В Kotlin свойство (property) — это абстракция над полями (fields), которая позволяет обращаться к значению переменной через методы геттера и сеттера, вместо прямого доступа к полю. Field — это переменная, которая содержит значение и может быть доступна напрямую или через геттер/сеттер. Пример определения свойства с геттером и сеттером в классе: class Person { var name: String = "" get() = field.toUpperCase() // возвращает значение поля name в верхнем регистре set(value) { field = value.trim() // устанавливает значение поля name без начальных и конечных пробелов } } В данном примере свойство name содержит поле, которое может быть доступно напрямую только внутри класса, и методы геттера и сеттера, которые позволяют получать и изменять значение свойства через специальные методы.

Комментарии
0/3000
29

Отложенная и ленивая инициализация свойств

Отложенная и ленивая инициализация свойств — это механизмы, которые позволяют отложить инициализацию переменных до момента их первого использования. Оба варианта позволяют экономить ресурсы, т.к. избегают необходимости создания объектов при инициализации класса. 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, мы просто отключаем одну из проверок компилятора, которая гарантирует, что программа не обращается ни к одной переменной до того, как она получит значение. Вместо этого мы обещаем сделать эту проверку сами.

Комментарии
0/3000
30

Что такое делегированные свойства?

Делегированные свойства (Delegated properties) — это свойства, которые не хранят своё значение напрямую, а делегируют это значение другому объекту, который реализует интерфейс Delegate. При доступе к свойству, его значение запрашивается у делегата, который может выполнить какую-то дополнительную логику, а затем вернуть требуемое значение. Пример: class Example { var p: String by Delegate() } Ключевое слово by используется для обозначения свойств, методы чтения и записи которых реализованы другим объектом, который называют делегатом. В Kotlin существуют несколько встроенных делегатов для работы с делегированными свойствами: lazy() — позволяет создавать лениво инициализированные свойства observable() — позволяет реагировать на изменения свойства vetoable() — позволяет отклонять изменения значения свойства на основе заданного условия notNull() — гарантирует, что свойство не будет иметь значение null map() — позволяет хранить значения свойств в словаре (Map) Кроме того, в Kotlin можно создавать свои собственные делегаты, реализуя интерфейс ReadOnlyProperty или ReadWriteProperty. Это дает возможность создавать кастомные поведения для свойств, например, кеширование значений или логирование операций чтения/записи.

Комментарии
0/3000
31

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

Чтобы написать кастомный делегат, нужно определить класс, который реализует интерфейс 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. При установке значения свойства или получении его значения будут вызываться соответствующие методы нашего делегата, и мы увидим соответствующие сообщения в консоли.

Комментарии
0/3000
32

Типы конструкторов в Kotlin

Свойств у класса может быть столько, сколько ему нужно. Но все они должны быть инициализированы при создании экземпляра этого класса. Поэтому для удобства был придуман конструктор — специальный блок кода, который вызывается при создании экземпляра класса. Ему передаются необходимые значения, которые потом используются для инициализации свойств. Класс в Kotlin может иметь основной конструктор и один или более вторичных конструкторов. У класса может и не быть конструктора, но 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)

Комментарии
0/3000
33

Блок инициализации (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 { ... } // и т.д. } Блок инициализации может быть добавлен, даже если у класса нет основного конструктора. В этом случае его код будет выполнен раньше кода вторичных конструкторов.

Комментарии
0/3000
34

Что такое Data классы?

Data класс предназначен исключительно для хранения каких-либо данных. Основное преимущество: для параметров, переданных в основном конструкторе автоматически будут переопределены методы toString(), equals(), hashCode(), copy(). Также для каждой переменной, объявленной в основном конструкторе, автоматически генерируются функции componentN(), где N — номер позиции переменной в конструкторе. Благодаря наличию вышеперечисленных функций внутри data класса мы исключаем написание шаблонного кода.

Комментарии
0/3000
35

Что такое мульти-декларации (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

Комментарии
0/3000
36

Что делает функция 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 на отдельные переменные.

Комментарии
0/3000
37

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

- Класс должен иметь хотя бы одно свойство, объявленное в основном конструкторе. - Все параметры основного конструктора должны быть отмечены val или var (рекомендуется val). - Классы данных не могут быть abstract, open, sealed или inner.

Комментарии
0/3000
38

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

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

Комментарии
0/3000
39

Что такое абстрактные классы?

Абстрактный класс — это класс, представляющий из себя "заготовку" для целого семейства классов, который описывает для них общий шаблон поведения. Экземпляр такого класса не может быть создан. Абстрактному классу не нужен модификатор 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())

Комментарии
0/3000
40

Что такое интерфейс?

Интерфейс — это совокупность методов и правил, которые определяют поведение класса или общее поведение для группы независимых друг от друга классов. Интерфейсы похожи на абстрактные классы тем, что нельзя создать их экземпляры и они могут определять абстрактные или конкретные функции и свойства. Отличие в том, что интерфейсу не важна связь "родитель-наследник", он задаёт лишь правила поведения. Интерфейсы в 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} не плодоносит.") }

Комментарии
0/3000
41

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

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

Комментарии
0/3000
42

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

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

Комментарии
0/3000
43

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

По умолчанию, классы в 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() }

Комментарии
0/3000
44

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

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" }

Комментарии
0/3000
45

Что такое 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 }

Комментарии
0/3000
46

Что такое 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, чтобы вычислить их площади.

Комментарии
0/3000
47

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

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

Комментарии
0/3000
48

Что такое 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) класс, который может обращаться к компонентам внешнего класса.

Комментарии
0/3000
Смежные категории
Git
20 вопросов
Вопросы и ответы с собеседований по Git
1784 просмотра
Computer Science
15 вопросов
Вопросы и ответы с собеседований по DDD
1527 просмотров
Computer Science
28 вопросов
Объяснение паттернов проектирования с примерами
1424 просмотра
Computer Science
11 вопросов
Вопросы и ответы про интернет-протоколы
1466 просмотров
Computer Science
12 вопросов
Вопросы с собеседований про операционные системы
1062 просмотра
Java
53 вопроса
Вопросы и ответы с собеседований по Java
1358 просмотров
Рекомендуем
Computer Science
12 вопросов
Вопросы с собеседований про операционные системы
1062 просмотра
Git
20 вопросов
Вопросы и ответы с собеседований по Git
1784 просмотра
Computer Science
15 вопросов
Вопросы и ответы с собеседований по DDD
1527 просмотров
Computer Science
28 вопросов
Объяснение паттернов проектирования с примерами
1424 просмотра
Computer Science
11 вопросов
Вопросы и ответы про интернет-протоколы
1466 просмотров
Базы данных
60 вопросов
Вопросы и ответы с собеседований по SQL
2378 просмотров
Другие разделы

Лента

Активность пользователей Девстанции

Перейти к ленте

Лидеры

Рейтинг самых результативных пользователей сообщества

Перейти к лидерам

Треды

Общение по интересам и связь с разработчиками

Перейти к тредам

Задачи

Решение алгоритмических задач с собеседований

Перейти к задачам

Вопросы

Ответы на вопросы с технических собеседований

Вы находитесь здесь

Викторины

Интерактивные викторины по вопросам с собеседований

Перейти к викторинам
Мы в Telegram
Новости проекта, общение с разработчиками, общение по интересам - присоединяйтесь!