Освой Kotlin играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Классы

Kotlin пытается сократить количество кода при создании новых классов. В Kotlin решили реализовать другой подход к классам. Например, объявления по умолчанию получают модификаторы final и public. Вложенные классы по умолчанию не являются внутренними и другие особенности.

В Kotlin нет жёсткой привязки классов и файлов. Вы можете создать несколько классов в одном файле и выбрать любое имя для этого файла. При этом также не важно размещение файлов с классами на диске, можно даже не обращать внимание на имена пакетов и соблюдать точную иерархию вложенности. Но лучше придерживаться правил Java, особенно, если у вас уже большой опыт и вы привыкли к стандартному расположению.

Для класса используется ключевое слово class. С ним могут использоваться также ключевые слова: data, open, internal, abstract, public.

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

Если ваше приложение предназначено для хранения информации о котах, вы можете определить класс Cat для создания ваших собственных объектов Cat. В таких объектах скорее всего вам понадобится хранить имя кота (name), его вес (weight) и порода (breed). Вы можете придумать свои варианты для ваших конкретных задач.

В большинстве случаев класс содержит свойства и функции. Хотя Kotlin позволяет создать пустой бессмысленный класс.

Создадим простейший класс для понимания.


class Cat

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

Вызываем класс в коде.


val cat = Cat()

Никаких new. Одна строчка кода для создания класса, одна строчка для вызова.

В Java для классов используются поля, Kotlin оперирует свойствами. Свойство - это то, что объект знает о себе. Уважающий себя кот знает, как его зовут, сколько он весит и какой он породы. Класс Car (машина) будет иметь свой набор свойств - марка, цвет, скорость и т.п.

То, что объект может сделать, — это его функции (в Java методы). Они определяют поведение объекта и могут использовать свойства объекта. Класс Cat может содержать функцию sleep().

Создадим полноценный класс.

.

class Cat(val name: String, var weight: Int, val breed: String) {
    
}

Мы сейчас не только создали класс, но и задали ему три свойства, используя ключевые слова val и var (это важно).

Функции пишутся внутри фигурных скобок. Создадим функцию sleep(). Если это ещё маленький котёнок, то во сне он сопит, а солидный кот уже храпит.


class Cat(val name: String, var weight: Int, val breed: String) {
    fun sleep() {
        println(if (weight < 3) "сопит!" else "храпит!")
    }
}

Создаём объект Барсик, используя все свойства класса.


var barsik = Cat("Барсик", 4, "Сибирская")

Доступ к любому свойству достигается через оператор . (точка):


println(barsik.name)
println(barsik.weight)
println(barsik.breed)

Если свойство задано ключевым словом var, то мы можем не только прочитать свойство, но и установить новое значение. В нашем случае таким свойством является weight. Давайте накормим кота, чтобы увеличить его вес.


barsik.weight = 6
// Проверяем
println(barsik.weight) // было 4, стало 6

Если вы захотите изменить свойства с ключевым словом val, то у вас ничего не получится. Компилятор сообщит об ошибке (error: val cannot be reassigned). Попробуйте это сделать самостоятельно, чтобы увидеть своими глазами.

Функция вызывается также через оператор "точка", но не забывайте про круглые скобки.


barsik.sleep()

Мы можем создать массив объектов из нашего класса.


val cats = arrayOf(Cat("Барсик", 4, "Сибирская"), Cat("Мурзик", 5, "Бенгальская"))
cats[1].weight = 6 // накормили второго кота
println(cats[1].name) // а напомните, как его зовут?
println(cats[1].weight) // и сколько он весит сейчас?

cats[0].sleep() // как спит первый кот?

Создание объекта очень похоже на вызов функции - используются круглые скобки, только вместо названия функции пишем имя класса. На самом деле - это конструктор. Подробнее о конструкторах поговорим позже. Пока нужно запомнить - конструктор содержит код, необходимый для инициализации объекта. Часто используют конструкторы для определения свойств объекта и присваивания им значений. Объект иногда называют экземпляром класса, а его свойства — переменными экземпляров.

