Основные типы
В 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. В Kotlin0и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, описанных в этом разделе, порядок приоритета следующий:
!xor(и другие инфиксные функции)&&||
В следующем примере компилятор вычисляет && раньше ||:
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(),arrayOfNulls()илиemptyArray() - конструктор
Array
В этом примере используется функция 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 для коллекций.