Вопросы и ответы для собеседования по Kotlin. Часть 2

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

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

1. Модификаторы доступа. Свойства. Делегаты. Конструкторы. Init блок.

  • Модификаторы доступа — private, protected, internal, public

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

  • Свойства, методы get и set

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

  • Отложенная и ленивая инициализация свойств (lateinit и by lazy)

  • Что такое делегированные свойства (Delegated properties)?

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

  • Конструкторы. Какие типы конструкторов вы знаете?

  • Блок инициализации (init блок)

2. Data класс

  • Расскажите о Data классах. Какие преимущества они имеют?

  • Что такое мульти-декларации (destructuring declarations)?

  • Что делает функция componentN()?

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

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

Модификаторы доступа — private, protected, internal, public

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

В Kotlin есть четыре модификатора доступа: private, protected, internal и public.
Если модификатор явно не указан, то присваивается значение по умолчанию — public.

  • Private — доступ к членам класса только в пределах самого класса. То есть, поля и методы с модификатором private недоступны из других классов и даже из наследников.

  • Protected — доступ к членам класса только в пределах класса и его наследников. То есть, поля и методы с модификатором protected доступны из класса и его наследников, но не из других классов.

  • Internal — доступ к членам модуля (module). Модуль — это набор файлов, компилирующихся вместе, поэтому все классы, объявленные внутри модуля, могут иметь доступ к членам с модификатором internal.

  • Public — не ограничивает доступ к членам класса. Поля и методы с модификатором public доступны из любого места программы, включая другие модули.

1. Модификатор private.

Private — самый строгий модификатор доступа. При его использовании данные будут доступны только в пределах конкретного класса или файла.

// Переменная видима внутри данного файла
private const val a = 20

// Класс доступен только внутри данного файла
private class Person() {

    // Переменную можно использовать только внутри класса Person
    private val b = a
}

// ERROR: переменную b нельзя использовать за пределами класса Person
private const val c = b

По сути, главное предназначение данного модификатора — реализация инкапсуляции в программе.

2. Модификатор protected.

Данные, отмеченные модификатором protected будут видны:

  • внутри класса, в котором они объявлены

  • в дочерних классах

При этом нельзя отметить модификатором protected данные высокого уровня. К таким данным относятся классы, а также переменные или функции, объявленные вне класса.

// ERROR: Нельзя использовать protected для переменных вне класса
protected const val a = 20

// ERROR: Нельзя использовать protected для класса
protected class Person() {

  // Переменная видима внутри класса Person
  protected val b = a
}

Если в дочернем классе мы переопределим метод с модификатором protected, то он унаследует модификатор доступа от родителя и будет виден только внутри дочернего класса. Несмотря на то, что модификатор не будет указан явно.

open class Person() {
    protected open fun getAge() = 20
}

private class Student : Person() {
    // модификатор явно не указан, но он такой же, как и в родительском классе
    override fun getAge() = 25
}

Помимо модификатора protected такому методу можно задать модификатор public. При использовании остальных модификаторов Kotlin ругается.

private class Student : Person() {
    override fun public getAge() = 25
}

3. Модификатор internal.

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

Данный модификатор можно применять ко всем типам данных. Однако он полезен только в том случае, если в проекте есть более одного модуля. Иначе используется модификатор public.

Например, в проекте есть два модуля — Module1 и Module2. В первом модуле есть класс Person().

// Module1

// Переменная видима для всего Module1
internal const val a = 20

// Класс доступен для всего Module1
internal open class Person() {
    // Переменная видима для всего Module1
    internal val b = a
}

И еще в первом модуле есть такой файл:

// Module1

private const val c = a + b

Так как этот файл тоже находится в Module1, то мы можем получить доступ к переменным a и b. Но если попытаться к ним обратиться из Module2 — получим ошибку.

// Module2

// ERROR: переменные a и b недоступны для данного модуля
private const val d = a + b

4. Модификатор public.

Если при объявлении каких-либо данных использовать модификатор public, то они будут видны всем (даже в космосе). Еще public является модификатором по умолчанию для тех данных, которым модификатор явно не был указан.

// Переменные доступны из любого места

public const val a = 20

public open class Person() {