Создание класса с готовыми свойствами в виде параметров очень удобно и часто используемый приём. Kotlin помогает писать код в удобном компактном виде. Но иногда нужно со свойством что-то сделать - проверить на условие, присвоить значение по умолчанию и т.д. В этом случае вы можете использовать стиль, который принят в Java.


class WildCat(name: String, weight: Int, breed: String){
	// это свойства
    val name = name
    var weight = weight
    val breed = breed
}

val manul = WildCat("Manulik", 4, "Uknown")
println(manul.name)

В конструкторе мы используем ключевые слова val и var - в данном случае name, weight и breed уже не являются свойствами класса, а являются обычными параметрами конструктора. А свойства задаются уже внутри фигурных скобок. Не очень красиво, что имена свойств совпадают с именами параметров, код становится нечитаемым. Некоторые программисты предпочитают избегать такой схожести и используют следующий подход.


class WildCat(name_param: String, weight_param: Int, breed_param: String){
    val name = name_param
    var weight = weight_param
    val breed = breed_param
}

val manul = WildCat("Manulik", 4, "Uknown")
println(manul.breed)

Так легче различать, где свойство и где параметр.

Другой популярный способ - знак подчёркивания.


class WildCat(_name: String, _weight: Int, _breed: String){
    val name = _name
    var weight = _weight
    val breed = _breed
}

Если вы привыкли использовать одно имя, то добавьте this.


class WildCat(name: String, weight: Int, breed: String){
    val this.name = name
    var this.weight = weight
    val this.breed = breed
}

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

Предположим, вы хотите определить для нового свойства значение по умолчанию, не включая его в конструктор. Добавим в класс свойство activities и инициализируем его массивом, который по умолчанию содержит значение «Play».


class Cat(val name: String, var weight: Int, val breed: String) {
	
	var activities = arrayOf("Play")
	
    fun sleep() {
        println(if (weight < 3) "сопит!" else "храпит!")
    }
}

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


class Cat(val name: String, var weight: Int, breed_param: String) {
    var activities = arrayOf("Play")
    val breed = breed_param.toUpperCase()
    ...
}

Обратите внимание, что мы убрали из конструктора свойство breed (нет ключевого слова val) и заменили параметром breed_param. В теле класса создали свойство breed, которое принимает параметр конструктора и преобразовывает его.

Блок инициализации init

Указанный способ инициализации свойств хорошо работает, если вы хотите присвоить простое значение или выражение. А если понадобится сделать что-то более сложное? Или нужно выполнить дополнительный код, который сложно добавить в конструктор?

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


class Cat(val name: String, var weight: Int, breed_param: String) {
    
	// Сначала будут созданы свойства, заданные в конструкторе
    // Затем выполнится первый блок инициализации    	
    init {
        println("Кот $name был создан.")
    }
	
	// Потом будут созданы свойства после завершения первого блока инициализации
	var activities = arrayOf("Play")
    val breed = breed_param.toUpperCase()
	
    // Теперь наступает черёд выполнения второго блока инициализации
	init {
        println("Порода: $breed.")
    }
	
	...
}

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

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

Конструкторы

Мы уже ранее сталкивались с конструктором. Познакомимся с ним поближе.

Можно создать класс без конструктора - после имени класса не используем круглые скобки. Тогда нужно добавить только фигурные скобки для блока.


class Cat{
	fun sleep(){
		println("Кот спит")
	}
}

Когда вы определяете класс без конструктора, компилятор генерирует его за вас. Он добавляет пустой конструктор (конструктор без параметров) в откомпилированный код, который ничего не делает. Ваш код будет равносилен коду:


class Cat(){
	...
}

И создавать объект вам всё-равно придётся с использованием круглых скобок.


