Основные типы
В 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) |
Все переменные, инициализированные целыми значениями, не превышающими максимальное значение 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
- Тип
- Шестнадцатеричные числа:
0x0F - Двоичные числа:
0b00001011
ВНИМАНИЕ: Восьмеричные литералы не поддерживаются.
Также 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(): BytetoShort(): ShorttoInt(): InttoLong(): LongtoFloat(): FloattoDouble(): DoubletoChar(): 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)– сдвиг влево с учётом знака (<<в Java)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 - Создание диапазона и проверка диапазона:
a..b,x in a..b,x !in a..b
Когда статически известно, что операнды a и b являются Float или Double
или их аналогами с nullable-значением (тип объявлен или является результатом
умного приведения), операции с числами и диапазоном, который они образуют,
соответствуют стандарту IEEE 754 для арифметики с плавающей точкой.
Однако для поддержки общих вариантов использования и обеспечения полного упорядочивания,
когда операнды статически не объявлены как числа с плавающей запятой (например, Any, Comparable<...>,
параметр типа), операции используют реализации equals и compareTo для Float и Double,
которые не согласуются со стандартом, так что:
NaNсчитается равным самому себеNaNсчитается больше, чем любой другой элемент, включая “POSITIVE_INFINITY”-0.0считается меньше, чем0.0
Целые беззнаковые числа
В дополнение к целочисленным типам, в Kotlin есть следующие типы целых беззнаковых чисел:
UByte: беззнаковое 8-битное целое число, в диапазоне от 0 до 255UShort: беззнаковое 16-битное целое число, в диапазоне от 0 до 65535UInt: беззнаковое 32-битное целое число, в диапазоне от 0 до 232 - 1ULong: беззнаковое 64-битное целое число, в диапазоне от 0 до 264 - 1
Беззнаковые типы поддерживают большинство операций своих знаковых аналогов.
Изменение типа с беззнакового типа на его знаковый аналог (и наоборот) является двоично несовместимым изменением.
Беззнаковые массивы и диапазоны
Беззнаковые массивы и операции над ними находятся в стадии бета-тестирования. Они могут быть несовместимо изменены в любое время.
Как и в случае с примитивами, каждому типу без знака соответствует тип массивов знаковых типов:
UByteArray: массив беззнаковыхbyteUShortArray: массив беззнаковыхshortUIntArray: массив беззнаковыхintULongArray: массив беззнаковых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 не подходит константе
uLandULявно помечают литерал как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)
В JVM: nullable-ссылки на логические объекты заключены в рамки аналогично числам.
Символы
Символы в Kotlin представлены типом Char. Символьные литералы заключаются в одинарные кавычки: '1'.
Специальные символы начинаются с обратного слеша \.
Поддерживаются следующие escape-последовательности: \t, \b, \n, \r, \', \", \\ и \$.
Для кодирования любого другого символа используйте синтаксис escape-последовательности Юникода: '\uFF00'.
val aChar: Char = 'a'
println(aChar)
println('\n') // выводит дополнительный символ новой строки
println('\uFF00')
Если значение символьной переменной – цифра, её можно явно преобразовать в Int с помощью функции
digitToInt().
В JVM: Подобно числам, символы оборачиваются при необходимости использования nullable-ссылки. При использовании обёрток тождественность (равенство по ссылке) не сохраняется.
Строки
Строки в Kotlin представлены типом String. Как правило, строка представляет собой последовательность
символов в двойных кавычках (").
val str = "abcd 123"
Строки состоят из символов, которые могут быть получены по порядковому номеру: s[i].
Проход по строке выполняется циклом for.
for (c in str) {
println(c)
}
Строки являются неизменяемыми. После инициализации строки вы не можете изменить ее значение
или присвоить ей новое. Все операции, преобразующие строки, возвращают новый объект 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 ${s.length}") // выведет "abc.length is 3"
Шаблоны поддерживаются как в обычных, так и в экранированных строках.
При необходимости вставить символ $ в обычную строку (такие строки не поддерживают экранирование
обратным слешом) перед любым символом, который разрешен в качестве начала идентификатора,
используйте следующий синтаксис:
val price = """
${'$'}_9.99
"""
Массивы
Массивы в Kotlin представлены классом Array, обладающим функциями get и set (которые обозначаются [] согласно соглашению о перегрузке операторов), и свойством size, а также несколькими полезными встроенными функциями.
class Array<T> private constructor() {
val size: Int
operator fun get(index: Int): T
operator fun set(index: Int, value: T): Unit
operator fun iterator(): Iterator<T>
// ...
}
Для создания массива используйте функцию arrayOf(), которой в качестве аргумента передаются элементы массива,
т.е. выполнение arrayOf(1, 2, 3) создаёт массив [1, 2, 3].
С другой стороны функция arrayOfNulls() может быть использована для создания массива заданного размера, заполненного значениями null.
Также для создания массива можно использовать фабричную функцию, которая принимает размер массива и функцию, возвращающую начальное значение каждого элемента по его индексу.
// создаёт массив типа Array<String> со значениями ["0", "1", "4", "9", "16"]
val asc = Array(5) { i -> (i * i).toString() }
asc.forEach { println(it) }
Как отмечено выше, оператор [] используется вместо вызовов встроенных функций get() и set().
Обратите внимание: в отличие от Java массивы в Kotlin являются инвариантными. Это значит,
что Kotlin запрещает нам присваивать массив Array<String> переменной типа Array<Any>,
предотвращая таким образом возможный отказ во время исполнения (хотя вы можете использовать Array<out Any>,
см. Проекции типов).
Массивы примитивных типов
Также в 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) { it * 1 }