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

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

В этом разделе описаны основные типы, используемые в Kotlin:

Чтобы узнать о других типах Kotlin, таких как Nothing, Any и Unit, обратитесь к справочнику Kotlin API:

Подробнее о проверках и приведениях типов см. в разделе Проверки и приведения типов.

Числа

Числовые типы Kotlin представляют:

  • целые значения (Byte, Short, Int и Long)
  • значения с плавающей точкой (Float и Double)

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

Выбор числового типа

В большинстве случаев для выбора подходящего числового типа можно ориентироваться на следующие правила:

  • Используйте Int для целых чисел.
  • Используйте Long для целых чисел за пределами диапазона Int.
  • Используйте Double для десятичных чисел.
  • Используйте Float, когда допустима или требуется меньшая точность.
  • Используйте Byte и Short, когда этого требует API или формат данных.

Kotlin также предоставляет беззнаковые целочисленные типы как бета-функцию.

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

Kotlin предоставляет четыре целочисленных типа с разными размерами и диапазонами значений:

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

Объявление целочисленных значений

Kotlin поддерживает следующие формы литералов для целочисленных значений:

  • десятичные: 123
  • шестнадцатеричные: 0x0F
  • двоичные: 0b00001011

Kotlin не поддерживает восьмеричные литералы.

Чтобы объявить числовое значение, укажите тип явно:

val one: Int = 1

// Используйте нижние подчёркивания, чтобы повысить читаемость
val oneBillion: Long = 1_000_000_000
val hexBytes: Int = 0x7F_EC_DE_5E
val bytes: Int = 0b01010010_01101001_10010100_10010010

val oneByte: Byte = 1
val oneShort: Short = 1

Чтобы объявить значение Long, можно также добавить суффикс L:

val oneLong = 1L

Когда вы объявляете числовой тип явно, компилятор проверяет, что значение входит в диапазон этого типа:

// Значение входит в диапазон Byte
val oneByte: Byte = 1

// Ошибка: значение не входит в диапазон Byte
val tooBig: Byte = 128

Если вы не указываете числовой тип, Kotlin выводит Int, когда значение входит в диапазон Int. В остальных случаях Kotlin выводит Long:

val million = 1_000_000 // Int
val threeBillion = 3_000_000_000 // Long

Если значение может отсутствовать, используйте nullable-типы:

val maybeAbsent: Int? = null

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

Для чисел с дробной частью Kotlin предоставляет типы Float и Double.

Типы с плавающей точкой соответствуют стандарту IEEE 754. Float отражает одинарную точность, а Double — двойную.

Типы с плавающей точкой различаются размером и точностью:

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

Объявление значений с плавающей точкой

Чтобы объявить литерал с плавающей точкой, добавьте десятичную точку (.) или используйте экспоненциальную запись:

val pi = 3.14
val avogadro = 6.02214076e23

По умолчанию Kotlin выводит литералы с плавающей точкой как Double. Чтобы объявить Float, добавьте суффикс f или F:

val pi = 3.14 // Double
val eFloat = 2.7182817f // Float

Kotlin округляет литерал Float, если он содержит больше точности, чем может хранить Float.

Если значение может отсутствовать, используйте nullable-типы:

val maybeAbsent: Double? = null

Арифметические операции

Kotlin поддерживает стандартные арифметические операции над числами: +, -, *, / и %.

Используйте эти операторы для обычных вычислений:

fun main() {
    println(1 + 2) // 3
    println(2_500_000_000L - 1L) // 2499999999
    println(3.14 * 2.71) // 8.5094
    println(10.0 / 3) // 3.3333333333333335
}

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

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

Целочисленное деление

Деление целочисленных значений всегда возвращает целочисленный результат. Компилятор отбрасывает дробную часть:

fun main() {
    val intValue = 5 / 2
    println(intValue) // 2

    val longValue = 5L / 2
    println(longValue) // 2
}

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

fun main() {
    val a = 5 / 2.0
    println(a) // 2.5

    val b = 5 / 2.toDouble()
    println(b) // 2.5
}

Преобразование типов

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

Например, функция, ожидающая Double, не может принять значение Int или Float без преобразования:

fun main() {
    fun printDouble(x: Double) {
        print(x)
    }

    val x = 1.0
    val xInt = 1
    val xFloat = 1.0f
    val one: Double = 1 // Ошибка: несоответствие типа инициализатора

    printDouble(x) // OK
    printDouble(xInt) // Ошибка: несоответствие типа аргумента
    printDouble(xFloat) // Ошибка: несоответствие типа аргумента
}

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

  • toByte()
  • toShort()
  • toInt()
  • toLong()
  • toFloat()
  • toDouble()

Например, следующий код преобразует значение Int в Double:

fun main() {
    val intValue: Int = 1
    val doubleValue = intValue.toDouble()

    println(doubleValue) // 1.0
}

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

fun main() {
    val d: Double = 1.5
    val l: Long = d.toLong()

    println(l) // 1
}

Смешанные числовые выражения

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

val intNumber: Int = 1
val longNumber: Long = 1000
val result = intNumber + longNumber // 1001, Long

Если попытаться присвоить результат меньшему типу, компилятор сообщит об ошибке:

val intNumber: Int = 1
val longNumber: Long = 1000
val result: Int = intNumber + longNumber
// Ошибка: несоответствие типа инициализатора

Переполнение данных

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

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

Такое поведение может повлиять на результат вашего кода, даже если компилятор его принимает.

Переполнение в операциях

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

fun main() {
    val intNumber: Int = 2147483647
    // Максимальное значение Int равно 2147483647
    println(intNumber + 1) // -2147483648
}

Здесь результат переходит через границу диапазона, потому что значение больше не помещается в Int.

Компилятор не выдаёт ошибку автоматически при целочисленном переполнении.

Переполнение при смене знака

Переполнение также может возникнуть при смене знака. Например, положительный аналог Int.MIN_VALUE нельзя представить как Int.

fun main() {
    val min = Int.MIN_VALUE
    println(-min) // -2147483648
}

Сужающие преобразования

Когда вы преобразуете значение в меньший целочисленный тип, результат может не сохранить исходное число:

fun main() {
    val large: Int = 130
    val narrowed: Byte = large.toByte()

    println(narrowed) // -126
}

Однако, поскольку типы с плавающей точкой соответствуют стандарту IEEE 754, очень большие результаты могут становиться Infinity:

fun main() {
    println(Double.MAX_VALUE * 2) // Infinity
}

Побитовые операции

Kotlin предоставляет побитовые операции для Int и Long. Эти операции представлены набором инфиксных функций и функцией inv().

fun main() {
    val x = 1

    println(x shl 2) // 4
    println(x and 0x000FF000) // 0
}

Побитовые операции включают:

  • shl() — знаковый сдвиг влево
  • shr() — знаковый сдвиг вправо
  • ushr() — беззнаковый сдвиг вправо
  • and() — побитовое И
  • or() — побитовое ИЛИ
  • xor() — побитовое исключающее ИЛИ
  • inv() — побитовая инверсия

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

В Kotlin сравнение чисел с плавающей точкой зависит от статического типа операндов.

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

Однако в обобщённых сценариях (например, Any, Comparable<...> или Collection<T>) поведение отличается для операндов, которые статически не типизированы как числа с плавающей точкой. В этих случаях Kotlin использует реализации equals() и compareTo() для Float и Double.

В результате:

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

Следующий пример показывает различие между операндами, статически типизированными как числа с плавающей точкой, и операндами, используемыми через обобщённые типы:

fun generalizedEquals(a: Any, b: Any): Boolean {
    return a == b
}

fun main() {
    // Операнды статически типизированы как числа с плавающей точкой
    println(Double.NaN == Double.NaN) // false
    println(0.0 == -0.0) // true

    // Операнды используются через статический тип, не являющийся типом с плавающей точкой
    println(generalizedEquals(Double.NaN, Double.NaN)) // true
    println(generalizedEquals(0.0, -0.0)) // false
}

Упаковка и кэширование чисел в JVM