val cat = Cat() // круглые скобки обязательны

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


class Milk: Food()

Можно обойтись без фигурных скобок.


class Cat(val name: String, var weight: Int, val breed: String)

var barsik = Cat("Барсик", 4, "Сибирская")

Подобный конструктор считается основным или первичным (primary) в Kotlin. Он объявляется вне тела класса. Дополнительные вторичные конструкторы объявляются уже в теле класса.

Предыдущий пример создания класс можно было переписать чуть длиннее.


class Cat constructor(_name: String, var weight: Int, val breed: String) {
    var name: String

    init {
		name = _name
    }
}

Напомню, что знак подчёркивания используют, чтобы отличить параметр конструктора от свойства. Если вы привыкли использовать одно имя, то добавьте this.


this.name = name

Параметрам конструктора можно назначать значения по умолчанию, как в параметрах функций.


class Cat(val name: String, val isBlack: Boolean = true)

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


val cat = Cat("Barsik") // достаточно указать один параметр

Если класс имеет суперкласс, то основной конструктор должен инициализировать свойства от суперкласса.


open class Cat(val name: String) {...} // суперкласс

class SeaCat(name: String) : Cat(name) {...}

Вторичные конструкторы

При создании дополнительных конструкторов используется ключевое слово constructor (впрочем для первичного конструктора тоже можно). Вторичный конструктор делегирует первичный конструктор с помощью ключевого слова this. Добавим в конструктор новое свойство email:


class Cat(var name: String, var age: Int, var address: String?) {
     
    var email: String = ""
 
    constructor(name: String, age: Int, address: String?, email: String) : this(name, age, address) {
        this.email = email
    }
}

var vaska = Cat("Васька", 6, "Омск", "vaska@kitty.com")

Если требуется несколько конструкторов, то можно объявить столько вторичных конструкторов, сколько вам требуется.

Геттеры/сеттеры

У свойств есть проблема - их нельзя контролировать. Никто не мешает присвоить свойству weight отрицательное значение. Котик в опасности!

Для решения проблемы существуют get- и set-методы (иногда их называют геттеры и сеттеры). Get- и set-методы предназначены для чтения и записи значений свойств. Цель get-метода — вернуть значение, запрошенное для данного свойства. А set-методы получают значение аргумента и используют его для записи значения в свойство. Таким образом get- и set-методы позволяют защитить значения свойств и управлять тем, какие значения читаются или записываются в свойства.

В очередной раз переделаем класс. Добавим в класс новое свойство weightInGramms и напишем для него пользовательский get-метод, который будет возвращать соответствующее значение.


class Cat(val name: String, var weight: Int, breed_param: String) {

    var activities = arrayOf("Play")
    val breed = breed_param.toUpperCase()

    val weightInGramms: Int
        get() = weight * 1000

    fun sleep() {
        println(if (weight < 3) "сопит!" else "храпит!")
    }
}

Сразу после объявления свойства мы создаём get-метод, который представляет собой функцию без параметров. Тип возвращаемого значения должен совпадать с типом свойства, значение которого должен возвращать get-метод, в противном случае код не будет компилироваться.

Проверим, как работает свойство.


val cat = Cat("Васька", 3, "Дворовая")
println(cat.weightInGramms) // возвращает 3000

Дополним свойство weight пользовательским set-методом, чтобы свойство могло обновляться только значениями больше 0. Для этого необходимо переместить определение свойства weight из конструктора в тело класса, а затем добавить set-метод к свойству.


class Cat(val name: String, weight_param: Int, breed_param: String) {

    var activities = arrayOf("Play")
    val breed = breed_param.toUpperCase()
    
    var weight = weight_param
        set(value) {
            if (value > 0) field = value
        }

    val weightInGramms: Int
        get() = weight * 1000

    fun sleep() {
        println(if (weight < 3) "сопит!" else "храпит!")
    }
}

