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

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

Шкодим

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

Интерфейсы

fun interface

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

Итак, перед нами известная деревня Простоквашино и его житель Дядя Фёдор со своими друзьями. Каждое утро Дядя Фёдор здоровается с котом Матроскиным и кот отвечает ему. Хотя кот вполне владеет русским языком, оставим ему родное кошачье приветствие.

Создадим два класса. Пусть Дядя Фёдор будет фермером, ну а кот он и в Африке кот.


class Cat(val name: String){
    fun speak() = println("Мяу!")
}

class Farmer(val name: String){
    fun greet(cat: Cat){
        println("Доброе утро, ${cat.name}!")
        cat.speak()
    }
}

Можно здороваться.


val fedor = Farmer("Дядя Фёдор")
val matroskin = Cat("Матроскин")

fedor.greet(matroskin)

Доброе утро, Матроскин Мяу!

Также Дядя Фёдор здоровается с псом Шариком. Кот с собакой не здоровается, а говорит каждое утро: "Шарик, ты балбес". Впрочем, эту фразу мы кодировать не будем.


class Dog(val name: String, val times: Int){
    fun speak(){
        repeat(times) {
            println("Гав")
        }
    }
}

При попытке поздороваться с Шариком мы получим ошибку.


val sharik = Dog("Шарик", 2)
fedor.greet(sharik) // ошибка!

Мы жёстко закодировали приветствие, здороваясь только с котом, что вполне естественно. Незачем здороваться с собакой сутулой. Ладно, ладно. Будем вежливыми и поздороваемся с балбесом.


class Farmer(val name: String) {
    fun greet(cat: Cat) {
        println("Доброе утро, ${cat.name}!")
        cat.speak()
    }

    // новая перегруженная версия
    fun greet(dog: Dog){
        println("Доброе утро, ${dog.name}")
        dog.speak()
    }
}

Теперь ошибки не будет и можно здороваться с Шариком.

На этом наша история не заканчивается. Матроскин завёл себе корову Мурку. И с ней Дядя Фёдор тоже должен здороваться. Пишем код по накатанной дорожке.


class Cow(val name: String){
    fun speak() = println("Му-у-у!")
}

Чтобы здороваться с коровой, нужно снова добавлять в класс Farmer новую функцию.


fun greet(cow: Cow){
    println("Доброе утро, ${cow.name}!")
    cow.speak()
}

Здороваемся.


val murka = Cow("Мурка")
fedor.greet(murka)

На этом этапе нам уже надоедает писать однотипный код. А ведь ещё есть галчонок Хватайка, телёнок Гаврюша и прочие персонажи. Создаваемые функции очень похожи, отличия только в именах и типах параметров. Нам нужно каким-то образом создать универсальную функцию, которая может работать с любым персонажем.

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

Но вместо ожидаемого класса Animal мы создадим интерфейс с таким же именем.


interface Animal{
    val name: String
    fun speak()
}

Обратите внимание, что в теле функции нет никакого кода, мы просто дали функции имя.

На основе интерфейса нельзя создать экземпляр, как это происходит с классом.


// Так не работает
val gavr = Animal("Гаврюша")
gavr.speak()

Подключим интерфейс - переделаем класс Cow.


class Cow(override val name: String) : Animal {
    override fun speak() = println("Му-у-у!")
}

Добавив в описании класса реализацию интерфейса, мы сообщаем классу, что ему необходимо использовать свойство name и функцию speak().

В случае с Cow это было не так очевидно, так как в нашем классе изначально были эти свойства и функции. Но попробуем создать новый класс для галчонка.


class Bird(): Animal{

}

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


class Bird(override val name: String): Animal{
    override fun speak() {
        TODO("Not yet implemented")
    }
}

Мы замечаем, что при реализации интерфейса мы обязаны добавлять ключевое слово override перед свойствами и функциями, которые определены в самом интерфейсе.

Переделаем классы Cat и Dog аналогичным образом.


class Cat(override val name: String) : Animal {
    override fun speak() = println("Мяу!")
}

class Dog(override val name: String, val times: Int) : Animal {
    override fun speak() {
        repeat(times) {
            println("Гав")
        }
    }
}

Теперь мы можем удалить все перегруженные версии функции greet() у класса Farmer и создать взамен универсальную функцию.


class Farmer(val name: String) {
//    fun greet(cat: Cat) {
//        println("Доброе утро, ${cat.name}!")
//        cat.speak()
//    }
//
//    fun greet(dog: Dog) {
//        println("Доброе утро, ${dog.name}")
//        dog.speak()
//    }
//
//    fun greet(cow: Cow) {
//        println("Доброе утро, ${cow.name}!")
//        cow.speak()
//    }
    fun greet(animal: Animal) {
        println("Доброе утро, ${animal.name}!")
        animal.speak()
    }
}

Код класса сократился в несколько раз. И мы можем легко добавить новых друзей без добавления новых функций в класс.


class Bird(override val name: String) : Animal {
    override fun speak() = println("Кто там?")
}

...
val hvat = Bird("Хватайка")
fedor.greet(hvat)

Наличие интерфейса Animal не только сократил код в классе Farmer, но и позволит нам сократить код при приветствии.


val fedor = Farmer("Дядя Фёдор")
val matroskin = Cat("Матроскин")
val sharik = Dog("Шарик", 2)
val murka = Cow("Мурка")

fedor.greet(matroskin)
fedor.greet(sharik)
fedor.greet(murka)

val animals : List<Animal> = listOf(
    Cat("Матроскин"),
    Dog("Шарик", 2),
    Cow("Мурка")
)

animals.forEach{fedor.greet(it)}

