Какой тип по умолчанию возвращают функции kotlin
Перейти к содержимому

Какой тип по умолчанию возвращают функции kotlin

  • автор:

Какой тип по умолчанию возвращают функции kotlin

Статья проплачена кошками — всемирно известными производителями котят.

Если статья вам понравилась, то можете поддержать проект.

  • Именованные параметры
  • Параметры по умолчанию
  • Unit. Если функция ничего не возвращает
  • Ключевое слово vararg — переменное число параметров
  • Вложенные (локальные) функции
  • Функции верхнего уровня
  • Функция TODO()
  • infix
  • Имена функций в обратных кавычках

Коты забавные, поэтому ввели ключевое слово fun (есть спорное мнение, что на самом деле это сокращение от «function» для обозначения функций, которые являются аналогами методов в Java).

Объявление функции начинается с ключевого слова fun, затем идёт имя функции, в круглых скобках указываются параметры. Тип возвращаемого значения указывается после списка параметров и отделяется от него двоеточием. Функция всегда возвращает значение. Если вы сами не указали возвращаемое значение, то функция вернёт Unit, который схож с void, но является объектом.

Параметры в функциях объявляется немного иначе, чем в Java — сначала имя параметра, потом его тип.

 fun add(x: Int, y: Int): Int

С функциями можно работать как с значениями — можно сохранить в переменной, передать в качестве параметра, возвратить из другой функции.

Стандартный вывод «Hello Kitty» для Kotlin-программы (Desktop, не Android):

 fun main()

Данная функция ничего не возвращает. Напишем другую функцию, возвращающую результат.

 fun max(a: Int, b: Int): Int < return if (a >b) a else b > 
 println(max(7, 2)) // выводит 7 

Обратите внимание, что if является выражением в Kotlin, а не Java-оператором и соответствует тернарному оператору в Java:

 (a > b) ? a : b 

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

 fun max(a: Int, b: Int): Int = if (a > b) a else b 

Можно даже убрать возвращаемый тип. Гулять так гулять.

 fun max(a: Int, b: Int) = if (a > b) a else b 

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

В других случаях (тело-блок) вы обязаны указывать возвращаемый тип и использовать инструкцию return.

Функции верхнего уровня можно импортировать для сокращения кода.

 import strings.lastChar val cat = "Cat".lastChar() 

Доступен вариант со звёздочкой.

 import strings.* val c = "Cat".lastChar() 

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

 import strings.lastChar as last val c = "Cat".last() 

Именованные параметры

Мы привыкли, что при вызове метода следует соблюдать очерёдность параметров. С именованными параметрами такая необходимость отпала. Создадим новую функцию из двух параметров.

 fun sayHelloByName(firstName: String, secondName: String)

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

 sayHelloByName(secondName = "Котофеевич", firstName = "Котофей") 

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

Данный приём не сработает при работе с методами, написанными на Java. Поддержка именованных аргументов есть в Java 8, но Kotlin поддерживает совместимость с Java 6, поэтому приходится смириться. Возможно, в будущем, эта проблема решится автоматически, когда откажутся от поддержки старых версий.

Параметры по умолчанию

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

Добавим в класс активности новую функцию для вывода всплывающего сообщения (в примере используется функция-расширение).

 fun Context.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT)

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

 toast("Meow") // просто и аккуратно toast("Meow-w-w", Toast.LENGTH_LONG) // используем второй параметр 

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

 fun sayHello(firstWord: String, secondWord: String = "Kitty", thirdWord: String)

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

 sayHello("Hello", "Kitty") // не компилируется 

В этом случае на помощь приходят именованные параметры.

 sayHello("Hello", thirdWord = "Kitty") 

Третий параметр теперь нам известен, опущенный параметр относится ко второму, оставшийся относится к первому.

У класса Thread имеется восемь конструкторов! Вы можете создавать гораздо удобные решения с параметрами по умолчанию.