Set-метод представляет собой функцию с именем set, которая записывается под объявлением свойства. Set-метод имеет один параметр (обычно с именем value), который содержит новое значение свойства. В нашем случае значение свойства weight обновляется только в том случае, если значение параметра value больше 0. Если попытаться обновить свойство weight значением, меньшим либо равным 0, set-метод проигнорирует обновление свойства. Внимание! Для обновления свойства weight set-метод использует идентификатор field, обозначающий поле данных для свойства. Очень важно использовать именно field вместо имени свойства, потому что так вы предотвращаете зацикливание. Просто поверьте на слово.

Мы создавали get- и set- методы вручную, чтобы контролировать данные для свойства. Но вдобавок компилятор незаметно генерирует get- и set-методы для всех свойств, у которых пользовательских методов нет. Если свойство определяется с ключевым словом val, то компилятор добавляет get-метод, а если свойство определяется с ключевым словом var, то компилятор добавляет и get-, и set-метод.

Рассмотрим примеры. По ключевому слову val можно догадаться, что у name есть только геттер, так как этот тип переменной отвечает за неизменяемые данные. У isMale есть и геттер и сеттер.


class Cat() {
    val name: String = "Кот"
    var isMale: Boolean = false
}

Вызываем экземпляр класса, присваиваем коту имя и пол. Результат выводим на экран.


val cat = Cat()
cat.isMale = true // можем менять
println(cat.name) // можем читать
println(cat.isMale) // проверяем изменённое значение

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


private set(value) {
    field = "Cat's Name: ${value.trim()}"
}

Теперь изменять свойство может только сам экземпляр класса. А предыдущий пример не станет работать. Это полезно, если требуется запретить изменение свойства другими частями вашего приложения.

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


class Rectangle(val width: Int, val height: Int){
	val isSquare: Boolean
	    get(){
			return width == height
		}
		
		// get() = width == height // запись в одну строку
}

Вызываем объект класса и проверяем.


val rectangle = Rectangle(25, 25)
println(rectangle.isSquare)

Мы получили вычисляемое свойство, у которого значение меняется. У него нет начального значения, значения по умолчанию и нет поля, которое могло бы хранить значение.

Импорт Java-классов

Можно импортировать стандартный Java-класс и вызывать его методы.


import java.util.Random

fun main(args: Array<String>) {
    
    val random = Random()
    println(random.nextInt(3))
}

Получить имя класса

Вместо стандартного метода Java getClass() можно вызвать javaClass.


val list = listOf(1, 3, 9)
println(list.javaClass)
// class java.util.Arrays$ArrayList

Иногда в логах используют имя текущего класса. Проверим на стандартной активности


Log.d(javaClass.simpleName, "Meow") // вернет D/MainActivity: Meow

В Kotlin есть новое ключевое слово internal, применимое к пакетам. Позволяет указать, что классы доступны внутри модуля.

Переопределяем метод toString()

Можно переопределять метод toString():


class Client(val name: String, val postalCode: Int) {
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}

Вызываем.


val client = Client("Alice", 342562)
println(client)
//Client(name=Alice, postalCode=342562)

Расширение классов

Можно расширять классы не изменяя код самого класса при помощи функций-расширений. Добавим функцию isBlack() к классу Cat вне самого класса:


fun Cat.isBlack(): Boolean {
    //если цвет кота чёрный, то не повезёт, если он перейдёт вам дорогу
    //вернет true
    return true // придумайте свой код
}

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

Абстрактные классы

Как и в Java, класс можно объявить абстрактным, добавив ключевое слово abstract. Создать экземпляр такого класса нельзя. Абстрактные методы всегда открыты, поэтому использование модификатора open необязательно.

Абстрактный класс может содержать абстрактные свойства и функции. Абстрактный класс также может содержать и неабстрактные свойства и функции. А ещё абстрактный класс может не содержать ни одного абстрактного свойства или функции. Если класс содержит какие-либо свойства и функции, помеченные как абстрактные, весь класс должен быть абстрактным.


