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

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

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

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

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

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

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

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

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

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

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

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

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

...

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

...

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

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

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

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

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

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

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

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

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

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

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

...

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

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

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

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

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

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

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

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

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

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

  override fun startPhotosynthesis() {
    ...
  }
}

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

  override val bloom = false
}

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

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

  fun startPhotosynthesis() {
    ...
  }
}

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

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

interface Cultivable : Fruitable {
  ...

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

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

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

...

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

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

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

  • Вам нужно создать более конкретную версию класса? Создайте подкласс этого класса и добавьте недостающее поведение.

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

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

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

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

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

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

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

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

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

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

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

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

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

open class Fraction {
  ...
}

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

open class Fraction {

  open fun toAttack() {
    ...
  }

}

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

open class Fraction {

  open val name: String = "default"

  open fun toAttack() {
    ...
  }

}

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

open class Fraction {

  open val name: String = "default"

  fun toAttack() {
    ...
  }

}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

enum class ColorType {
  RED,
  BLUE,
  GREEN
}

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

var color: ColorType
color = ColorType.RED

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

enum class ColorType {
  RED,
  BLUE,
  GREEN;

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

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

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

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

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

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

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

    TALKING {
        override fun signal() = WAITING
    };

    abstract fun signal(): ProtocolState
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В отли­чие от обыч­ного клас­са, value класс инлай­новый. Он не будет существо­вать в резуль­тиру­ющем байт‑коде при­ложе­ния. Ком­пилятор раз­вернет все value клас­сы и будет исполь­зовать вмес­то них сох­ранен­ные внут­ри зна­чения.

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

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

  2. Улучшение производительности за счет уменьшения количества операций копирования объектов.

  3. Улучшение безопасности за счет возможности установки ограничений на значения свойств value класса.

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

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

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

  3. Класс должен иметь одно свойство (только val), инициализированное в основном конструкторе.

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

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

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

Сравнение и преимущества value над data классами и typealias подробно описаны в статье: https://habr.com/ru/post/691152

Краткие выводы из статьи про классы значений:

  • Делают объявление переменных и сигнатуры функций более выразительными.

  • Сохраняют производительность примитивных типов.

  • Несовместимы по присваиванию с их базовым типом, предотвращая пользователя от совершения глупых вещей.

  • Поддерживают множество особенностей data классов, таких как конструкторы, init, методы и даже дополнительные свойства (но только через геттеры).

По словам автора (точнее переводчика оригинала): единственное оставшееся применение для data классов — это когда вам нужно обернуть несколько параметров. Value классы ограничены одним параметром в их конструкторе.

Простой пример использования value класса:

@JvmInline
value class Age(val age: Int) {
    init {
        require(age >= 0) { "Age cannot be negative" }
    }
}

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

fun main() {
    val person = Person("Alice", Age(30))
    println("Name: ${person.name}, Age: ${person.age.age}")
}

В этом примере Age — это value класс, описывающий возраст человека. Он имеет один параметр age, который передается в конструктор. Затем Age используется в качестве свойства в классе Person. Таким образом, мы можем гарантировать, что возраст всегда будет неотрицательным, потому что в конструкторе Age используется блок init, проверяющий, что переданный возраст не меньше нуля.

Возможно, что у вас возникнет вопрос: "Так можно же заменить value класс Age на data класс и все будет работать также. В чем тогда преимущество в применении здесь value класса?"

Преимущество использования value класса здесь заключается в том, что он позволяет явно выразить намерение разработчика создать класс, который будет использоваться в качестве значения. Это может помочь в дальнейшей оптимизации кода, так как компилятор может производить дополнительные оптимизации для value классов, которые недоступны для обычных или data классов. Также использование value класса Age с аннотацией @JvmInline позволяет избежать создания объекта при обращении к значению возраста, что может ускорить выполнение кода. Несмотря на то, что в данном примере это не так очевидно, но для более сложных и вычислительно затратных операций это может оказаться значительным преимуществом.

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

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

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

Typealias — это механизм создания синонимов (псевдонимов) для существующих типов. То есть, можно создать новое имя для уже существующего типа данных.

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

typealias NodeSet = Set<Network.Node>
typealias FileTable<K> = MutableMap<K, MutableList<File>>

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

К примеру, если в проекте используется много Map<String, String> и вместо этого вы хотите использовать более описательное название, например Properties, вы можете определить новый тип для Map<String, String> с помощью следующего кода:

typealias Properties = Map<String, String>

Теперь вместо использования Map<String, String> можно использовать Properties для обозначения одного и того же типа данных. Таким образом, код становится более читаемым и понятным.

Во что компилируется typealias?

Typealias не создает новый тип данных, а только создает псевдоним для существующего типа. При компиляции кода, все typealias заменяются на соответствующий тип, поэтому typealias не приводит к увеличению размера кода.

Например, typealias IntPredicate = (Int) -> Boolean при компиляции будет заменено на (Int) -> Boolean, то есть функцию, принимающую значение типа Int и возвращающую значение типа Boolean.

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

Да, можно использовать typealias для функциональных типов в Kotlin. Например, вы можете создать псевдоним для типа функции, которая принимает два параметра типа Int и возвращает значение типа String, следующим образом:

typealias IntToString = (Int, Int) -> String

Это позволит вам использовать созданный псевдоним вместо полного объявления типа, то есть вместо:

fun processValues(f: (Int, Int) -> String) {
    // ...
}

можно использовать:

fun processValues(f: IntToString) {
    // ...
}

Как и в случае с другими typealias, компилятор Kotlin просто заменяет псевдоним на соответствующий тип при компиляции кода.

Подробнее: kotlinlang.ru, hr-vector.com

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

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

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