Поскольку в Java нет понятия параметров по умолчанию, вам придётся явно указывать все значения при вызове функции Kotlin из Java-кода. В этом случае добавьте аннотацию @JvmOverloads, который создаст перегруженные версии методов, опуская каждый из параметров по одному, начиная с последнего.

Unit. Если функция ничего не возвращает

Стоит немного рассказать о функциях, которые не возвращают никаких значений. В Java мы используем ключевое слово void для подобных случаев. В Kotlin был придуман новый тип Unit для подобных ситуаций. Получается, что функция всегда что-то возвращает, в нашем случае Unit, который мы никак не используем.

 fun sayHello(name: String): Unit < println("Hello $name") >button.setOnClickListener

Но Kotlin достаточно умен и понимает, что мы не хотим ничего возвращать. Поэтому мы можем сократить код.

 fun sayHello(name: String)

Можно сократить код, убрав фигурные скобки, так как у функции только одно выражение.

 fun sayHello(name: String) = println("Hello $name") 

Ключевое слово vararg — переменное число параметров

В Java при вызове методов с разным числом аргументов использовалось троеточие (. ). В Kotlin существует другой подход — ключевое слово vararg.

 fun printNumbers(vararg integers: Int) < for (number in integers) < println("$number") >> 

Вызываем функцию с любым количеством аргументов.

 printNumbers(1, 2, 3, 4, 5) printNumbers(4) 

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

По сути vararg работает с массивом, но простое добавление массива Kotlin не пропустит. Следует использовать специальный оператор *.

 val intArray: IntArray = intArrayOf(6, 7, 8, 9) printNumbers(1, 2, 3, 4, 5, *intArray) 

Вложенные (локальные) функции

Внутри одной функции можно создать ещё одну локальную функцию.

 fun doIt(param: String) < // fun justDoIt(innerParam: String) < println(innerParam) println(param) >> 

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

Создадим функцию, которая выводим имя кота в верхнем регистре. Заодно создадим вложенную функцию, которая подсчитывает длину имени кота.

 fun getCat(name: String) < fun makeStrange(): Int < return name.length * 2 >println(name.uppercase() + makeStrange()) > // вызываем функцию getCat("barsik") // BARSIK12 getCat("vaska") // VASKA10 

Функции верхнего уровня

Функцию можно объявить в начале файла, не обязательно размещать его в теле класса. Это удобно, когда вам нужны методы, которые не относятся к конкретному классу или вы не хотите перезагружать имеющийся класс лишним кодом. Часто для этих целей программисты создавали отдельные классы со словом Util.

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

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

 // файл cats.kt package kitten fun someFun(. ): String

Kotlin незаметно для вас создаст класс CatsKt по имени файла и все функции скомпилирует в статические методы. Если будете вызывать функцию в Java-коде, то это будет выглядеть следующим образом.

 import kitten.CatsKt; . CatsKt.someFun(. ); 

Если имя класса вас не устраивает, то добавьте аннотацию @JvmName перед именем пакета.

 @file:JmvName("CatFunctions") package kitten 

Тогда вызов в Java-коде будет другим.

 import kitten.CatFunctions; . CatFunctions.someFun(. ); 

Функция TODO()

В стандартную библиотеку Kotlin входит функция TODO() (надо сделать). Её описание выглядит следующим образом.

 /** * Всегда возбуждает [NotImplementedError], сигнализируя, что операция не реализована. */ public inline fun TODO(): Nothing = throw NotImplementedError() 

Функция TODO() возбуждает исключение, т.е. вызов функции гарантированно завершится ошибкой — она возвращает тип Nothing. Считайте функцию временной заглушкой. Разработчик знает, что некоторая функция должна вернуть строку или другой объект, но пока отсутствуют другие функции, необходимые для ее реализации. Создадим для примера две функции.

 fun shouldReturnAString(): String < TODO("implement the string building functionality here to return a string") >fun shouldReturnACat(): Cat