В этом примере мы тоже явно указали, что создаём список из Animal, но при заполнении списка указываем уже Cat, Dog, Cow, потому что эти классы реализуют интерфейс Animal.

Рассмотрим одну особенность. Обычно мы указываем у переменной её тип, который часто опускается. Для примера объявим тип явно. И добавим новую переменную с типом Animal.


val murka: Cow = Cow("Мурка") // явно указали, но обычно тип опускают
var gavr: Animal = Cow("Гаврюша")

Как видите, оба примера работают. Но второй вариант иногда помогает использовать возможности интерфейса.

В то же время такой подход может и ограничить некоторые возможности. Переделаем класс Cow, добавив новое свойство, которое будет отвечать за число родившихся телят (как минимум Гаврюшу она родила точно).


class Cow(
    override val name: String,
    var numberOfChildren: Int = 0
) : Animal {
    override fun speak() = println("Му-у-у!")
}

Мы можем создать экземпляр класса и затем указать число детёнышей.


val murka = Cow("Мурка")
murka.numberOfChildren = 1

А такой код уже не сработает.


val murka: Animal = Cow("Мурка")
murka.numberOfChildren = 2

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

Но это ограничение не должно вас расстраивать. Вы можете получить доступ к нужному свойству через ключевое слово is.


val murka: Animal = Cow("Мурка")
if(murka is Cow){
    murka.numberOfChildren = 1
    println("Сколько телят родила Мурка? Ответ: ${murka.numberOfChildren}")
}

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

Подобный способ называется smart cast. Этот способ будет работать только внутри фигурных скобок оператора if, а за его пределами переменная murka снова будет вести себя как животное (в Бобруйск!).

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

Изменим функцию greet().


class Farmer(val name: String) {
    fun greet(animal: Animal) {
        println("Доброе утро, ${animal.name}!")

        val cow: Cow = animal as Cow
        println("У коровы ${cow.numberOfChildren} телят")
        animal.speak()
    }
}

Здороваемся с Муркой.


val fedor = Farmer("Дядя Фёдор")
val murka = Cow("Мурка")
murka.numberOfChildren = 5
fedor.greet(murka)

Доброе утро, Мурка! У коровы 5 телят Му-у-у!

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


val matroskin = Cat("Матроскин")
fedor.greet(matroskin) // ошибка при выполнении программы!

Кот не корова, поэтому код не запускается. Но можно избежать проблемы, если использовать as с восклицательным знаком. Если объект нужного типа, то код выполнится, в противном случае выражение будет пропущено.

На этом наше занимательное знакомство с интерфейсами закончено. Теперь переходим к более научному объяснению.

Интерфейсы в Kotlin ближе к Java 8 и имеют свои особенности. Например, интерфейсы могут содержать объявления свойств.

Класс интерфейса может содержать абстрактные и конкретные функции. Если функция не содержит тела, то она является абстрактной и помечать её как abstract нет необходимости. Интерфейсы могут предоставлять те же преимущества, что и абстрактные классы, но обладают большей гибкостью. Если функция конкретная, то как обычно добавляем тело после фигурных скобок.

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

Объявим интерфейс с одним абстрактным методом.


interface Clickable
{
    fun click()
}

Реализация интерфейса в классе.


class Button : Clickable {
    override fun click() = println("I was clicked")
}

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

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


// Наследуется от класса Animal и использует интерфейсы Clickable и Focusable
class Cat : Animal(), Clickable, Focusable {
    ...
}

Ключевое слово override используется вместо аннотации @Override и является обязательным, что снижает количество потенциальных ошибок при неправильном использовании.

У интерфейса можно задать значения по умолчанию. Если в Java 8 для этих целей используется ключевое слово default, то в Kotlin просто указываете тело метода. Добавим в интерфейс ещё один метод, использующий значение по умолчанию.


interface Clickable
{ 
    fun click()
    fun meow() = println("Я мяукаю!")
}

Если вас устраивает значение по умолчанию, то реализовывать его в классе не нужно. Либо вы можете изменить поведение метода вместе с click().

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

Добавим ещё один интерфейс.


interface Focusable {
    fun setFocus(b: Boolean) =
            println("I ${if (b) "got" else "lost"} focus.")
    fun meow() = println("В фокусе я рычу. Р-р-р!")
}

class Button : Clickable, Focusable {
    override fun click() = println("I was clicked")
    override fun meow() {
        super<Clickable>.meow()
        super<Focusable>.meow()
    }
}

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


override fun meow() = super<Clickable>.meow()

Вызываем методы.


val button = Button()
button.meow()
button.setFocus(true)
button.click()

Получим следующее.


Я мяукаю!
В фокусе я рычу. Р-р-р!
I got focus.
I was clicked

Мы вызвали метод meow() один раз, но отработало два раза, так как такой метод встречается в двух интерфейсах.

Можно добавлять в интерфейс абстрактные свойства без указания abstract. Свойствам нельзя присвоить значение и инициализировать.


interface Roamable {
    val velocity: Int
}

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


interface Roamable {
    val velocity: Int
        get() = 10
}

fun interface

Перед ключевым словом interface можно добавить ещё одно ключевое слово fun. Получится необычная конструкция - то ли функция, то ли интерфейс. Давайте разберёмся.

Иногда приходится создавать анонимный класс из интерфейсов. Код становится немного запутанным для чтения. Для примера создадим интерфейс Music.


interface Music {
    fun play()
}

Теперь пробудем использовать его.


al balalayka = object : Music{
    override fun play() {
        println("FA")
    }
}

balalayka.play()

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


fun interface Music {
    fun play()
}

val balalayka = Music { println("FA") }
balalayka.play()

Код стал короче и проще.

Реклама