Вопросы и ответы для собеседования по Kotlin. Часть 1 - вы находитесь здесь
Вопросы и ответы для собеседования по Kotlin. Часть 2
Вопросы и ответы для собеседования по Kotlin. Часть 3 (скоро)

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

1. Kotlin и Java

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

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, появляется исключение в виде «NullPointerExpertion!».

Функции-расширения (Extensions)

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

Классы данных (data classes)

Разработчику на Java приходится писать много стандартного, но часто встречающегося кода (т.н. шаблонный код или boilerplate). В Kotlin же есть возможность создания специальных классов для определения полей для хранения данных, конструктора, функций сеттеров и геттеров для каждого поля, и функций Hashcode(), toString() и equals(). Для этого достаточно добавить data в определение класса, затем компилятор сделает все сам.

Функции высшего порядка и Лямбда-выражения

Функции высшего порядка (higher-order function) — функция, которая может принимать в качестве параметра функцию или лямбда-выражение, или же может возвращать функцию как результат. Лямбда-выражения — это функция, которая способна передаваться как выражение, хоть и не является объявленной.

Синглтоны на уровне языка (Object)

В Java все должно объявляться внутри класса. Но в Kotlin все иначе. Компоненты могут объявляться за пределами класса, и это автоматически делает их статическими. Поэтому нам не требуется ключевое слово static. В Java статические члены обрабатываются не так, как члены-объекты. Это означает, что для статических членов нам недоступны такие вещи, как реализация интерфейса, помещение экземпляра в ассоциативный список (map) или передача его в качестве параметра методу, который принимает объект. В Kotlin static не является ключевым словом и вместо статических членов используются объекты-компаньоны, позволяющие преодолеть вышеуказанные ограничения. В этом и заключается преимущество. Даже если члены объектов-компаньонов выглядят как статические члены в других языках, во время выполнения они все равно остаются членами экземпляров реальных объектов и могут, например, реализовывать интерфейсы.

Корутины

Kotlin предоставляет возможность создавать дополнительные потоки, однако в нем также существуют т.н. корутины (сопрограммы), которые позволяют использовать меньше памяти в сравнении с обычным потоком, т.к. реализованы они без стека. Корутины же в свою очередь способны выполнять интенсивные и длительные задачи методом приостановления выполнения без блокировки потока и его последующего восстановления. Что в дальнейшем позволяет сгенерировать асинхронный код без блокирования, который при его выполнении не отличить от синхронного. К тому же, они генерируют эффектные доп. стили например async или await.

Зачем это нужно? Все просто. Процессы, которые проводят длительные операции сильно загружают процессор, вследствие чего вызывающий поток вплоть до полного завершения операции — блокируется. Android является однопоточным, из-за чего в подобных ситуациях блокируется основной поток, не давая возможности использовать пользовательский интерфейс. Также это помогает сократить размер кода. Java же решает эту проблему немного иначе, создавая фоновый поток основного интенсивного потока. Но наличие нескольких потоков и управление ими будет непростым и приводит к ошибкам в коде.

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

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

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

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

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

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

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

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.

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

—————— ↑↑↑ к списку вопросов ↑↑↑ ——————

Кратко о 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() { ... }

—————— ↑↑↑ к списку вопросов ↑↑↑ ——————

БОНУС:

1. Ключевые слова и операторы Kotlin: kotlinlang.ru
2. Темы по Kotlin: hr-vector.com
3. Шпаргалка по Kotlin: devhints.io

Вопросы и ответы для собеседования по Kotlin. Часть 1 - вы находитесь здесь
Вопросы и ответы для собеседования по Kotlin. Часть 2
Вопросы и ответы для собеседования по Kotlin. Часть 3 (скоро)

Комментарии (1)


  1. sergiodev
    00.00.0000 00:00

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

    Функции высшего порядка и Лямбда-выражения

    Сомнительное преимущество - в Java ещё с восьмой версии лямбды есть так-то