Обратите внимание, что возвращаемое значение для shouldReturnAString() — это String, но на самом деле функция ничего не возвращает. Аналогично у shouldReturnACat().

Возвращаемый тип Nothing у TODO() показывает компилятору, что функция гарантированно вызовет ошибку, поэтому проверять возвращаемое значение после TODO() не имеет смысла, так как shouldReturnAString() и shouldReturnACat() ничего не вернут. Компилятор не будет ругаться, а разработчик может продолжать разработку, отложив на потом реализацию функции-заглушки.

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

 fun shouldReturnACat(): Cat < TODO() println("миссия невозможна") // этот код не будет вызван >

infix

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

Например, в ассоциативных списках часто используют следующий приём.

 // инфиксная нотация val map = mapOf(1 to "one", 3 to "three", 9 to "nine") println(map) 

Пример можно заменить на более традиционный.

 // обычный способ val map2 = mapOf(1.to("one"), 2.to("two"), 5.to("five")) println(map2) 

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

Имена функций в обратных кавычках

Можно объявить или вызвать функцию с именем, содержащим нестандартные символы. Для этого достаточно заключить имя в обратные кавычки `. Например, объявим функцию:

 fun `12!cat`() = println("I am a cat!") 
 `12!cat`() 

Данная возможность нужна, чтобы поддерживать совместимость с Java в тех моментах, когда встречаются зарезервированные ключевые слова. Использование обратных кавычек позволяют избежать несовместимости в случаях, если это необходимо. На практике такое почти не встречается.

На данный момент под Android такой способ не работает, студия будет ругаться.

Основные типы

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

Числа

Целочисленные типы

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

Тип Размер (биты) Минимальное значение Максимальное значение
Byte 8 -128 127
Short 16 -32768 32767
Int 32 -2,147,483,648 (-2 31 ) 2,147,483,647 (2 31 — 1)
Long 64 -9,223,372,036,854,775,808 (-2 63 ) 9,223,372,036,854,775,807 (2 63 — 1)

Все переменные, инициализированные целыми значениями, не превышающими максимальное значение Int , имеют предполагаемый тип Int . Если начальное значение превышает это значение, то тип Long . Чтобы явно указать тип Long , добавьте после значения L .

val one = 1 // Int val threeBillion = 3000000000 // Long val oneLong = 1L // Long val oneByte: Byte = 1 

Типы с плавающей точкой

Для действительных чисел в Kotlin есть типы с плавающей точкой Float и Double . Согласно стандарту IEEE 754, типы с плавающей точкой различаются своим десятичным разрядом, то есть количеством десятичных цифр, которые они могут хранить. С точки зрения IEEE 754 Float является одинарно точным, а Double обеспечивает двойную точность.

Тип Размер (биты) Значимые биты Биты экспоненты Разряды
Float 32 24 8 6-7
Double 64 53 11 15-16

Вы можете инициализировать переменные Double и Float числами, имеющими дробную часть. Она должна быть отделена от целой части точкой ( . ). Для переменных, инициализированных дробными числами, компилятор автоматически определяет тип Double .

val pi = 3.14 // Double // val one: Double = 1 // Ошибка: несоответствие типов val oneDouble = 1.0 // Double 

Чтобы явно указать тип Float , добавьте после значения f или F . Если такое значение содержит более 6-7 разрядов, оно будет округлено.

val e = 2.7182818284 // Double val eFloat = 2.7182818284f // Float, фактическое значение 2.7182817 

Обратите внимание, что в отличие от некоторых других языков, в Kotlin нет неявных преобразований для чисел. Например, функция с Double параметром может вызываться только для Double , но не для Float , Int или других числовых значений.

fun main() < fun printDouble(d: Double) < print(d) >val i = 1 val d = 1.0 val f = 1.0f printDouble(d) // printDouble(i) // Ошибка: несоответствие типов // printDouble(f) // Ошибка: несоответствие типов > 

Чтобы преобразовать числовые значения в различные типы, используйте Явные преобразования.

Символьные постоянные

В языке Kotlin присутствуют следующие виды символьных постоянных (констант) для целых значений:

  • Десятичные числа: 123
    • Тип Long обозначается заглавной L : 123L

    ВНИМАНИЕ: Восьмеричные литералы не поддерживаются.

    Также Kotlin поддерживает числа с плавающей запятой:

    • Тип Double по умолчанию: 123.5 , 123.5e10
    • Тип Float обозначается с помощью f или F : 123.5f

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

    val oneMillion = 1_000_000 val creditCardNumber = 1234_5678_9012_3456L val socialSecurityNumber = 999_99_9999L val hexBytes = 0xFF_EC_DE_5E val bytes = 0b11010010_01101001_10010100_10010010 

    Представление чисел в JVM

    Обычно платформа JVM хранит числа в виде примитивных типов: int , double и так далее. Если же вам необходима ссылка, которая может принимать значение null (например, Int? ), то используйте обёртки. В этих случаях числа помещаются в Java классы как Integer , Double и так далее.

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

    val a: Int = 100 val boxedA: Int? = a val anotherBoxedA: Int? = a val b: Int = 10000 val boxedB: Int? = b val anotherBoxedB: Int? = b println(boxedA === anotherBoxedA) // true println(boxedB === anotherBoxedB) // false 

    Все nullable-ссылки на a на самом деле являются одним и тем же объектом из-за оптимизации памяти, которую JVM применяет к Integer между «-128» и «127». Но b больше этих значений, поэтому ссылки на b являются разными объектами.

    Однако, равенство по значению сохраняется.

    val b: Int = 10000 println(b == b) // Prints 'true' val boxedB: Int? = b val anotherBoxedB: Int? = b println(boxedB == anotherBoxedB) // Prints 'true' 

    Явные преобразования

    Из-за разницы в представлениях меньшие типы не являются подтипами бОльших типов. В противном случае возникли бы сложности.

    // Возможный код, который на самом деле не скомпилируется: val a: Int? = 1 // "Обёрнутый" Int (java.lang.Integer) val b: Long? = a // неявное преобразование возвращает "обёрнутый" Long (java.lang.Long) print(b == a) // Нежданчик! Данное выражение выведет "false" т. к. метод equals() типа Long предполагает, что вторая часть выражения также имеет тип Long 

    Таким образом, будет утрачена не только тождественность (равенство по ссылке), но и равенство по значению.

    Как следствие, неявное преобразование меньших типов в большие НЕ происходит. Это значит, что мы не можем присвоить значение типа Byte переменной типа Int без явного преобразования.

    val b: Byte = 1 // всё хорошо, литералы проверяются статически // val i: Int = b // ОШИБКА val i1: Int = b.toInt() 

    Каждый численный тип поддерживает следующие преобразования:

    • toByte(): Byte
    • toShort(): Short
    • toInt(): Int
    • toLong(): Long
    • toFloat(): Float
    • toDouble(): Double
    • toChar(): Char

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

    val l = 1L + 3 // Long + Int => Long 

    Операции

    Котлин поддерживает стандартный набор арифметических операций над числами: + , — , * , / , % . Они объявляются членами соответствующих классов.

    println(1 + 2) println(2_500_000_000L - 1L) println(3.14 * 2.71) println(10.0 / 3) 

    Вы также можете переопределить эти операторы для пользовательских классов. См. Перегрузка операторов для деталей.

    Деление целых чисел

    Деление целых чисел всегда возвращает целое число. Любая дробная часть отбрасывается.

    val x = 5 / 2 // println(x == 2.5) // ОШИБКА: Оператор '==' не может быть применен к 'Int' и 'Double' println(x == 2) // true 

    Это справедливо для деления любых двух целочисленных типов.

    val x = 5L / 2 println(x == 2L) // true 

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

    val x = 5 / 2.toDouble() println(x == 2.5) // true 
    Побитовые операции

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

    val x = (1 shl 2) and 0x000FF000 

    Ниже приведён полный список битовых операций:

    • shl(bits) – сдвиг влево с учётом знака (
    • shr(bits) – сдвиг вправо с учётом знака ( >> в Java)
    • ushr(bits) – сдвиг вправо без учёта знака ( >>> в Java)
    • and(bits) – побитовое И
    • or(bits) – побитовое ИЛИ
    • xor(bits) – побитовое исключающее ИЛИ
    • inv() – побитовое отрицание

    Сравнение чисел с плавающей точкой

    В этом разделе обсуждаются следующие операции над числами с плавающей запятой:

    • Проверки на равенство: a == b и a != b
    • Операторы сравнения: a < b , a >b , a = b
    • Создание диапазона и проверка диапазона: a..b , x in a..b , x !in a..b

    Когда статически известно, что операнды a и b являются Float или Double или их аналогами с nullable-значением (тип объявлен или является результатом умного приведения), операции с числами и диапазоном, который они образуют, соответствуют стандарту IEEE 754 для арифметики с плавающей точкой.

    `, a type parameter), the operations use the `equals` and `compareTo` implementations for `Float` and `Double`, which disagree with the standard, so that: —>

    Однако для поддержки общих вариантов использования и обеспечения полного упорядочивания, когда операнды статически не объявлены как числа с плавающей запятой (например, Any , Comparable <. >, параметр типа), операции используют реализации equals и compareTo для Float и Double , которые не согласуются со стандартом, так что:

    • NaN считается равным самому себе
    • NaN считается больше, чем любой другой элемент, включая «POSITIVE_INFINITY»
    • -0.0 считается меньше, чем 0.0

    Целые беззнаковые числа

    В дополнение к целочисленным типам, в Kotlin есть следующие типы целых беззнаковых чисел:

    • UByte : беззнаковое 8-битное целое число, в диапазоне от 0 до 255
    • UShort : беззнаковое 16-битное целое число, в диапазоне от 0 до 65535
    • UInt : беззнаковое 32-битное целое число, в диапазоне от 0 до 2 32 — 1
    • ULong : беззнаковое 64-битное целое число, в диапазоне от 0 до 2 64 — 1

    Беззнаковые типы поддерживают большинство операций своих знаковых аналогов.

    Changing type from unsigned type to signed counterpart (and vice versa) is a *binary incompatible* change. —>

    Изменение типа с беззнакового типа на его знаковый аналог (и наоборот) является двоично несовместимым изменением.

    Беззнаковые массивы и диапазоны

    Unsigned arrays and operations on them are in [Beta](components-stability.md). They can be changed incompatibly at any time. > Opt-in is required (see the details below). —>

    Беззнаковые массивы и операции над ними находятся в стадии бета-тестирования. Они могут быть несовместимо изменены в любое время.

    Как и в случае с примитивами, каждому типу без знака соответствует тип массивов знаковых типов:

    • UByteArray : массив беззнаковых byte
    • UShortArray : массив беззнаковых short
    • UIntArray : массив беззнаковых int
    • ULongArray : массив беззнаковых long

    Как и целочисленные массивы со знаком, такие массивы предоставляют API, аналогичный классу Array , без дополнительных затрат на оборачивание.

    При использовании массивов без знака вы получите предупреждение, что эта функция еще не стабильна. Чтобы удалить предупреждение используйте аннотацию @ExperimentalUnsignedTypes . Вам решать, должны ли ваши пользователи явно соглашаться на использование вашего API, но имейте в виду, что беззнаковый массив не является стабильной функцией, поэтому API, который он использует, может быть нарушен изменениями в языке. Узнайте больше о требованиях регистрации.

    Диапазоны и прогрессии поддерживаются для UInt и ULong классами UIntRange , UIntProgression , ULongRange и ULongProgression . Вместе с целочисленными беззнаковыми типами эти классы стабильны.

    Литералы

    Чтобы целые беззнаковые числа было легче использовать, в Kotlin можно помечать целочисленный литерал суффиксом, указывающим на определенный беззнаковый тип (аналогично Float или Long ):

    • u и U помечают беззнаковые литералы. Точный тип определяется на основе ожидаемого типа. Если ожидаемый тип не указан, компилятор будет использовать UInt или ULong в зависимости от размера литерала.
    val b: UByte = 1u // UByte, есть ожидаемый тип val s: UShort = 1u // UShort, есть ожидаемый тип val l: ULong = 1u // ULong, есть ожидаемый тип val a1 = 42u // UInt: ожидаемого типа нет, константе подходит тип UInt val a2 = 0xFFFF_FFFF_FFFFu // ULong: ожидаемого типа нет, тип UInt не подходит константе 
    • uL and UL явно помечают литерал как unsigned long .
    val a = 1UL // ULong, даже несмотря на то, что ожидаемого типа нет и константа вписывается в UInt 
    Дальнейшее обсуждение

    См. предложения для беззнаковых типов для технических деталей и дальнейшего обсуждения.

    Логический тип

    Тип Boolean представляет логический тип данных и принимает два значения: true и false .

    При необходимости использования nullable-ссылок логические переменные оборачиваются Boolean? .

    Встроенные действия над логическими переменными включают:

    • || – ленивое логическое ИЛИ
    • && – ленивое логическое И
    • ! – отрицание
    val myTrue: Boolean = true val myFalse: Boolean = false val boolNull: Boolean? = null println(myTrue || myFalse) println(myTrue && myFalse) println(!myTrue) 

    **On JVM**: nullable references to boolean objects are boxed similarly to [numbers](#numbers-representation-on-the-jvm). —>

    В JVM: nullable-ссылки на логические объекты заключены в рамки аналогично числам.

    Символы

    Символы в Kotlin представлены типом Char . Символьные литералы заключаются в одинарные кавычки: ‘1’ .

    Специальные символы начинаются с обратного слеша \ . Поддерживаются следующие escape-последовательности: \t , \b , \n , \r , \’ , \» , \\ и \$ .

    Для кодирования любого другого символа используйте синтаксис escape-последовательности Юникода: ‘\uFF00’ .

    val aChar: Char = 'a' println(aChar) println('\n') // выводит дополнительный символ новой строки println('\uFF00') 

    Если значение символьной переменной – цифра, её можно явно преобразовать в Int с помощью функции digitToInt() .

    **On JVM**: Like [numbers](#numbers-representation-on-the-jvm), characters are boxed when a nullable reference is needed. >Identity is not preserved by the boxing operation. —>

    В JVM: Подобно числам, символы оборачиваются при необходимости использования nullable-ссылки. При использовании обёрток тождественность (равенство по ссылке) не сохраняется.

    Строки

    Строки в Kotlin представлены типом String . Как правило, строка представляет собой последовательность символов в двойных кавычках ( » ).

    val str = "abcd 123" 

    Строки состоят из символов, которые могут быть получены по порядковому номеру: s[i] . Проход по строке выполняется циклом for .

    for (c in str)

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

    val str = "abcd" println(str.uppercase()) // Создается и выводится новый объект String println(str) // исходная строка остается прежней 

    Для объединения строк используется оператор + . Это работает и для объединения строк с другими типами, если первый элемент в выражении является строкой.

    val s = "abc" + 1 println(s + "def") // abc1def 

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

    Строковые литералы

    В Kotlin представлены два типа строковых литералов:

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

    Вот пример экранированной строки:

    val s = "Hello, world!\n" 

    Экранирование выполняется общепринятым способом, а именно с помощью обратного слеша ( \ ). Список поддерживаемых escape-последовательностей см. в разделе Символы выше.

    Обычная строка выделена тройной кавычкой ( «»» ), не содержит экранированных символов, но может содержать символы новой строки и любые другие символы:

    val text = """ for (c in "foo") print(c) """ 

    Чтобы удалить пробелы в начале обычных строк, используйте функцию trimMargin() .

    val text = """ |Tell me and I forget. |Teach me and I remember. |Involve me and I learn. |(Benjamin Franklin) """.trimMargin() 

    По умолчанию | используется в качестве префикса поля, но вы можете выбрать другой символ и передать его в качестве параметра, например, trimMargin(«>») .

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

    Строки могут содержать шаблонные выражения, т.е. участки кода, которые выполняются, а полученный результат встраивается в строку. Шаблон начинается со знака доллара ( $ ) и состоит либо из простого имени (например, переменной),

    val i = 10 println("i = $i") // выведет "i = 10" 

    либо из произвольного выражения в фигурных скобках.

    val s = "abc" println("$s.length is $") // выведет "abc.length is 3" 

    Шаблоны поддерживаются как в обычных, так и в экранированных строках. При необходимости вставить символ $ в обычную строку (такие строки не поддерживают экранирование обратным слешом) перед любым символом, который разрешен в качестве начала идентификатора, используйте следующий синтаксис:

    val price = """ $_9.99 """ 

    Массивы

    Массивы в Kotlin представлены классом Array , обладающим функциями get и set (которые обозначаются [] согласно соглашению о перегрузке операторов), и свойством size , а также несколькими полезными встроенными функциями.

    class Array private constructor() < val size: Int operator fun get(index: Int): T operator fun set(index: Int, value: T): Unit operator fun iterator(): Iterator// . > 

    Для создания массива используйте функцию arrayOf() , которой в качестве аргумента передаются элементы массива, т.е. выполнение arrayOf(1, 2, 3) создаёт массив [1, 2, 3] . С другой стороны функция arrayOfNulls() может быть использована для создания массива заданного размера, заполненного значениями null .

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

    // создаёт массив типа Array со значениями ["0", "1", "4", "9", "16"] val asc = Array(5) < i ->(i * i).toString() > asc.forEach

    Как отмечено выше, оператор [] используется вместо вызовов встроенных функций get() и set() .

    ` to an `Array `, which prevents a possible runtime failure (but you can use `Array `, see [Type Projections](generics.md#type-projections)). —>

    Обратите внимание: в отличие от Java массивы в Kotlin являются инвариантными. Это значит, что Kotlin запрещает нам присваивать массив Array переменной типа Array , предотвращая таким образом возможный отказ во время исполнения (хотя вы можете использовать Array , см. Проекции типов).

    Массивы примитивных типов

    Также в Kotlin есть особые классы для представления массивов примитивных типов без дополнительных затрат на оборачивание: ByteArray , ShortArray , IntArray и т.д. Данные классы не наследуют класс Array , хотя и обладают тем же набором методов и свойств. У каждого из них есть соответствующая фабричная функция:

    val x: IntArray = intArrayOf(1, 2, 3) x[0] = x[1] + x[2] 
    // int массив, размером 5 со значениями [0, 0, 0, 0, 0] val arr = IntArray(5) // инициализация элементов массива константой // int массив, размером 5 со значениями [42, 42, 42, 42, 42] val arr = IntArray(5) < 42 >// инициализация элементов массива лямбда-выражением // int массив, размером 5 со значениями [0, 1, 2, 3, 4] (элементы инициализированы своим индексом) var arr = IntArray(5)

    © 2015—2023 Open Source Community

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

    • Расскажите о 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()
    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()

    Разница между 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 совместно с ключевым словом 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

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

    var name: String = "" get() = field.toUpperCase() set(value)

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

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

    В чем отличие 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 — это тип свойства, и этот объект используется для хранения значения свойства.

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

    val myProperty: Int by lazy

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

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

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

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

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

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

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

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

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

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

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

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

    class Example

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

    Синтаксис выглядит так: val/var : by . Выражение после by — делегат, потому что обращения get() , set() к свойству будут делегированы его методам getValue() и setValue() . Делегат не обязан реализовывать какой-то интерфейс, достаточно, чтобы у него были метод getValue() (и setValue() для var ‘ов) с определённой сигнатурой.

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

    • lazy() — позволяет создавать лениво инициализированные свойства
    • observable() — позволяет реагировать на изменения свойства
    • vetoable() — позволяет отклонять изменения значения свойства на основе заданного условия
    • notNull() — гарантирует, что свойство не будет иметь значение null
    • map() — позволяет хранить значения свойств в словаре ( Map )

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

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

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

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

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

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

    class LoggingDelegate(private var value: T) : ReadWriteProperty < override fun getValue(thisRef: Any?, property: KProperty): T < println("Getting value of $: $value") return value > override fun setValue(thisRef: Any?, property: KProperty, value: T) < println("Setting value of $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 . При установке значения свойства или получении его значения будут вызываться соответствующие методы нашего делегата, и мы увидим соответствующие сообщения в консоли.

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

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

    Класс в 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 (только для чтения) или 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, 30 println("$, $") // 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.

    Блок инициализации (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 < . >// и т.д. >

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

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

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

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

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

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

    Что такое мульти-декларации (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

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

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

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

    class Person(val firstName: String, val lastName: String, val age: Int)

    Теперь можно использовать мульти-декларации для класса 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
    • собеседование вопросы
    • подготовка к собеседованию
    • android development
    • андроид
    • котлин
    • вопросы для собеседования
    • android
    • faq
    • учебные материалы
    • Программирование
    • Java
    • Разработка под Android
    • Kotlin
    • Учебный процесс в IT

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

    • Какие коллекции есть в Kotlin?
    • List
    • Set
    • Map
    • Какая из коллекций не является имплементацией Collection?
    • Sequences и их отличия от коллекций
    • Промежуточные (intermediate) и терминальные (terminal) операции в Sequences

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

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

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

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

    abstract class Tree

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

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

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

    abstract class Tree < abstract val name: String abstract val description: String fun info(): String = "$name - $." > . 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 - $." >

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

    abstract class Tree(val name: String, val description: String) < open fun info(): String = "$name - $." >

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

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

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

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

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

    interface Cultivable

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

    abstract class Tree : Cultivable < abstract val name: String abstract val description: String open fun info(): String = "$name - $." override val bloom = false override fun startPhotosynthesis() < . >>

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

    abstract class Tree : Cultivable < abstract val name: String abstract val description: String open fun info(): String = "$name - $." override fun startPhotosynthesis() < . >> class Pine : Tree()

    В интерфейсе можно определять свойства и функции с конкретной реализацией (по умолчанию). Классы, реализующие этот интерфейс, могут использовать реализацию по умолчанию или определить свою. При этом реализация свойств осуществляется с помощью метода доступа 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("Плод - $.") > else < println("$не плодоносит.") >

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

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

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

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

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

    Почему классы в 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()

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

    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" >

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

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

    enum class ColorType

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

    var color: ColorType color = ColorType.RED

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

    enum class ColorType

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

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

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

    enum class Color(val rgb: Int)

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

    enum class ProtocolState < WAITING < override fun signal() = TALKING >, TALKING < override fun signal() = WAITING >; abstract fun signal(): ProtocolState >

    Что такое 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 , чтобы вычислить их площади.

    Какая разница между 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 ) класс, который может обращаться к компонентам внешнего класса.

    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: $, Age: $") >

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

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

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

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

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

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

    typealias NodeSet = Set typealias FileTable = MutableMap>

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

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

    typealias Properties = Map

    Теперь вместо использования Map можно использовать 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 просто заменяет псевдоним на соответствующий тип при компиляции кода.

    Какие коллекции есть в 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() 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() linkedHashSet.add("Дуб") val hashSet = HashSet() 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() linkedHashMap.put(1, "Дуб") val hashMap = HashMap() 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()

    Промежуточные (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
    • собеседование вопросы
    • подготовка к собеседованию
    • котлин
    • андроид
    • faq
    • учебные материалы
    • android development
    • вопросы для собеседования
    • android

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *