Документация на Kotlin: https://kotlinlang.org/docs/reference/basic-syntax.html.
Kotlin — это относительно молодой язык от российской компании JetBrains. Появился он в 2011 году.
Для нас он интересен тем, что на конференции Google I/O 2017 команда разработчиков Android сообщила, что Kotlin получил официальную поддержку для разработки Android-приложений. Сейчас Kotlin становится основным языком для разработки под Android. Именно его поддержке уделяется наибольшее внимание.
Основные возможности и преимущества Kotlin:
IntelliJ IDEA — среда разработки, созданная компанией JetBrains. Сейчас повсеместно используется для разработки Java- и Kotlin-приложений и не только.
В Kotlin не существует примитивных типов в привычном их понимании. В Kotlin всё является объектом, в том смысле, что пользователь может вызвать метод или получить доступ к свойству любой переменной. Некоторые типы являются встроенными (подобные примитивным типам в Java), хотя для пользователя они могут выглядеть как обычные классы.
Kotlin работает с численными типами так же как и Java. Для представления чисел в Kotlin используются следующие встроенные типы:
Числовые константы описываются следующим образом.
Десятичные числа: 123
.
Long-тип обозначаются заглавной буквой L
: 123L
.
Шестнадцатеричные числа имеют приставку 0x
: 0x0F
.
Двоичные числа имеют приставку 0b
: 0b00001011
.
Чтобы сделать числовые константы более удобными к чтению, можно использовать нижние подчеркивания для разделения разрядов. Например:
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
Меньшие типы являются подтипами больших типов.
// Возможный код, который на самом деле не скомпилируется:
val a: Int = 1 // "Int (java.lang.Integer)
val b: Long = a // Неявное преобразование возвращает Long (java.lang.Long)
println(a == b) // Данное выражение выведет "false" т.к. метод equals() типа Long предполагает, что вторая часть выражения также имеет тип Long
Для выполнения корректного сравнения по значению необходимо выполнять преобразования типов:
a.toLong() // Приведение Int к Long
// или
b.toInt() // Приведение Long к Int
Каждый численный тип поддерживает следующие преобразования:
toByte(): Byte
toShort(): Short
toInt(): Int
toLong(): Long
toFloat(): Float
toDouble(): Double
При выполнении арифметических действий с разными типами выполняется преобразование результата к наибольшему типу.
val l = 1L + 3 // Long + Int => Long
Символы в Kotlin представлены типом Char
. Они не являются числами, однако могут быть преобразованы к Int
.
Символьные литералы записываются в одинарных кавычках: '1', '\n', '\uFF00'. Мы можем явно привести символ в число типа Int:
val c = 'A'
val num: Int = c.toInt() // Явное преобразование в число
println(num)
Тип Boolean
представляет логический тип данных и принимает два значения: true
и false
.
Встроенные действия над логическими переменными те же, что и в Java:
||
– логическое ИЛИ,&&
– логическое И,!
- отрицание.Массивы в Kotlin представлены классом Array<T>
. Массивы обладают функциями get
и set
(которые обозначаются [] согласно соглашению о перегрузке операторов), и свойством size
, а также несколькими полезными встроенными функциями.
Для создания массива можно использовать библиотечную функцию arrayOf()
, которой в качестве аргумента передаются элементы массива, т.е. выполнение arrayOf(1, 2, 3)
создаёт массив [1, 2, 3]
. Также существует библиотечная функция arrayOfNulls()
. Она может быть использована для создания массива, заполненного значениями null
.
val arr: Array<Int> = arrayOf(1, 2, 3)
println(arr.contentToString()) // Вывод содержимого массива в лог
Также в Kotlin есть особые классы для представления массивов примитивных типов без дополнительных затрат на оборачивание: ByteArray
, ShortArray
, IntArray
, Boolean
и т.д. Данные классы не наследуют класс Array
, хотя и обладают тем же набором методов и свойств. У каждого из них есть соответствующая фабричная функция:
val x: IntArray = intArrayOf(1, 2, 3)
x[0] = x[1] + x[2] // [5, 2, 3]
Строки в Kotlin представлены типом String
. Строки являются неизменяемыми. Строки состоят из символов, которые могут быть получены с помощью операции индексирования: s[i]
.
Строки могут содержать шаблонные выражения, т.е. участки кода, которые выполняются, а полученный результат встраивается в строку. Шаблон начинается со знака доллара ($) и состоит либо из простого имени переменной, либо из произвольного выражения в фигурных скобках.
Примеры:
val i = 10
val s = "i = $i" // Строка "i = 10"
val s = "abc"
val str = "$s.length is ${s.length}" // Строка "abc.length is 3"
При необходимости использования символ $
может быть представлен с помощью следующего синтаксиса:
val price = "${'$'}9.99"
Файл с исходным кодом может начинаться с объявления пакета:
package org.example.kotlinlearn
fun someFunction() {}
class SomeClass {}
// ...
Всё содержимое файла с исходниками (например, классы и функции) располагается в объявленном пакете. Таким образом, в приведённом выше примере полное имя функции someFunction()
будет org.example.kotlinlearn.someFunction
, а полное имя класса SomeClass
- org.example.kotlinlearn.SomeClass
.
Каждый файл может содержать свои собственные объявления импорта. Можно импортировать одно имя, например:
import org.example.kotlinlearn.SomeClass
// теперь SomeClass можно использовать без указания пакета
или доступное содержимое пространства имён (пакет, класс, объект и т.д.):
import org.example.kotlinlearn.*
// всё в 'org.example.kotlinlearn' становится доступно без указания пакета
Переменные в Kotlin бывают изменяемые и неизменяемые.
Неизменяемые (только для чтения) переменные объявляются с помощью ключевого слова val
:
val a: Int = 1
val b = 1 // Тип Int выведен автоматически
val c: Int // Тип обязателен, когда значение не инициализируется
c = 1 // Последующее присвоение
c = 2 // Изменение невозможно, ошибка при компиляции
Изменяемые переменные объявляются с помощью ключевого слова var
:
var x = 5 // Тип Int выведен автоматически
x += 1
if
В языке Kotlin if
является выражением, т.е. оно возвращает значение. Это позволяет отказаться от тернарного оператора (условие ? условие истинно : условие ложно), поскольку выражению if
может его заменить.
// обычное использование
var max = a
if (a < b)
max = b
// с блоком else
var max: Int
if (a > b)
max = a
else
max = b
// в виде выражения
val max = if (a > b) a else b
"Ветви" выражения if
могут содержать несколько строк кода, при этом последнее выражение является значением блока и может быть возвращено:
val max = if (a > b) {
println("возвращаем a")
a
}
else {
println("возвращаем b")
b
}
println(max) // Напечатает "возвращаем a", если a > b, и "возвращаем b", если a <= b.
Если конструкция if
используется в качестве выражения (например, возвращая его значение или присваивая его переменной), то использование ветки else
обязательно.
when
Ключевое слово when
призвано заменить оператор switch
, присутствующий в C-подобных языках. В простейшем виде его использование выглядит так:
when (x) {
1 -> println("x == 1")
2 -> println("x == 2")
else -> {
println("x is neither 1 nor 2")
}
}
Значение ветки else
вычисляется в том случае, когда ни одно из условий в других ветках не удовлетворено.
Если для нескольких значений выполняется одно и то же действие, то условия можно перечислять в одной ветке через запятую:
when (x) {
0, 1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}
Помимо констант в ветках можно использовать произвольные выражения:
when (x) {
parseInt(s) -> println("s encodes x")
else -> println("s does not encode x")
}
Также можно проверять вхождение аргумента в интервал in
или !in
или его наличие в коллекции:
when (x) {
in 1..10 -> println("x is in the range")
in validNumbers -> println("x is valid")
!in 10..20 -> println("x is outside the range")
else -> println("none of the above")
}
Иногда выражение when
удобно использовать вместо цепочки условий вида if-else if
. При отстутствии аргумента, условия работают как простые логические выражения, а тело ветки выполняется при его истинности. Однако злоупотреблять таким решением не стоит, цепочки if-else if
чаще бывают понятнее для чтения, особенно, когда условия в них достаточно длинные.
when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}
for
Цикл for
обеспечивает перебор всех значений коллекции.
for (item in collection) {
print(item)
}
Если при проходе по массиву или списку требуется порядковый номер элемента, необходимо использовать свойство indices
:
for (i in array.indices) {
print(array[i])
}
Также можно использовать библиотечную функцию withIndex()
для получения индекса и значения по этому индексу одновременно:
for ((index, value) in array.withIndex()) {
println("the element at $index is $value")
}
while
Циклы while
и do..while
абсолютно аналогичны таковым же в Java:
// цикл while
while (x > 0) {
x--
}
// цикл do..while
do {
val y = retrieveData()
} while (y != null) // y здесь доступно!
Классы в Kotlin, как и в Java, объявляются с помощью ключевого слова class
:
class Person {
}
Тело класса является необязательным: если у класса нет тела, фигурные скобки могут быть опущены:
class Customer
Класс в Kotlin может иметь основной конструктор (primary constructor) и один или более дополнительных конструкторов (secondary constructor). Основной конструктор является частью заголовка класса, его объявление идёт сразу после имени класса (и необязательных параметров):
class Customer constructor(name: String)
Ключевое слово constructor
может быть опущено:
class Customer(name: String)
Основной конструктор не содержит в себе исполняемого кода. Код для инициализации класса принято помещать в соответствующий блок (initializers blocks), который помечается словом init
. По сути блок init
содержит код конструктора в привычном его виде. Параметры основного конструктора могут использоваться в инициализирующем блоке.
class Customer(name: String) {
init {
println("Customer initialized with value $name")
}
}
В действительности, для объявления и инициализации свойств основного конструктора в Kotlin есть лаконичное синтаксическое решение:
class Person(val firstName: String, val lastName: String, var age: Int) {
// Далее в теле класса свойства firstName, lastName и age доступны
// и проинициализированы значениями, переданными в конструктор класса Person
}
Для объявления дополнительных (secondary) конструкторов используется ключевое слово constructor
:
class Customer(name: String) {
constructor(name: String, cash: Int) : this(name) {
println("Customer initialized with name $name and cash $cash")
}
}
Здесь класс Customer
имеет два конструктора: основной, принимающий на вход параметр name
; и дополнительный, принимающий на вход параметры name
и cash
. Если класс имеет основной конструктор с параметром, то каждый его дополнительный конструктор должен делегировать вызов основному с передачей параметров. Именно для этого после сигнатуры конструктора после двоеточия указывается ключевое слово this
с передачей параметров. Таким образом сначала будет вызван основной конструктор, а после него дополнительный.
Для создания экземпляра класса конструктор вызывается так, как если бы он был обычной функцией (без ключевого слова new
, как в Java):
val customer = Customer("Joe Smith")
Для явного объявления суперкласса мы помещаем его имя за знаком двоеточия в объявлении класса:
open class Base(p: Int) {
fun baseFunc() {}
}
class Derived(p: Int) : Base(p) {
fun derivedFunc() {
baseFunc()
}
}
В данном примере класс Derived
наследуется от класса Base
и наследует его функцию baseFunc()
.
Ключевое слово open
является противоположностью слову final
в Java: оно позволяет другим классам наследоваться от данного. По умолчанию, все классы в Kotlin имеют статус final
.
Если у класса нет основного (primary) конструктора, тогда каждый дополнительный (secondary) конструктор должен включать в себя инициализацию базового типа с помощью ключевого слова super
. Примечательно, что любые дополнительные конструкторы могут ссылаться на разные конструкторы базового типа:
open class View(ctx: Context) {
constructor(ctx: Context, attrs: AttributeSet) : this(ctx) {
this.attrs = attrs
}
}
class MyView : View {
constructor(ctx: Context) : super(ctx) {
}
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
}
}
Kotlin требует указания аннотации и для методов класса, которые могут быть переопределены, и для самого переопределения:
open class Base {
open fun v() {}
fun nv() {}
}
class Derived() : Base() {
override fun v() {}
override fun nv() {} // Ошибка компиляции, т.к. nv() не помечен как open в классе Base
open fun nov() {} // Пометить можно, но это не будет иметь эффекта, т.к. сам класс Derived не помечен как open
}
Для Derived.v()
необходима аннотация override
для переопределения метода. Если она отсутствует, компилятор выдаст ошибку. Если у функции типа Base.nv()
нет аннотации open
, переопределение этого метода в производном классе невозможно. В final
классе (классе без аннотации open
), запрещено использование аннотации open
для его методов, как в случае с Derived.nov()
.
Член класса, помеченный override
, является сам по себе open
, т.е. он может быть переопределён в производных классах.
Класс и некоторые его члены могут быть объявлены как abstract
. Абстрактный член не имеет реализации в своём классе. Обратите внимание, что нам не надо аннотировать абстрактный класс или функцию словом open
- это подразумевается и так.
abstract class Base(p: Int) {
fun baseFunc() {}
abstract fun abstractFunc();
}
class Derived(p: Int) : Base(p) {
fun derivedFunc() {
baseFunc()
}
override fun abstractFunc() {
println("Base.abstractFunc()")
}
}
Классы в Kotlin могут иметь свойства: изменяемые (mutable) и неизменяемые (read-only) — var
и val
соответственно.
public class Address {
public var name: String = ...
public var street: String = ...
public var city: String = ...
}
Для того, чтобы воспользоваться свойством, мы просто обращаемся к нему по имени:
fun copyAddress(address: Address): Address {
val result = Address()
result.name = address.name // вызов методов доступа
result.street = address.street
result.city = address.city
return result
}
Методы доступа (геттеры и сеттеры) могут быть описаны самостоятельно, как и обычные функции, прямо при объявлении свойств. Например, пользовательский геттер и сеттер:
val stringRepresentation: String
get() = "$name of $street in $city"
set(value) {
field = "$value new string"
}
Классы в Kotlin не могут иметь полей. Т.е. переменные, которые объявляются внутри класса только выглядят и ведут себя как поля из Java, хотя на самом деле являются свойствами, для них неявно реализуются методы get()
и set()
.
Нередко создаются классы лишь с целью хранения данных. Функционал таких классов зависит от самих данных, которые в них хранятся. В Kotlin класс может быть отмечен словом data
и такой класс называют классом данных:
data class User(val name: String, val age: Int)
Компилятор автоматически формирует следующие методы для класса из свойств, объявленных в основном конструкторе:
equals()
/hashCode()
— для возможности сравнения конкретных объектов по значению и ссылке.toString()
в форме "User(name=John, age=42)"
— для приведения объекта к строке,componentN()
, которые соответствуют свойствам, в соответствии с порядком их объявления (name=component1(), age=component2()
),copy()
— для создания копии объекта.Для обеспечения согласованности и осмысленного поведения сгенерированного кода классы данных должны удовлетворять следующим требованиям:
val
или var
.open
).Интерфейсы в Kotlin очень похожи на интерфейсы в Java. Основное отличие — интерфейсы могут содержать методы с реализацией. Эта особенность делает интерфейсы похожими на абстрактные классы, однако интерфейсы в отличие от них позволяют реализовать множественное наследование.
Интерфейс определяется ключевым словом interface
:
interface MyInterface1 {
fun interface1()
fun method1() {
// тело
}
}
interface MyInterface2 {
fun interface2()
fun method2() {
// тело
}
}
Класс или объект могут реализовать любое количество интерфейсов:
class Child : MyInterface1, MyInterface2 {
override fun interface1() {
// тело
}
override fun interface2() {
// тело
}
}
В интерфейсах могут быть объявлены свойства. Свойство, объявленное в интерфейсе, может быть либо абстрактным, либо иметь свою реализацию методов доступа (геттеров и сеттеров).
interface MyInterface {
val prop: Int // абстрактное свойство
var _propWithImpl: String // абстрактное свойство
var propWithImpl: String
get() = _propWithImpl
set(value) {
_propWithImpl = value
}
fun interface2()
fun method2() {
// тело
}
}
class Child : MyInterface {
override val prop: Int = 29
override val _propWithImpl: String = "property with implementation"
}
В рамках сеттеров интерфейса невозможно использовать ключевое слово field
для доступа к значению свойства. Компилятор выдаст ошибку. Поэтому, если требуется переопределить сеттер свойства интерфейса, необходимо заводить еще одно дополнительное свойство, которое будет содержать значение свойства, сеттер которого мы переопределяем. Например, в приведенном выше примере выполняется переопределение геттера и сеттера свойства propWithImpl
. Для этого заводится свойство _propWithImpl
, которое используется в переопределяемых сеттере и геттере для получения и сохранения значения.
Система типов в языке Kotlin нацелена на то, чтобы избавить программиста от опасности обращения к null
-значениям.
Одним из самых распространённых подводных камней многих языков программирования, в том числе Java, является попытка произвести доступ к null
-значению. Это приводит к ошибке. В Java такая ошибка называется NullPointerException
.
Kotlin призван исключить ошибки подобного рода. NullPointerException
в Kotlin могу возникать только в следующих случаях:
throw NullPointerException()
.!!
(описано ниже).this
на данные, которые не были ещё проинициализированы).Система типов в Kotlin различает два типа ссылок: которые могут иметь значение null
(nullable ссылки) и те, которые не могут иметь null-знаечния (non-null ссылки). Для того, чтобы разрешить null
значение, необходимо добавить к объявлению типа переменноq знак вопроса ?
, например, String?
.
Пример:
var a: String = "abc"
a = null // ошибка компиляции, a — по умолчанию non-null
var l = a.length // ошибка компиляции, переменная a имеет значение null
var b: String? = "abc"
b = null // нет ошибки, переменная b может иметь значение null
var l = b.length // ошибка: переменная b имеет значение null
Пример выше показывает, что при вызове метода с использованием переменной a
, получение NullPointerException
исключено, т.к. компилятор не позволит собрать такой код, где a
может иметь значение null
.
Получить доступ к значению переменной b
для вызова метода — небезопасно и компилятор предупредит об ошибке. Но если есть необходимость получить доступ к свойству, то есть несколько способов:
null
.?:
).!!
.null
Первый способ. Можно явно проверить переменную на null
значение и обработать два варианта по отдельности:
val l = if (a != null) a.length else -1
Компилятор отслеживает информацию о проведённой проверке и позволяет вызывать length
внутри блока if
.
Вторым способом является оператор безопасного вызова ?.
:
var l = a?.length
Этот код возвращает a.length
в том, случае, если a
не null
. Иначе он возвращает null
, и тогда типом этого выражения (переменной l
) будет Int?
.
Он же elvis operator или оператор ?:
(не путать с тернарным оператором).
Полезен в ситуации, когда необходимо либо вернуть некоторое значение сслылки, если она не null
, либо некое значение "по-умолчанию".
Пример с if-else
:
val l: Int = if (a != null) b.length else -1
Аналогом такому if
-выражению является элвис-оператор ?:
:
val l = a?.length ?: -1
Если выражение, стоящее слева от элвис-оператора, не является null
, то элвис-оператор его вернёт (a.length
). В противном случае в качестве возвращаемого значения послужит то, что стоит справа (-1
).
!!
Для любителей NPE существует ещё один способ. Можно написать a!!
и это вернёт либо non-null значение a
, либо выкинет NPE:
val l = a!!.length
New -> Kotlin -> JVM -> Next.
Project Name -> Finish.
Add kotlin file -> Add configuration -> Run.