  • List (список). Упорядоченная коллекция, в которой к элементам можно обращаться по их индексам. Идентичные элементы (дубликаты) могут встречаться в списке более одного раза. Примером списка является предложение: это группа слов, их порядок важен, и они могут повторяться.

  • Set (множество/набор). Неупорядоченная коллекция без повторяющихся значений. Примером множества является алфавит.

  • Map (словарь/ассоциативный список). Набор из пар "ключ-значение". Ключи уникальны и каждый из них соответствует ровно одному значению. В коллекции могут присутствовать повторяющиеся значения, но не повторяющиеся ключи. Пример — ID сотрудников и их должностей. Map не является наследником интерфейса Collection.

Два типа интерфейсов, на основе которых создаются коллекции:

  1. Неизменяемый (read-only) — дают доступ только для чтения (Set, List, Map, Collection).

  2. Изменяемый (mutable) — расширяет предыдущий интерфейс и дополнительно даёт доступ к операциям добавления, удаления и обновления элементов коллекции (MutableSet, MutableList, MutableMap, MutableCollection).

Функции коллекций в доске Trello.

Подробнее о коллекциях: tproger.ru и kotlinlang.ru

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

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, "Ясень")

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

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

Интерфейс Map не является наследником интерфейса Collection.

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

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

Sequences и их отличия от коллекций

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

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

Отличия коллекции от последовательности:

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

  2. Iterable завершает каждый шаг для всей коллекции, а затем переходит к следующему шагу. Sequence выполняет все шаги один за другим для каждого отдельного элемента.

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

Зачем вообще нужны Sequences?

Для оптимизации производительности в работе с большими коллекциями (от 1000). Фишка в том, что значения в таких коллекциях создаются только по мере необходимости, не инициализируя их заранее. Из-за этого нет доступа к содержимому по индексу, а также не контролируется размер.

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

Статья о разнице между Sequences и Iterable на примере сортировки карандашей (с разъяснениями и картинками): typealias.com

Создать последовательность можно через функцию sequenceOf():

val cats = sequenceOf("Барсик", "Мурзик", "Рыжик", "Васька")

Если у вас есть уже готовые списки List или множества Set, то их можно преобразовать в последовательность через asSequence().

val cats = listOf("Барсик", "Мурзик", "Рыжик", "Васька")
val catsSequence = cats.asSequence()

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

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

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