    public val b = a
}

public class Student() {

    public val с = a + b
}

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

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

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

  1. var — это изменяемая переменная. После инициализации мы можем изменять данные, хранящиеся в переменной.

Переменные val и const val доступны только для чтения — это неизменяемые переменные.

  1. val — константа времени выполнения, т.е. значение можно назначить во время выполнения программы.

  2. const val — константа времени компиляции, т.к. значения константам присваивается при компиляции (в момент, когда программа компилируется).

В отличие от val, значение const val должно быть известно во время компиляции.

Особенности const val:

  • могут получать значение только базовых типов: Int, Double, Float, Long, Short, Byte, Char, String, Boolean.

  • объявляются в глобальной области видимости, то есть за пределами функции main() или любой другой функции.

  • нет пользовательского геттера.

Расширенный ответ — модификатор const

Чтобы объявить константу, нужно использовать модификатор const совместно с ключевым словом val. Переменные, отмеченные модификатором const, также называют константами времени компиляции. Это означает, что значения таких переменных известны во время компиляции. Отсюда следует, что они должны соответствовать следующим требованиям:

  • находиться на самом верхнем уровне (вне класса) или быть членом объекта (object или companion object)

  • тип данных должен соответствовать одному из примитивных (например, String)

  • не иметь геттера

Пример:

class SomeClass {
    companion object {    
        const val FILE_EXTENSION = ".jpg"    

        val FILENAME: String
          get() = "Img_" + System.currentTimeMillis() + FILE_EXTENSION
    }
}

Здесь мы в объекте-компаньоне объявили константу, значением которой является расширение фотографии. Помимо этого мы объявили неизменяемую переменную, которая будет хранить имя фотографии и инициализироваться с помощью get-метода.

Мы заранее (до компиляции) знаем, что расширение у всех фотографий будет одно и то же. Нам не нужно его вычислять. Поэтому логично будет его объявить как константу.

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

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

Как стоит объявлять свои константы в Kotlin — при помощи companion object или вне класса?

На самом деле оба эти подхода приемлемы. Однако, использование companion object может быть излишним: компилятор Kotlin преобразует companion object во вложенный класс. Слишком много кода для простой константы.

Если вам не требуется поведение, специфичное для companion object, объявляйте константы вне класса, так как это будет способствовать более эффективному байт-коду. Да и сам синтаксис объявления констант вне класса более чистый и читабельный.

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

Свойства, методы get и set

Свойства класса — это переменные, которые хранят состояние объекта класса. Как и любая переменная, свойство может иметь тип, имя и значение.

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

class Person {
    var name: String = ""
    val age: Int = 0
}

При создании своего класса мы хотим сами управлять его свойствами, контролируя то, какие данные могут быть предоставлены или перезаписаны. С этой целью создаются get и set методы (геттеры и сеттеры). Цель get-метода — вернуть значение, а set-метода — записать полученное значение в свойство класса.

var name: String = ""
    get() = field.toUpperCase()
    set(value) {
        field = "Name: $value"
    }

В данном примере свойство name имеет тип String и начальное значение пустой строки. Геттер возвращает значение свойства, преобразованное к верхнему регистру. Сеттер устанавливает значение свойства с добавлением префикса "Name: " перед переданным значением. Слово field используется для обращения к текущему значению свойства.

Если get и set методы не были созданы вручную, то для таких свойств Kotlin незаметно сам их генерирует. При этом для свойства, объявленного с val, генерируется get-метод, а для свойства, объявленного с varи get, и set методы.

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

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

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

В Kotlin свойство (property) — это абстракция над полями (fields), которая позволяет обращаться к значению переменной через методы геттера и сеттера, вместо прямого доступа к полю.

Field — это переменная, которая содержит значение и может быть доступна напрямую или через геттер/сеттер.

Пример определения свойства с геттером и сеттером в классе:

class Person {
    var name: String = ""
        get() = field.toUpperCase()  // возвращает значение поля name в верхнем регистре
        set(value) {
            field = value.trim()    // устанавливает значение поля name без начальных и конечных пробелов
        }
}

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

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

Отложенная и ленивая инициализация свойств (lateinit и by lazy)

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

1. lateinit

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

