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

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

Шкодим

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

Классы

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

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

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

Принцип создания классов одинаков - объявляются поля класса, затем геттеры и сеттеры. Очень утомительно. Есть способ получше.

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


class Cat

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


val cat = Cat()

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

В Java для классов используются поля, Kotlin оперирует свойствами. Рассмотрим примеры.

Сначала на Java.


public class Cat {
    private final String name;
    
    public Cat(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}

В Kotlin этот код выглядит следующим образом.


class Cat(val name: String)

Опять всего одна строчка кода!

Есть расширенный вариант.


class Cat(name:String) {
  val name:String
  init{
    this.name = name
  }
}

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

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

Если нам нужны стандартные поля с геттером и сеттером, то используем var.

Код на Java.


public class Cat {
    private final String name;
    private boolean isMale;

    public Cat(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public boolean isMale() {
        return isMale;
    }

    public void setMale(boolean male) {
        isMale = male;
    }
}

На Kotlin.


class Cat(name:String) {
  val name:String
  var isMale:Boolean = false
  init{
    this.name = name
  }
}

В классе нет методов getXXX/setXXX(), однако Kotlin как-то догадывается о их существовании. Магия!

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


val cat = Cat("Мурзик")
cat.isMale = true;

println("${cat.name} ${if(cat.isMale) "Кот" else "Кошка"}")
    
// Выводится: Мурзик Кот

Потренируемся на кошках для страховки.


val cat = Cat("Мурка")
cat.isMale = false;

println("${cat.name} ${if(cat.isMale) "Кот" else "Кошка"}")
    
// Выводится: Мурка Кошка

Как видите, мы обращается к сеттеру и геттеру как свойству класса через точку. Обратите внимание, что ключевое слово new не используется.

Как бы это выглядело на Java.


Cat cat = new Cat("Мурзик");
cat.setMale(true);
String who = "";
if(cat.isMale())
    who = "Кот";
else 
    who = "Кошка";
System.out.println(cat.getName() + who);

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

Синтаксис конструктора очень компактный. Мы явно не объявляем его, но он создаётся! Смотрим:


class Cat(var name: String, var age: Int, var address: String?) {

}

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


class Cat(var name: String, var age: Int, var address: String?)

var barsik = Cat("Барсик", 4, "Москва")

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

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

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


class Cat constructor(_name: String, var age: Int, var address: String?) {
    var name: String

    init {
		name = _name
    }
}

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


this.name = name

Блоков init может быть несколько.


class Cat(val name: String, var weight: Int, breed_param: String){
	init{
		println("Cat $name has been created")
	}
	
	var habits = arrayOf("Sleep")
	val breed = breed_param.toUpperCase()
	
	init{
		println("The breed is $breed")
	}
	
}

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


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

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

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


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

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

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


open class Food // будет создан конструктор по умолчанию без параметров

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


class Milk: Food()

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


class Cat {
    fun meow(){
	    println("Meow!")
    }
}

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

При создании дополнительных конструкторов используется ключевое слово 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")

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

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

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


import java.util.Random

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

Геттеры и сеттеры

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

Начнём с геттера. Добавляем метод get() для нужного свойства (в примере это name), который будет возвращать строку, в которой все символы имени преобразован в верхний регистр.


class Cat {
    val name: String = "barsik"
        get() = field.toUpperCase()
}

Ключевое слово field ссылается на поле со значением, которое Kotlin автоматически создаёт для свойства. Когда возвращается версия имени в верхнем регистре, содержимое самого поля не меняется.

Сеттер, напротив, изменяет поле свойства. Метод записи set() указывает способ изменения. Обратите внимание, что у свойства придётся изменить val на var, так как студия будет ругаться - ведь мы пытаемся изменить значение у неизменяемой переменной.


class Cat {
    var name: String = "barsik"
        get() = field.toUpperCase()

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

// Проверяем
val cat = Cat()
cat.name = "  Murzik  "
println(cat.name)

Писать геттер и сеттер нужно сразу после нужного свойства, в данном случае name.

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


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 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 // придумайте свой код
}

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

Модификатор open

По умолчанию класс всегда наследуется от Any (аналог Object в Java) и является закрытым (final) (в Java по умолчанию открыты). В этом случае нельзя наследоваться от него. Но мы можем наследоваться от другого конкретного класса, который явно объявлен как open или abstract.

Также добавьте модификатор open ко всем свойствам и методам, которые можно переопределять.


// класс, который можно наследовать
open class Pet : Runnable {
	fun disable() {} // закрыто, переопределять нельзя
	
	open fun jump() {} // открыто, переопределять можно
	
	override fun run() {} // переопределение открытой функции тоже является открытым
	
	final override fun walk() {} // запрещаем переопределять метод, который по умолчанию открыт
}

Вместо ключевого слова extends в Kotlin используется : (двоеточие).

Примеры классов.


open class Animal(name: String) // можем наследоваться от этого класса

class Cat(firstName: String, lastName: String) : Animal(firstName)

Если используется конструктор с параметром, то мы указываем его у родительского конструктора. Это похоже на вызов super() в Java.

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

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


abstract class Animal {
	abstract fun fly() // реализовать в подклассе
	
	open fun sleep() {} // обычные функции в абстрактных классах закрыты, но мы можем их открыть
}

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


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

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

По умолчанию класс имеет модификатор 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 не требуется.

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

Data Classes

Реклама