Вопросы и ответы для собеседования по Kotlin. Часть 1
Вопросы и ответы для собеседования по Kotlin. Часть 2
Вопросы и ответы для собеседования по Kotlin. Часть 3 — вы находитесь здесь
Вопросы и ответы для собеседования по Kotlin. Часть 4
Список тем и вопросов:
1. Классы и интерфейсы
2. Коллекции и последовательности (Sequences)
![](https://habrastorage.org/getpro/habr/upload_files/ba3/94c/742/ba394c742e1cee6fbaf17ffa02af0e09.png)
Что такое абстрактные классы и интерфейсы?
Абстрактные классы и интерфейсы используются для описания абстрактных концепций, не имеющих реализации.
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:
Экономия памяти за счет уменьшения количества объектов, которые создаются в программе.
Улучшение производительности за счет уменьшения количества операций копирования объектов.
Улучшение безопасности за счет возможности установки ограничений на значения свойств
value
класса.
При использовании value
классов необходимо учитывать следующие ограничения и условия:
Класс должен быть помечен аннотацией
@JvmInline
, чтобы быть оптимизированным компилятором.Value
класс не может иметь перегруженных конструкторов или конструкторов без параметров.Класс должен иметь одно свойство (только
val
), инициализированное в основном конструкторе.Value
класс не может быть наследником или наследоваться от другого класса.Value
класс может наследоваться от интерфейсов.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.
Два типа интерфейсов, на основе которых создаются коллекции:
Неизменяемый (read-only) — дают доступ только для чтения (
Set
,List
,Map
,Collection
).Изменяемый (mutable) — расширяет предыдущий интерфейс и дополнительно даёт доступ к операциям добавления, удаления и обновления элементов коллекции (
MutableSet
,MutableList
,MutableMap
,MutableCollection
).
![](https://habrastorage.org/getpro/habr/upload_files/471/232/032/4712320326f3181bc5e9ef0ea07a943f.png)
Функции коллекций в доске 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, но он не является коллекцией. Последовательности очень похожи на коллекции, они предоставляют те же функции. Ключевая разница в том, что они применяют другой подход с многоэтапной обработкой элементов (например, когда вы последовательно вызываете некую цепочку вызовов к коллекции).
Последовательность — это итерируемый тип, с которым можно работать, не создавая ненужных промежуточных коллекций, выполняя все применимые операции над каждым элементом перед переходом к следующему.
Отличия коллекции от последовательности:
Если обработка
Iterable
состоит из нескольких шагов, то они выполняются немедленно: при завершении обработки каждый шаг возвращает свой результат — промежуточную коллекцию. Следующий шаг выполняется для этой промежуточной коллекции.Sequence
же по возможности выполняет обработку "лениво" — фактически вычисления происходят только тогда, когда запрашивается результат выполнения всех шагов.Iterable
завершает каждый шаг для всей коллекции, а затем переходит к следующему шагу.Sequence
выполняет все шаги один за другим для каждого отдельного элемента.Iterable
могут занимать больше памяти, чем последовательности, так как они вычисляют все элементы сразу и хранят их в памяти.Sequence
вычисляют элементы при необходимости и не хранят все элементы в памяти.
Зачем вообще нужны Sequences?
Для оптимизации производительности в работе с большими коллекциями (от 1000). Фишка в том, что значения в таких коллекциях создаются только по мере необходимости, не инициализируя их заранее. Из-за этого нет доступа к содержимому по индексу, а также не контролируется размер.
Последовательности позволяют избежать создания промежуточных результатов для каждого шага, тем самым повышая производительность всей цепочки вызовов. Однако "ленивый" характер последовательностей добавляет некоторые накладные расходы, которые могут быть значительными при обработке небольших коллекций или при выполнении более простых вычислений. Следовательно, вы должны рассмотреть, а затем самостоятельно решить, что вам подходит больше — Sequence
или Iterable
.
Статья о разнице между Sequences
и Iterable
на примере сортировки карандашей (с разъяснениями и картинками): typealias.com
![](https://habrastorage.org/getpro/habr/upload_files/23d/a0e/fba/23da0efba9d5fb49921b3e3c760f4d1d.png)
Создать последовательность можно через функцию sequenceOf()
:
val cats = sequenceOf("Барсик", "Мурзик", "Рыжик", "Васька")
Если у вас есть уже готовые списки List
или множества Set
, то их можно преобразовать в последовательность через asSequence()
.
val cats = listOf("Барсик", "Мурзик", "Рыжик", "Васька")
val catsSequence = cats.asSequence()
Подробнее о Sequences: alexanderklimov.ru, bimlibik.github.io, metanit.com
—————— ↑↑↑ к списку вопросов ↑↑↑ ——————
Промежуточные (intermediate) и терминальные (terminal) операции в Sequences
Sequence
представляет собой последовательность элементов, которые можно обрабатывать по одному или несколько штук сразу. Обработка элементов Sequence
происходит с помощью функций высшего порядка, которые называются операциями.
Операции над Sequence
можно разделить на две категории: промежуточные (intermediate) и терминальные (terminal).
Промежуточные операции (intermediate) — это операции, которые возвращают новую Sequence
.
Они не выполняются немедленно, а лишь формируют новую последовательность элементов на основе исходной. Промежуточные операции не приводят к запуску вычислений, а готовят данные для последующих операций. Примеры:
filter(predicate: (T) -> Boolean)
: фильтрует элементы по заданному условию и возвращает новуюSequence
map(transform: (T) -> R)
: преобразует каждый элемент в новый элемент типаR
и возвращает новуюSequence
sortedBy(selector: (T) -> R?)
: сортирует элементы по заданному ключу и возвращает новуюSequence
Терминальные операции (terminal) — это операции, которые выполняются немедленно и возвращают результат (не Sequence
).
Терминальные операции могут быть вызваны только после всех промежуточных операций, так как они завершают последовательность и начинают вычисление результатов на основе всей последовательности, полученной после выполнения всех промежуточных операций. Если же терминальная операция вызывается до выполнения всех промежуточных операций, то она не будет иметь доступа к полной последовательности и вернет неполный результат. Примеры:
toList()
: преобразуетSequence
в списокtoSet()
: преобразуетSequence
в множествоcount()
: возвращает количество элементов вSequence
forEach(action: (T) -> Unit)
: выполняет действие для каждого элементаSequence
ВАЖНО: вычисления запускаются только при вызове терминальной функции (до этого момента никаких вычислений не производится).
Вопросы и ответы для собеседования по Kotlin. Часть 1
Вопросы и ответы для собеседования по Kotlin. Часть 2
Вопросы и ответы для собеседования по Kotlin. Часть 3 — вы находитесь здесь
Вопросы и ответы для собеседования по Kotlin. Часть 4