Правила использования модификатора lateinit:

  • lateinit может использоваться только с var свойствами класса

  • lateinit может быть применен только к свойствам, объявленным внутри тела класса (но не в основном конструкторе), а также к переменным на верхнем уровне и локальным переменным

  • lateinit свойства могут иметь любой тип, кроме примитивных типов (таких как Int, Long, Double и т.д.)

  • lateinit свойства не могут быть nullable (т.е. обязательно должно быть объявлены без знака вопроса)

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

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

  • Нельзя использовать lateinit для переменных, определенных внутри локальных областей видимости (например, внутри функций)

  • При использовании модификатора lateinit у свойства не должно быть пользовательских геттеров и сеттеров

Для проверки факта инициализации переменной вызывайте метод isInitialized(). Функцию следует использовать экономно — не следует добавлять эту проверку к каждой переменной с отложенной инициализацией. Если вы используете isInitialized() слишком часто, то скорее всего вам лучше использовать тип с поддержкой null.

lateinit var catName: String

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContentView(R.layout.activity_main)
 
    catName = "Barsik"

    if (::catName.isInitialized) {
        Log.d("Kot", "Hi, $catName")
    }
}

2. by lazy

Ленивая инициализация (lazy initialization) — это подход, при котором объект инициализируется только при необходимости, а не сразу после создания. В Kotlin для ленивой инициализации свойств используется делегат lazy.

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

При использовании ленивой инициализации свойств с помощью by lazy в Kotlin, создается объект типа Lazy<T>, где T — это тип свойства, и этот объект используется для хранения значения свойства.

Когда код доходит до места, где используется свойство, вызывается метод getValue() этого объекта Lazy<T>. Если значение свойства еще не было проинициализировано, то вызывается лямбда-выражение, переданное в lazy { }, и ее результат используется для инициализации свойства. Значение сохраняется в объекте Lazy<T> и возвращается как результат метода getValue(). Если значение уже было проинициализировано, то просто возвращается сохраненное значение. Например, если у нас есть свойство:

val myProperty: Int by lazy { computeValue() }

то при первом обращении к свойству myProperty будет выполнена функция computeValue(), а результат будет сохранен. При последующих обращениях к свойству будет возвращено сохраненное значение.

3. Сравнение ленивой и отложенной инициализации

ленивая инициализация является одним из Delegate

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

ленивая инициализация применяется только к val

отложенная инициализация применяется только к var

у нас может быть ленивое свойство примитивного типа

lateinit применяется только к ссылочным типам

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

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

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

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

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

Делегированные свойства (Delegated properties) — это свойства, которые не хранят своё значение напрямую, а делегируют это значение другому объекту, который реализует интерфейс Delegate. При доступе к свойству, его значение запрашивается у делегата, который может выполнить какую-то дополнительную логику, а затем вернуть требуемое значение.

В Kotlin существуют несколько встроенных делегатов для работы с делегированными свойствами:

  • observable() — позволяет реагировать на изменения свойства

  • vetoable() — позволяет отклонять изменения значения свойства на основе заданного условия

  • notNull() — гарантирует, что свойство не будет иметь значение null

  • map() — позволяет хранить значения свойств в словаре (Map)

  • lazy() — позволяет создавать лениво инициализированные свойства

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

Подробнее: kotlinlang.ru, tech-geek.ru

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

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

Чтобы написать кастомный делегат, нужно определить класс, который реализует интерфейс ReadOnlyProperty для делегата val или ReadWriteProperty для делегата var.

Классы, реализующие ReadOnlyProperty и ReadWriteProperty, содержат два метода:

  • getValue(thisRef: T, property: KProperty<*>): R, который должен возвращать значение свойства.

  • setValue(thisRef: T, property: KProperty<*>, value: R), который должен устанавливать значение свойства.

Например, рассмотрим создание кастомного делегата для логирования изменения значения свойства:

class LoggingDelegate<T>(private var value: T) : ReadWriteProperty<Any?, T> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("Getting value of ${property.name}: $value")
        return value
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        println("Setting value of ${property.name} to $value")
        this.value = value
    }
}

