Вопросы и ответы для собеседования по Kotlin. Часть 1 — вы находитесь здесь
Вопросы и ответы для собеседования по Kotlin. Часть 2
Вопросы и ответы для собеседования по Kotlin. Часть 3
Вопросы и ответы для собеседования по Kotlin. Часть 4
Список тем и вопросов:
1. Kotlin и Java
2. Анонимные классы и объекты, object и companion object
Кратко про анонимные классы и объекты, object и companion object
Объявление объекта (object declaration), object как Singleton
Разница между анонимным и декларируемым (объявляемым) объектом
3. Null safety
4. Any
5. Unit
6. Nothing
Преимущества языка Kotlin перед Java
Код на Kotlin компактнее на 30-40%
Меньше кода = меньше ошибок, выше скорость разработки.
Безопасная работа с обнуляемыми переменными (Null Safety)
В отличие от Java, в Kotlin по умолчанию все типы являются non-nullable, то есть не могут принимать значение null. Присвоение или возврат null приведет к ошибке компиляции. Чтобы присвоить переменной значение null, в Kotlin необходимо явно пометить эту переменную как nullable (добавив после типа знак вопроса). В Java же при использовании ссылки на объект с указанным значением null, появляется исключение в виде «NullPointerException!».
Функции-расширения (Extensions)
Kotlin позволяет расширять класс путём добавления нового функционала без необходимости наследования от такого класса. Это реализовано с помощью специальных выражений, называемых расширения. Например, вы можете написать новые функции для класса из сторонней библиотеки, которую вы не можете изменить. Такие функции можно вызывать обычным способом, как если бы они были методами исходного класса. Этот механизм называется функцией расширения.
Классы данных (data classes)
Разработчику на Java приходится писать много стандартного, но часто встречающегося кода (т.н. шаблонный код или boilerplate). В Kotlin же есть возможность создания специальных классов для определения полей для хранения данных, конструктора, функций сеттеров и геттеров для каждого поля, и функций Hashcode(), toString() и equals(). Для этого достаточно добавить data в определение класса, затем компилятор сделает все сам.
Синглтоны на уровне языка (Object)
В Java все должно объявляться внутри класса. Но в Kotlin все иначе. Компоненты могут объявляться за пределами класса, и это автоматически делает их статическими. Поэтому нам не требуется ключевое слово static. В Java статические члены обрабатываются не так, как члены-объекты. Это означает, что для статических членов нам недоступны такие вещи, как реализация интерфейса, помещение экземпляра в ассоциативный список (map) или передача его в качестве параметра методу, который принимает объект. В Kotlin static не является ключевым словом и вместо статических членов используются объекты-компаньоны, позволяющие преодолеть вышеуказанные ограничения. В этом и заключается преимущество. Даже если члены объектов-компаньонов выглядят как статические члены в других языках, во время выполнения они все равно остаются членами экземпляров реальных объектов и могут, например, реализовывать интерфейсы.
Корутины
Kotlin предоставляет возможность создавать дополнительные потоки, однако в нем также существуют т.н. корутины (сопрограммы), которые позволяют использовать меньше памяти в сравнении с обычным потоком, т.к. реализованы они без стека. Корутины же в свою очередь способны выполнять интенсивные и длительные задачи методом приостановления выполнения без блокировки потока и его последующего восстановления. Что в дальнейшем позволяет сгенерировать асинхронный код без блокирования, который при его выполнении не отличить от синхронного. К тому же, они генерируют эффектные доп. стили например async или await.
Дополнительно:
Выведение типа для переменных и свойств
Более гибкая работа с generics
—————— ↑↑↑ к списку вопросов ↑↑↑ ——————
Разница между Exception в Java и Kotlin
Одним из ключевых отличий между Java и Kotlin является подход к исключениям. В Java есть два типа исключений: checked и unchecked.
Checked исключения это те, которые должны быть обработаны в коде, иначе компилятор не позволит коду скомпилироваться. Unchecked исключения не требуют обработки в коде.
С точки зрения исключений компилятор Kotlin отличается тем, что не различает checked и unchecked исключения. Все исключения — только unchecked, поэтому нет необходимости отлавливать или объявлять какие-либо исключения (вы самостоятельно принимаете решение, стоит ли их отлавливать и обрабатывать).
Такой подход был выбран разработчиками Kotlin, чтобы упростить и ускорить процесс разработки, сократив количество бойлерплейта и улучшив читаемость кода. Однако, это может привести к тому, что некоторые ошибки могут быть упущены при компиляции и проявиться только во время выполнения программы.
Некоторые разработчики считают, что отказ от checked исключений является недостатком Kotlin, поскольку это может привести к ошибкам, которые могут быть предотвращены на этапе компиляции в Java. Однако, другие разработчики утверждают, что этот подход снижает количество шаблонного кода и упрощает написание программ.
Подробнее: code.tutsplus.com, habr.com
—————— ↑↑↑ к списку вопросов ↑↑↑ ——————
Как перенести статичные методы из 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
}
}
Подробнее: kotlinlang.ru
—————— ↑↑↑ к списку вопросов ↑↑↑ ——————
В какой модификатор преобразуется 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
—————— ↑↑↑ к списку вопросов ↑↑↑ ——————
Кратко про анонимные классы и объекты, 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
достигается за счет того, что он создается внутри класса в качестве статического поля. Он будет инициализирован при первом обращении к нему или при создании первого экземпляра класса, в котором он объявлен.
Важно отметить, что 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.
—————— ↑↑↑ к списку вопросов ↑↑↑ ——————
Объект-выражение (анонимный объект/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:
Класс
Any
в Kotlin является не только базовым классом для пользовательских классов, но также и супертипом для всех не-nullable типов данных, включая примитивные. В то время как в Java, классObject
является базовым классом только для пользовательских классов.Класс
Any
в Kotlin также имеет nullable версиюAny?
, которая является супертипом для всех nullable типов данных в Kotlin. В то время как в Java, классObject
не имеет nullable версии.
—————— ↑↑↑ к списку вопросов ↑↑↑ ——————
Какой тип находится на вершине иерархии типов в Kotlin?
Аналогично Object
в Java, к чему можно привести любой тип в Kotlin?
Правильным ответом будет Any?
.
Сам по себе класс Any
это почти аналог Object
, однако, благодаря поддержке nullable и не-nullable типов в Kotlin мы получили Any?
. Фактически, Any?
соответствует любому типу и null
, а Any
только любому типу.
Если по порядку:
Any
является корнем иерархии не-nullable типов.Any?
является корнем иерархии nullable типов.Так как
Any?
является супертипомAny
, тоAny?
находится в самом верху иерархии типов в Kotlin.
Картинка для понимания:
—————— ↑↑↑ к списку вопросов ↑↑↑ ——————
Кратко о 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
—————— ↑↑↑ к списку вопросов ↑↑↑ ——————
Назовите подтип всех типов в Kotlin
Nothing
в Kotlin — это т.н. bottom type, то есть он является подтипом любого другого типа. Наличие Nothing
в системе типов позволяет типизировано выражать то, что без него принципиально невозможно.
Bottom type — это тип, который не имеет значений и предназначен для обозначения невыполнимых ситуаций в программе.
—————— ↑↑↑ к списку вопросов ↑↑↑ ——————
Сколько существует 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() { ... }
—————— ↑↑↑ к списку вопросов ↑↑↑ ——————
БОНУС:
1. Ключевые слова и операторы Kotlin: kotlinlang.ru
2. Темы по Kotlin: hr-vector.com
3. Шпаргалка по Kotlin: devhints.io
Вопросы и ответы для собеседования по Kotlin. Часть 1 — вы находитесь здесь
Вопросы и ответы для собеседования по Kotlin. Часть 2
Вопросы и ответы для собеседования по Kotlin. Часть 3
Вопросы и ответы для собеседования по Kotlin. Часть 4