abstract class Animal {
    abstract val image: String
    abstract val food: String
    abstract val habitat: String
    var hunger = 10
}

Свойства в абстрактных свойствах нельзя инициализировать.

Абстрактные функции не могут иметь блок с фигурными скобками.


abstract class Animal {
    ...

    abstract fun makeNoise()

    abstract fun eat() 
	
	// обычные функции в абстрактных классах закрыты, но мы можем их открыть
	open fun roam() {
        println("Животное скитается")
    }
}

Расширяемся от абстрактного класса при помощи двоеточия, как и с с суперклассом.


class Bird : Animal(){
    override fun eat() {
        
    }
}

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

В абстрактных подклассах у вас есть выбор: либо реализовать абстрактные свойства и функции, либо передать эстафету их подклассам.

Модификаторы видимости

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

В Kotlin добавлен новый модификатор internal, обеспечивающий видимость в границах модуля. Модуль - это набор файлов, компилируемых вместе (модуль в IDEA, проект Eclipse, Maven, Gradle и т.д.). Члены класса и объявления верхнего уровня доступны в пределах модуля.

Члены класса с модификатором protected доступны в подклассах.

Члены класса с модификатором private доступны в самом классе, а объявления верхнего уровня с этим модификатором доступны в файле.

Внутренние (inner) и вложенные классы

Можно объявлять один класс внутри другого. Но в Kotlin вложенные классы не имеют доступа к экземпляру внешнего класса, если не запросить его явно.

В Kotlin вложенный класс без модификаторов является аналогом статического вложенного класса в Java (фактически независимый класс).

Чтобы создать внутренний класс со ссылкой на внешний класс, нужно добавить модификатор inner.

Чтобы получить доступ к внешнему классу из внутреннего, нужно использовать конструкцию this@Outer, где Outer - название внешнего класса.

Запечатанные классы (sealed)

Добавление модификатора sealed к суперклассу ограничивает возможность создания подклассов. Все прямые подклассы должны быть вложены в суперкласс. Запечатанный класс не может иметь наследников, объявленных вне класса.


sealed class SealedClass {
    class One(val value: Int) : SealedClass()
    class Two(val x: Int, val y: Int) : SealedClass()
    
    fun eval(e: SealedClass): Int =
            when (e) {
                is SealedClass.One -> e.value
                is SealedClass.Two -> e.x + e.y
            }
}

В методе eval() при использовании when не пришлось использовать ветку else, так как sealed позволяет указать все доступные варианты и значение по умолчанию не требуется.

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

По умолчанию запечатанный класс открыт и модификатор open не требуется.

Any

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


class Cat {
	...
}

Компилятор незаметно для вас преобразует ваш в класс в подкласс Any.


class Cat : Any {
	...
}

Подобный подход гарантирует, что каждый класс наследует общее поведение. В частности, любой класс будет содержать функцию equals().

Также вы можете создать функцию с параметрами Any или возвращаемый тип Any, который будет работать с объектами любых типов. Например, можно создать массив для хранения объектов любого типа:


val myArray = arrayOf(Cat(), Ball(), Table())

Класс содержит несколько функций, наследуемых каждым классом.

  • equals(any: Any): Boolean - проверяет, считаются ли два объекта "равными" (одним фактическим объектом). По умолчанию функция возвращает true, если используется для проверки одного объекта, или false — для разных объектов. Функция equals() вызывается каждый раз, когда используется оператор ==
  • hashCode(): Int - возвращает хеш-код для объекта. Хеш-коды часто используются некоторыми структурами данных для эффективного хранения и выборки значений
  • toString(): String - возвращает описание класса. По умолчанию сообщение содержит имя класса и "непонятное" число по определённому правилу

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

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

Дополнительное чтение

Data Classes

Реклама