Здесь мы определяем класс LoggingDelegate, который реализует интерфейс ReadWriteProperty. Метод getValue выводит в консоль текущее значение свойства и возвращает его, а метод setValue выводит новое значение свойства в консоль и сохраняет его в переменной value.

Затем мы можем использовать наш кастомный делегат следующим образом:

class MyClass {
    var myProperty: Int by LoggingDelegate(0)
}

fun main() {
    val obj = MyClass()
    obj.myProperty = 42 // Setting value of myProperty to 42
    println(obj.myProperty) // Getting value of myProperty: 42
}

Здесь мы создаем экземпляр класса MyClass, который содержит свойство myProperty, использующее наш кастомный делегат LoggingDelegate. При установке значения свойства или получении его значения будут вызываться соответствующие методы нашего делегата, и мы увидим соответствующие сообщения в консоли.

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

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

Конструкторы. Какие типы конструкторов вы знаете?

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

Класс в Kotlin может иметь основной конструктор (primary) и один или более вторичных конструкторов (secondary). У класса может и не быть конструктора, но Kotlin всё равно автоматически сгенерирует основной конструктор по умолчанию (без параметров).

1. Основной конструктор

Объявляется он сразу после имени класса и состоит из ключевого слова constructor и круглых скобок:

class Person constructor(name: String, age: Int) {
}

Можно обойтись и без ключевого слова constructor при условии, что нет аннотаций или модификаторов доступа.

class Person(name: String, age: Int)

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

class Person(name: String, age: Int) {
    val name = name
    var age = age
}

А можно упростить еще больше и из параметров конструктора сделать свойства класса. Для этого перед именем параметра нужно указать ключевое слово val (только для чтения) или var (для чтения и редактирования).

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

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

class Person(val name: String, var age: Int = 30)
...
val adam = Person("Adam")
val alice = Person("Alice", 25)
println("${adam.name}, ${adam.age}")      // Adam, 30
println("${alice.name}, ${alice.age}")    // Alice, 25

У класса может быть суперкласс. Тогда его основной конструктор должен инициализировать свойства, унаследованные от суперкласса.

open class Base(p: Int)

class Person(val name: String, var age: Int = 30, val p: Int) : Base(p)
...
val adam = Person("Adam", 30, 1000)
println(adam.p)                      // 1000

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

class Person private constructor(val name: String, var age: Int)
...
val adam = Person("Adam", 30)  // вылетит ошибка

2. Вторичный конструктор

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

Объявляется вторичный конструктор внутри тела класса при помощи ключевого слова constructor.

class Person {
  constructor(id: Int) {
    ...
  }
}

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

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

    constructor(name: String, age: Int, id: Int) : this(name, age) {
        ...
    }
}

Если основного конструктора нет, то и обращаться к нему не надо.

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

class Person(val name: String, var age: Int) {
    var id: Int = 0

    constructor(name: String, age: Int, id: Int) : this(name, age) {
        this.id = id
    }
}

Также во вторичный конструктор можно добавить какую-либо логику.

class Person(val name: String, var age: Int) {
    var id: Int = 0

    constructor(name: String, age: Int, id: Int) : this(name, age) {
        if (id > 0) this.id = id * 2
    }
}

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

open class Base(val p: Int)

class Person : Base {
    constructor(name: String, age: Int, p: Int) : super(p)
}
...
val adam = Person("Adam", 30, 1)
println(adam.p)                   // 1.

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

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

Блок инициализации (init блок)

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

class Person(val name: String, var age: Int) {
    var id: Int = 0

    // require выдает ошибку с указанным текстом, если условие в левой части false
    init {
        require(name.isNotBlank(), { "У человека должно быть имя!" })
        require(age > -1, { "Возраст не может быть отрицательным." })
    }

    constructor(name: String, age: Int, id: Int) : this(name, age) {
        if (id > 0) this.id = id * 2
    }
}

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

class Person(val name: String, var age: Int) {
    // сначала вызывается основной конструктор и создаются свойства класса
    // далее вызывается первый блок инициализации
    init {
        ...
    }

    // после первого вызывается второй блок инициализации
    init {
        ...
    }

    // и т.д.
}

Блок инициализации может быть добавлен, даже если у класса нет основного конструктора. В этом случае его код будет выполнен раньше кода вторичных конструкторов.

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

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