В JVM числовые значения, не допускающие null, обычно хранятся с помощью примитивных типов, таких как int, long или double. Однако при использовании обобщённых типов или nullable-числовых типов, таких как Int?, значение упаковывается и представляется как объект.

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

Например, JVM кэширует упакованные значения Integer в диапазоне от -128 до 127. Поэтому следующий код возвращает true:

fun main() {
    val score: Int = 100
    val savedScore: Int? = score
    val displayedScore: Int? = score

    println(savedScore === displayedScore) // true
}

Для значений вне кэшируемого диапазона упакованные значения являются отдельными объектами. В этом случае они не равны по ссылке, даже если их значения структурно равны. По этой причине для сравнения числовых значений используйте ==:

fun main() {
    val score: Int = 10000
    val savedScore: Int? = score
    val displayedScore: Int? = score

    println(savedScore === displayedScore) // false
    println(savedScore == displayedScore) // true
}

Беззнаковые целочисленные типы

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

Тип Размер (биты) Минимальное значение Максимальное значение
UByte 8 0 255
UShort 16 0 65,535
UInt 32 0 4,294,967,295 (232 - 1)
ULong 64 0 18,446,744,073,709,551,615 (264 - 1)

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

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

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

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

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

  • UByteArray: массив беззнаковых байтов.
  • UShortArray: массив беззнаковых short-значений.
  • UIntArray: массив беззнаковых int-значений.
  • ULongArray: массив беззнаковых long-значений.

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

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

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

Беззнаковые целочисленные литералы

