Основные типы
В 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(): Byte
toShort(): Short
toInt(): Int
toLong(): Long
toFloat(): Float
toDouble(): Double
toChar(): Char
Часто необходимости в явных преобразованиях нет, поскольку тип выводится из контекста, а арифметические действия перегружаются для подходящих преобразований.
val l = 1L + 3 // Long + Int => Long
Операции
Котлин поддерживает стандартный набор арифметических операций над числами: +
, -
, *
, /
, %
.
Они объявляются членами соответствующих классов.
println(1 + 2)
println(2_500_000_000L - 1L)
println(3.14 * 2.71)
println(10.0 / 3)
Вы также можете переопределить эти операторы для пользовательских классов. См. Перегрузка операторов для деталей.
Деление целых чисел
Деление целых чисел всегда возвращает целое число. Любая дробная часть отбрасывается.
val x = 5 / 2
// println(x == 2.5) // ОШИБКА: Оператор '==' не может быть применен к 'Int' и 'Double'
println(x == 2) // true
Это справедливо для деления любых двух целочисленных типов.
val x = 5L / 2
println(x == 2L) // true
Чтобы вернуть тип с плавающей точкой, явно преобразуйте один из аргументов в тип с плавающей точкой.
val x = 5 / 2.toDouble()
println(x == 2.5) // true
Побитовые операции
Kotlin поддерживает обычный набор побитовых операций над целыми числами.
Они работают на двоичном уровне непосредственно с битовыми представлениями чисел.
Побитовые операции представлены функциями, которые могут быть вызваны в инфиксной форме.
Они могут быть применены только к Int
и Long
.
val x = (1 shl 2) and 0x000FF000
Ниже приведён полный список битовых операций:
shl(bits)
– сдвиг влево с учётом знака (<<
в 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
: массив беззнаковыхbyte
UShortArray
: массив беззнаковыхshort
UIntArray
: массив беззнаковыхint
ULongArray
: массив беззнаковыхlong
Как и целочисленные массивы со знаком, такие массивы предоставляют API, аналогичный классу Array
,
без дополнительных затрат на оборачивание.
При использовании массивов без знака вы получите предупреждение, что эта функция еще не стабильна.
Чтобы удалить предупреждение используйте аннотацию @ExperimentalUnsignedTypes
. Вам решать,
должны ли ваши пользователи явно соглашаться на использование вашего API, но имейте в виду,
что беззнаковый массив не является стабильной функцией, поэтому API, который он использует,
может быть нарушен изменениями в языке. Узнайте больше о требованиях регистрации.
Диапазоны и прогрессии поддерживаются для UInt
и ULong
классами UIntRange
,UIntProgression
, ULongRange
и ULongProgression
.
Вместе с целочисленными беззнаковыми типами эти классы стабильны.
Литералы
Чтобы целые беззнаковые числа было легче использовать, в Kotlin можно помечать целочисленный литерал суффиксом,
указывающим на определенный беззнаковый тип (аналогично Float
или Long
):
u
иU
помечают беззнаковые литералы. Точный тип определяется на основе ожидаемого типа. Если ожидаемый тип не указан, компилятор будет использоватьUInt
илиULong
в зависимости от размера литерала.
val b: UByte = 1u // UByte, есть ожидаемый тип
val s: UShort = 1u // UShort, есть ожидаемый тип
val l: ULong = 1u // ULong, есть ожидаемый тип
val a1 = 42u // UInt: ожидаемого типа нет, константе подходит тип UInt
val a2 = 0xFFFF_FFFF_FFFFu // ULong: ожидаемого типа нет, тип UInt не подходит константе
uL
andUL
явно помечают литерал как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 }