Расскажите о Data классах. Какие преимущества они имеют?

Data класс предназначен исключительно для хранения каких-либо данных.

Основное преимущество: для параметров, переданных в основном конструкторе автоматически будут переопределены методы toString(), equals(), hashCode(), copy().

Также для каждой переменной, объявленной в основном конструкторе, автоматически генерируются функции componentN(), где N — номер позиции переменной в конструкторе.

Благодаря наличию вышеперечисленных функций внутри data класса мы исключаем написание шаблонного кода.

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

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

Что такое мульти-декларации (destructuring declarations)?

Мульти-декларации (destructuring declarations или деструктуризирующее присваивание) — это способ извлечения значений из объекта и присвоения их сразу нескольким переменным. В Kotlin этот механизм поддерживается с помощью оператора распаковки (destructuring operator) — componentN(), где N — номер компонента.

При создании data класса Kotlin автоматически создает функции componentN() для каждого свойства класса, где N — номер позиции переменной в конструкторе. Функции componentN() возвращают значения свойств в порядке их объявления в конструкторе. Это позволяет использовать мульти-декларации для распаковки значений свойств и присваивания их отдельным переменным.

Например, если у нас есть data класс Person с двумя свойствами name и age, мы можем использовать мульти-декларации, чтобы извлечь эти свойства и присвоить их двум переменным:

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

val person = Person("Alice", 29)
val (name, age) = person

println(name) // Alice
println(age) // 29

Также можно использовать мульти-декларации в циклах, чтобы итерироваться по спискам объектов и распаковывать значения свойств:

val people = listOf(Person("Alice", 30), Person("Bob", 40))
for ((name, age) in people) {
    println("$name is $age years old")
}

// Alice is 30 years old
// Bob is 40 years old

Мульти-декларации также могут быть использованы с массивами и другими коллекциями:

val list = listOf("apple", "banana", "orange")
val (first, second, third) = list

println(first) // apple
println(second) // banana
println(third) // orange

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

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

Что делает функция componentN()?

Функция componentN() возвращает значение переменной и позволяет обращаться к свойствам объекта класса по их порядковому номеру. Генерируется автоматически только для data классов.

Также функцию componentN() можно создать самому для класса, который не является data классом.

class Person(val firstName: String, val lastName: String, val age: Int) {
    operator fun component1() = firstName
    operator fun component2() = lastName
    operator fun component3() = age
}

Теперь можно использовать мульти-декларации для класса Person:

val person = Person("John", "Doe", 30)
val (firstName, lastName, age) = person
println("$firstName $lastName is $age years old.")

В данном примере мы определили функции component1(), component2() и component3() как операторы с ключевым словом operator. Они возвращают значения свойств firstName, lastName и age соответственно. После этого мы можем использовать мульти-декларации для разбивки объекта Person на отдельные переменные.

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

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

  • Класс должен иметь хотя бы одно свойство, объявленное в основном конструкторе.

  • Все параметры основного конструктора должны быть отмечены val или var (рекомендуется val).

  • Классы данных не могут быть abstract, open, sealed или inner.

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

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

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

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

Источник: https://habr.com/ru/post/722686/


Интересные статьи

Интересные статьи

Всем привет ещё раз, пришло время продолжить обсуждение применения  Composition root в Unity. В прошлой статье я описал основные положения данной архитектуры, способы проброса зависимостей и орга...
Мы продолжаем изучать волшебство преобразования энергии в ШИМ преобразователе. Почему волшебство? Теоретически, как мы убедились в предыдущей части, линейный стабилизатор...
Если у вас есть проект с интенсивной обработкой данных глубокими моделями (или еще нет, но вы собираетесь его создать), то вам будет полезно познакомиться с приемами по п...
Полтора года назад я просматривал блог одного из успешных российских фотографов-портретистов с узнаваемым стилем и в голову закралась мысль, а почему бы просто не поставить камеру на штатив, один...
Всем привет! Меня зовут Виталий Малкин. Я руководитель отдела анализа защищённости компании «Информзащита» и по совместительству капитан команды True0xA3. Чуть больше недели назад мы победили в о...