Чтобы упростить использование беззнаковых целых чисел, к целочисленному литералу можно добавить суффикс, указывающий определённый беззнаковый тип (аналогично F для Float или L для 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 и UL явно указывают, что литерал должен быть беззнаковым Long:
val a = 1UL // ULong, хотя ожидаемый тип не задан и константа помещается в UInt

Сценарии использования

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

data class Color(val representation: UInt)

val yellow = Color(0xFFCC00CCu)

Беззнаковые числа можно использовать для инициализации массивов байтов без явных преобразований литералов через toByte():

val byteOrderMarkUtf8 = ubyteArrayOf(0xEFu, 0xBBu, 0xBFu)

Ещё один сценарий — взаимодействие с нативными API. Kotlin позволяет представлять нативные объявления, сигнатура которых содержит беззнаковые типы. Такое отображение не заменяет беззнаковые целые числа на знаковые и сохраняет семантику без изменений.

Не цели

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

На это есть несколько причин:

  • Использование знаковых целых чисел помогает обнаруживать случайные переполнения и сигнализировать об ошибочных состояниях, например List.lastIndex равен -1 для пустого списка.
  • Беззнаковые целые числа нельзя считать версией знаковых чисел с ограниченным диапазоном, потому что их диапазон значений не является подмножеством диапазона знаковых целых чисел. Ни знаковые, ни беззнаковые целые числа не являются подтипами друг друга.

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

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

Используйте значения Boolean в функциях, которые отвечают на вопросы вида «да или нет», а также в условиях while, if и when.

Объявление переменной Boolean

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

val isTrue: Boolean = true
val isFalse = false // Kotlin выводит Boolean

Если значение может быть null, используйте Boolean?:

val isEnabled: Boolean? = null

Нельзя присвоить целочисленное значение переменной Boolean. В Kotlin 0 и 1 не являются значениями Boolean.

Получение значений Boolean

Для получения значений Boolean можно использовать выражения сравнения и функции:

fun main() {
    val number = 10
    val isPositive = number > 0
    println(isPositive) // true

    val language = "Kotlin"
    val isEmpty = language.isEmpty()
    println(isEmpty) // false
}

Результаты также можно использовать в условиях и других выражениях:

fun main() {
    val number = 10
    val isPositive = number > 0 // true

    if (isPositive) {
        println("The number is positive.")
    }
}

Операции с Boolean

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

Отрицание (NOT)

Оператор NOT инвертирует значение Boolean. Чтобы использовать NOT, поставьте оператор ! перед значением Boolean:

val isOn = true
val isOff = !isOn // isOff равно false

Логическое AND

Оператор AND возвращает true только если оба операнда равны true. Чтобы использовать логическое AND, поместите оператор && между операндами:

val a = false && false // false
val b = false && true // false
val c = true && false // false
val d = true && true  // true

Если первый операнд равен false, оператор && пропускает второй операнд. Чтобы вычислить оба операнда, используйте вместо него инфиксную функцию and.

Логическое OR

Оператор OR возвращает true, если хотя бы один операнд равен true. Чтобы использовать логическое OR, поместите оператор || между операндами:

val a = false || false // false
val b = false || true  // true
val c = true || false  // true
val d = true || true   // true

Если первый операнд равен true, оператор || пропускает второй операнд. Чтобы вычислить оба операнда, используйте вместо него инфиксную функцию or.

Исключающее OR (XOR)

Операция исключающего OR (XOR) возвращает true, если операнды имеют разные значения. Чтобы использовать XOR, напишите xor между операндами:

val a = false xor false // false
val b = false xor true  // true
val c = true xor false  // true
val d = true xor true   // false

xor — это инфиксная функция, а не оператор. Подробнее о функциях Boolean см. в справочнике API.

Приоритет операторов

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

Для операций Boolean, описанных в этом разделе, порядок приоритета следующий:

  1. !
  2. xor (и другие инфиксные функции)
  3. &&
  4. ||

В следующем примере компилятор вычисляет && раньше ||:

fun main() {
    val result = true || false && false
    println(result) // true
}

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

fun main() {
    val result = (true || false) && false
    println(result) // false
}

Boolean в условиях

if, when и while вычисляют выражения Boolean, чтобы управлять ходом программы.

Выражения if

fun main() {
    val number = 4
    val isEven = number % 2 == 0

    // Условие уже имеет тип `Boolean`
    // Его не нужно сравнивать с `true` или `false`
    if (isEven) {
        println("The number is even.")
    } else {
        println("The number is odd.")
    }
}

Выражения when

fun main() {
    val number = 3

    when {
        number > 0 -> println("The number is positive.")
        number < 0 -> println("The number is negative.")
        else -> println("The number is zero.")
    }
}

Циклы while

fun main() {
    var isCalculating = true

    while (isCalculating) {
        println("Calculating...")
        isCalculating = false
    }
}

Символы

Тип Char представляет один символ как кодовую единицу UTF-16.

Используйте Char для отдельных символьных значений: букв, цифр, знаков препинания или пробельных символов. Для последовательностей символов используйте String.

Char не является числовым типом, но у каждого символа есть числовое значение Unicode, к которому можно получить доступ. См. раздел Преобразование символов.

Синтаксис

Чтобы объявить символ, заключите значение в одинарные кавычки (' '). Можно указать тип Char явно или позволить Kotlin вывести его из значения:

val letter: Char = 'a'

// Kotlin выводит Char, потому что значения записаны в одинарных кавычках
val digit = '1'
val symbol = '!'
val space = ' '
val separator = ':'

Символьный литерал должен содержать ровно один символ. В противном случае компилятор Kotlin сообщит об ошибке:

val invalid = 'AB' // Ошибка
val invalidEmpty = '' // Ошибка

Nullable-значения

Чтобы хранить nullable-значение, используйте Char?:

val maybeAbsent: Char? = null

В JVM nullable-значения Char упаковываются при необходимости. То же относится к числовым типам.

Поддержка Unicode

Kotlin представляет значения Char как кодовые единицы UTF-16. Это означает, что один Char хранит одну кодовую единицу UTF-16, а не обязательно один полный символ Unicode.

Basic Multilingual Plane

Один Char может хранить значения в диапазоне от \u0000 до \uFFFF. Этот диапазон покрывает Basic Multilingual Plane (BMP), включающий символы почти всех современных языков и большое количество знаков.

Чтобы задать символ по значению Unicode, используйте \u, за которым следует четырёхзначное шестнадцатеричное значение из таблицы Unicode:

val unicodeNumber = '\u0031' // Равно '1'

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

Символы Unicode за пределами BMP, например эмодзи и некоторые исторические письменности, нельзя представить одним Char. В UTF-16 они кодируются суррогатной парой: два значения Char вместе представляют один символ Unicode в String:

fun main() {
    val emoji = "🥦"

    println(emoji.length) // 2
    println(emoji[0])     // Первый суррогат
    println(emoji[1])     // Второй суррогат
}

Чтобы обрабатывать 32-битные символы по отдельности, используйте кодовые точки Unicode, хранящиеся как значения Int.

Escape-последовательности

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

Поддерживаемая последовательность Описание
\t Табуляция
\b Backspace
\n Новая строка (LF)
\r Возврат каретки (CR)
\' Одинарная кавычка
\" Двойная кавычка
\\ Обратный слеш
\$ Знак доллара

Например:

val newLine = '\n'
val dollar = '\$'
val backslash = '\\'

Операции

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

Сравнение символов

Чтобы сравнивать значения Char, используйте стандартные операторы сравнения, такие как ==, !=, <, >, <= и >=. Kotlin сравнивает значения Char по их числовым значениям Unicode и возвращает значение Boolean:

val before = 'a' < 'b' // true
val after = 'c' > 'd' // false
val different = 'A' == 'a' // false
val equal = 'A' == 'A' // true

Обработка символов

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

fun main() {
    val myChar = 'A'
    // Проверяет, представляет ли символ цифру
    println(myChar.isDigit()) // false
    // Проверяет, представляет ли символ заглавную букву
    println(myChar.isUpperCase()) // true
    // Возвращает версию в нижнем регистре
    println(myChar.lowercaseChar()) // 'a'
}

Подробнее о доступных функциях см. в справочнике API.

Арифметика символов

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

fun main() {
    val a = 'a'

    println(a + 1)  // b
    println(a + 2)  // c
    println(a - 32) // A
}

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

С изменяемыми переменными также можно использовать операторы инкремента (++) и декремента (--) в префиксной и постфиксной формах:

fun main() {
    var a = 'A'

    a += 10
    println(a)   // 'K'

    println(++a) // 'L'  префиксный инкремент
    println(a++) // 'L'  постфиксный инкремент
    println(a)   // 'M'

    println(--a) // 'L'  префиксный декремент
    println(a--) // 'L'  постфиксный декремент
    println(a)   // 'K'
}

Преобразование символов

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

  • Используйте .code, чтобы получить числовое значение Unicode для символа:
fun main() {
    val letter = 'A'
    println(letter.code) // 65
}
  • Если символ представляет десятичную цифру, используйте digitToInt():
fun main() {
    val digit = '7'
    println(digit.digitToInt()) // 7
}

Если символ может не быть допустимой цифрой, используйте digitToIntOrNull().

Строки

Строки в Kotlin представлены типом String.

В JVM объект типа String в кодировке UTF-16 использует примерно 2 байта на символ.

Как правило, строковое значение — это последовательность символов в двойных кавычках ("):

val str = "abcd 123"

Элементы строки — это символы, к которым можно обратиться с помощью операции индексированного доступа: s[i]. По ним можно пройти циклом for:

fun main() {
    val str = "abcd"
    for (c in str) {
        println(c)
    }
}

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

fun main() {
    val str = "abcd"

    // Создаёт и выводит новый объект String
    println(str.uppercase())
    // ABCD

    // Исходная строка остаётся прежней
    println(str)
    // abcd
}

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

fun main() {
    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(">").

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

Строковые литералы могут содержать шаблонные выражения — фрагменты кода, которые вычисляются, а их результаты объединяются в строку. При обработке шаблонного выражения Kotlin автоматически вызывает функцию .toString() для результата выражения, чтобы преобразовать его в строку. Шаблонное выражение начинается со знака доллара ($) и состоит либо из имени переменной:

fun main() {
    val i = 10
    println("i = $i")
    // i = 10

    val letters = listOf("a", "b", "c", "d", "e")
    println("Letters: $letters")
    // Letters: [a, b, c, d, e]
}

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

fun main() {
    val s = "abc"
    println("$s.length is ${s.length}")
    // abc.length is 3
}

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

val price = """
${'$'}_9.99
"""

Чтобы не использовать последовательности ${'$'} в строках, можно воспользоваться экспериментальной многодолларовой строковой интерполяцией.

Многодолларовая строковая интерполяция

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

В однострочных строках можно экранировать литералы, но многострочные строки в Kotlin не поддерживают экранирование обратным слешом. Чтобы включить знаки доллара ($) как литеральные символы, нужно использовать конструкцию ${'$'}, предотвращающую строковую интерполяцию. Такой подход может ухудшать читаемость кода, особенно когда строки содержат несколько знаков доллара.

Многодолларовая строковая интерполяция упрощает это, позволяя рассматривать знаки доллара как литеральные символы и в однострочных, и в многострочных строках. Например:

val KClass<*>.jsonSchema : String
    get() = $$"""
    {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$id": "https://example.com/product.schema.json",
      "$dynamicAnchor": "meta",
      "title": "$${simpleName ?: qualifiedName ?: "unknown"}",
      "type": "object"
    }
    """

Здесь префикс $$ указывает, что для запуска строковой интерполяции требуются два последовательных знака доллара. Одиночные знаки доллара остаются литеральными символами.

Можно настроить, сколько знаков доллара запускает интерполяцию. Например, три последовательных знака доллара ($$$) позволяют $ и $$ оставаться литералами, а интерполяцию включают с помощью $$$:

val productName = "carrot"
val requestedData =
    $$$"""{
      "currency": "$",
      "enteredAmount": "42.45 $$",
      "$$serviceField": "none",
      "product": "$$$productName"
    }
    """

println(requestedData)
//{
//    "currency": "$",
//    "enteredAmount": "42.45 $$",
//    "$$serviceField": "none",
//    "product": "carrot"
//}

Здесь префикс $$$ позволяет строке включать $ и $$ без конструкции ${'$'} для экранирования.

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

Форматирование строк

Форматирование строк с помощью функции String.format() доступно только в Kotlin/JVM.

Чтобы отформатировать строку под конкретные требования, используйте функцию String.format().

Функция String.format() принимает строку формата и один или несколько аргументов. Строка формата содержит плейсхолдер для заданного аргумента (обозначается %), за которым следуют спецификаторы формата. Спецификаторы формата — это инструкции форматирования для соответствующего аргумента; они состоят из флагов, ширины, точности и типа преобразования. Вместе спецификаторы формата задают вид вывода. Часто используются спецификаторы %d для целых чисел, %f для чисел с плавающей точкой и %s для строк. Также можно использовать синтаксис argument_index$, чтобы несколько раз ссылаться на один и тот же аргумент в строке формата в разных форматах.

Подробное описание и полный список спецификаторов формата см. в документации Java Class Formatter.

Рассмотрим пример:

fun main() {
    // Форматирует целое число, добавляя ведущие нули до длины в семь символов
    val integerNumber = String.format("%07d", 31416)
    println(integerNumber)
    // 0031416

    // Форматирует число с плавающей точкой со знаком + и четырьмя знаками после запятой
    val floatNumber = String.format("%+.4f", 3.141592)
    println(floatNumber)
    // +3.1416

    // Форматирует две строки в верхнем регистре, каждая занимает один плейсхолдер
    val helloString = String.format("%S %S", "hello", "world")
    println(helloString)
    // HELLO WORLD

    // Форматирует отрицательное число в скобках, затем повторяет это же число
    // в другом формате (без скобок), используя `argument_index$`.
    val negativeNumberInParentheses = String.format("%(d means %1\$d", -31416)
    println(negativeNumberInParentheses)
    // (31416) means -31416
}

Функция String.format() предоставляет возможности, похожие на строковые шаблоны. Однако String.format() более гибкая, потому что поддерживает больше вариантов форматирования.

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

Будьте осторожны при использовании String.format(): в ней легко ошибиться с количеством или позицией аргументов относительно соответствующих плейсхолдеров.

Массивы

Массив — это структура данных, которая хранит фиксированное количество значений одного типа или его подтипов. Самый распространённый тип массива в Kotlin — массив объектного типа, представленный классом Array.

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

Когда использовать массивы

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

У коллекций по сравнению с массивами есть следующие преимущества:

  • Коллекции могут быть read-only, что даёт больше контроля и помогает писать надёжный код с понятным намерением.
  • В коллекции легко добавлять элементы и удалять их. Массивы, напротив, имеют фиксированный размер. Единственный способ добавить элемент в массив или удалить его — каждый раз создавать новый массив, что очень неэффективно:
fun main() {
    var riversArray = arrayOf("Nile", "Amazon", "Yangtze")

    // Операция присваивания += создаёт новый riversArray,
    // копирует исходные элементы и добавляет "Mississippi"
    riversArray += "Mississippi"
    println(riversArray.joinToString())
    // Nile, Amazon, Yangtze, Mississippi
}
  • Для проверки структурного равенства коллекций можно использовать оператор равенства (==). Для массивов этот оператор использовать нельзя. Вместо этого нужна специальная функция, подробнее см. в разделе Сравнение массивов.

Подробнее о коллекциях см. в разделе Обзор коллекций.

Создание массивов

Для создания массивов в Kotlin можно использовать:

В этом примере используется функция arrayOf(), которой передаются значения элементов:

fun main() {
    // Создаёт массив со значениями [1, 2, 3]
    val simpleArray = arrayOf(1, 2, 3)
    println(simpleArray.joinToString())
    // 1, 2, 3
}

В этом примере функция arrayOfNulls() создаёт массив заданного размера, заполненный элементами null:

fun main() {
    // Создаёт массив со значениями [null, null, null]
    val nullArray: Array<Int?> = arrayOfNulls(3)
    println(nullArray.joinToString())
    // null, null, null
}

В этом примере функция emptyArray() создаёт пустой массив:

var exampleArray = emptyArray<String>()

Благодаря выводу типов Kotlin тип пустого массива можно указать слева или справа от присваивания.

Например:

> var exampleArray = emptyArray<String>()
>
> var exampleArray: Array<String> = emptyArray()
> ```

<!-- The `Array` constructor takes the array size and a function that returns values for array elements given its index: -->
Конструктор `Array` принимает размер массива и функцию, которая возвращает значения элементов массива по их индексу:

```kotlin
fun main() {
    // Создаёт Array<Int>, инициализированный нулями [0, 0, 0]
    val initArray = Array<Int>(3) { 0 }
    println(initArray.joinToString())
    // 0, 0, 0

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

Как и в большинстве языков программирования, индексы в Kotlin начинаются с 0.

Вложенные массивы

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

fun main() {
    // Создаёт двумерный массив
    val twoDArray = Array(2) { Array<Int>(2) { 0 } }
    println(twoDArray.contentDeepToString())
    // [[0, 0], [0, 0]]

    // Создаёт трёхмерный массив
    val threeDArray = Array(3) { Array(3) { Array<Int>(3) { 0 } } }
    println(threeDArray.contentDeepToString())
    // [[[0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0]]]
}

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

Доступ к элементам и их изменение

Массивы всегда изменяемы. Чтобы обращаться к элементам массива и изменять их, используйте оператор индексированного доступа []:

fun main() {
    val simpleArray = arrayOf(1, 2, 3)
    val twoDArray = Array(2) { Array<Int>(2) { 0 } }

    // Обращается к элементу и изменяет его
    simpleArray[0] = 10
    twoDArray[0][0] = 2

    // Выводит изменённый элемент
    println(simpleArray[0].toString()) // 10
    println(twoDArray[0][0].toString()) // 2
}

Массивы в Kotlin инвариантны. Это значит, что Kotlin не позволяет присвоить Array<String> переменной типа Array<Any>, чтобы предотвратить возможный сбой во время выполнения. Вместо этого можно использовать Array<out Any>. Подробнее см. в разделе Проекции типов.

Работа с массивами

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

Передача переменного количества аргументов в функцию

В Kotlin можно передать в функцию переменное количество аргументов с помощью параметра vararg. Это полезно, когда количество аргументов заранее неизвестно, например при форматировании сообщения или создании SQL-запроса.

Чтобы передать в функцию массив с переменным количеством аргументов, используйте оператор распаковки (*). Он передаёт каждый элемент массива как отдельный аргумент выбранной функции:

fun main() {
    val lettersArray = arrayOf("c", "d")
    printAllStrings("a", "b", *lettersArray)
    // abcd
}

fun printAllStrings(vararg strings: String) {
    for (string in strings) {
        print(string)
    }
}

Подробнее см. в разделе Переменное количество аргументов (varargs).

Сравнение массивов

Чтобы проверить, содержат ли два массива одинаковые элементы в одинаковом порядке, используйте функции .contentEquals() и .contentDeepEquals():

fun main() {
    val simpleArray = arrayOf(1, 2, 3)
    val anotherArray = arrayOf(1, 2, 3)

    // Сравнивает содержимое массивов
    println(simpleArray.contentEquals(anotherArray))
    // true

    // Используя инфиксную запись, сравнивает содержимое массивов
    // после изменения элемента
    simpleArray[0] = 10
    println(simpleArray contentEquals anotherArray)
    // false
}

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

Преобразование массивов

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

Сумма

Чтобы вернуть сумму всех элементов массива, используйте функцию .sum():

fun main() {
    val sumArray = arrayOf(1, 2, 3)

    // Суммирует элементы массива
    println(sumArray.sum())
    // 6
}

Функцию .sum() можно использовать только с массивами числовых типов данных, например Int.

Перемешивание

Чтобы случайным образом перемешать элементы массива, используйте функцию .shuffle():

fun main() {
    val simpleArray = arrayOf(1, 2, 3)

    // Перемешивает элементы [3, 2, 1]
    simpleArray.shuffle()
    println(simpleArray.joinToString())

    // Снова перемешивает элементы [2, 3, 1]
    simpleArray.shuffle()
    println(simpleArray.joinToString())
}

Преобразование массивов в коллекции

Если вы работаете с разными API, где одни используют массивы, а другие — коллекции, вы можете преобразовывать массивы в коллекции и обратно.

Преобразование в List или Set

Чтобы преобразовать массив в List или Set, используйте функции .toList() и .toSet().

fun main() {
    val simpleArray = arrayOf("a", "b", "c", "c")

    // Преобразует в Set
    println(simpleArray.toSet())
    // [a, b, c]

    // Преобразует в List
    println(simpleArray.toList())
    // [a, b, c, c]
}

Преобразование в Map

Чтобы преобразовать массив в Map, используйте функцию .toMap(). В Map можно преобразовать только массив Pair<K,V>. Первое значение экземпляра Pair становится ключом, а второе — значением. В этом примере используется инфиксная запись для вызова функции to, создающей кортежи Pair:

fun main() {
    val pairArray = arrayOf("apple" to 120, "banana" to 150, "cherry" to 90, "apple" to 140)

    // Преобразует в Map
    // Ключи — фрукты, значения — количество калорий
    // Обратите внимание: ключи должны быть уникальными, поэтому последнее значение "apple"
    // перезаписывает первое
    println(pairArray.toMap())
    // {apple=140, banana=150, cherry=90}
}

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

Если использовать класс Array с примитивными значениями, эти значения упаковываются в объекты. В качестве альтернативы можно использовать массивы примитивных типов, которые позволяют хранить примитивы в массиве без побочного эффекта в виде накладных расходов на упаковку:

Массив примитивного типа Эквивалент в Java
BooleanArray boolean[]
ByteArray byte[]
CharArray char[]
DoubleArray double[]
FloatArray float[]
IntArray int[]
LongArray long[]
ShortArray short[]

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

В этом примере создаётся экземпляр класса IntArray:

fun main() {
    // Создаёт массив Int размером 5 со значениями, инициализированными нулями
    val exampleArray = IntArray(5)
    println(exampleArray.joinToString())
    // 0, 0, 0, 0, 0
}

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

Чтобы преобразовать массивы объектных типов в массивы примитивных типов, используйте .toBooleanArray(), .toByteArray(), .toCharArray() и так далее.

Что дальше

  • Чтобы подробнее узнать, почему для большинства случаев рекомендуется использовать коллекции, прочитайте Обзор коллекций.
  • Если вы Java-разработчик, прочитайте руководство по миграции с Java на Kotlin для коллекций.