Статьи

Kotlin From Scratch: абстрактные классы, интерфейсы, наследование и псевдонимы типов

Kotlin — это современный язык программирования, который компилируется в байт-код Java. Он бесплатный и с открытым исходным кодом и обещает сделать кодирование для Android еще более увлекательным.

В предыдущей статье вы узнали больше о свойствах Kotlin, таких как поздняя инициализация, расширение и встроенные свойства. Мало того, вы также узнали о продвинутых классах, таких как data, enum, nested и sealed, в Kotlin.

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

Kotlin поддерживает абстрактные классы — так же, как Java, это классы, из которых вы никогда не собираетесь создавать объекты. Абстрактный класс является неполным или бесполезным без каких-либо конкретных (не абстрактных) подклассов, из которых вы можете создавать экземпляры объектов. Конкретный подкласс абстрактного класса реализует все методы и свойства, определенные в абстрактном классе, иначе этот подкласс также является абстрактным классом!

Мы создаем абстрактный класс с модификатором abstract (аналогично Java).

1
2
3
abstract class Employee (val firstName: String, val lastName: String) {
    abstract fun earnings(): Double
}

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

1
2
3
4
5
6
7
abstract class Employee (val firstName: String, val lastName: String) {
    // …
     
    fun fullName(): String {
        return lastName + » » + firstName;
    }
}

Здесь мы создали неабстрактную функцию fullName() в абстрактном классе Employee . Конкретные классы (подклассы абстрактного класса) могут переопределять реализацию по умолчанию абстрактного метода — но только если в методе указан модификатор open (вскоре вы узнаете об этом подробнее).

Мы также можем хранить состояние в абстрактных классах.

1
2
3
4
abstract class Employee (val firstName: String, val lastName: String) {
    // …
    val propFoo: String = «bla bla»
}

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

1
2
3
4
5
6
class Programmer(firstName: String, lastName: String) : Employee(firstName, lastName) {
 
    override fun earnings(): Double {
        // calculate earnings
    }
}

Наш класс Programmer расширяет абстрактный класс Employee . В Kotlin мы используем один символ двоеточия ( : вместо ключевого слова extends Java для расширения класса или реализации интерфейса.

Затем мы можем создать объект типа Programmer и вызвать для него методы — либо в своем собственном классе, либо в суперклассе (базовом классе).

1
2
val programmer = Programmer(«Chike», «Mgbemena»)
println(programmer.fullName()) // «Mgbemena Chike»

Вас может удивить то, что у нас есть возможность переопределить свойство val (неизменяемое) с помощью var (mutable).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
open class BaseA (open val baseProp: String) {
 
}
 
class DerivedA : BaseA(«») {
 
    private var derivedProp: String = «»
 
    override var baseProp: String
        get() = derivedProp
        set(value) {
            derivedProp = value
        }
}

Убедитесь, что вы используете эту функцию с умом! Имейте в виду, что мы не можем сделать обратное — переопределить свойство var с помощью val .

Интерфейс — это просто набор связанных методов, которые обычно позволяют указывать объектам, что делать, а также как это делать по умолчанию. (Методы по умолчанию в интерфейсах — это новая функция, добавленная в Java 8.) Другими словами, интерфейс — это контракт, которому должны следовать реализующие классы.

Интерфейс определяется с помощью ключевого слова interface в Kotlin (аналогично Java).

1
2
3
4
5
6
7
class Result
class Student
 
interface StudentRepository {
    fun getById(id: Long): Student
    fun getResultsById(id: Long): List<Result>
}

В приведенном выше коде мы объявили интерфейс StudentRepository . Этот интерфейс содержит два абстрактных метода: getById() и getResultsById() . Обратите внимание, что включение abstract ключевого слова является избыточным в методе интерфейса, поскольку оно уже неявно абстрактно.

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

1
2
3
4
5
6
7
8
9
class StudentLocalDataSource : StudentRepository {
    override fun getResults(id: Long): List<Result> {
       // do implementation
    }
 
    override fun getById(id: Long): Student {
        // do implementation
    }
}

Здесь мы создали класс StudentLocalDataSource который реализует интерфейс StudentRepository .

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

Обратите внимание на следующие дополнительные правила интерфейсов в Kotlin:

  • Класс может реализовывать столько интерфейсов, сколько вы хотите, но он может расширять только один класс (аналогично Java).
  • Модификатор override является обязательным в Kotlin — в отличие от Java.
  • Наряду с методами мы также можем объявить свойства в интерфейсе Kotlin.
  • Метод интерфейса Kotlin может иметь реализацию по умолчанию (аналогично Java 8).

Давайте посмотрим на пример метода интерфейса с реализацией по умолчанию.

1
2
3
4
5
6
interface StudentRepository {
    // …
    fun delete(student: Student) {
        // do implementation
    }
}

В предыдущем коде мы добавили новый метод delete() с реализацией по умолчанию (хотя я не добавил фактический код реализации для демонстрационных целей).

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

1
2
3
4
5
6
class StudentLocalDataSource : StudentRepository {
    // …
    override fun delete(student: Student) {
       // do implementation
    }
}

Как уже говорилось, интерфейс Kotlin может иметь свойства, но имейте в виду, что он не может поддерживать состояние. (Однако помните, что абстрактные классы могут поддерживать состояние.) Поэтому будет работать следующее определение интерфейса с объявлением свойства.

1
2
3
4
interface StudentRepository {
    val propFoo: Boolean // will work
    // …
}

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

1
2
3
4
interface StudentRepository {
    val propFoo: Boolean = true // Error: Property initializers are not allowed in interfaces
    // ..
}

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

01
02
03
04
05
06
07
08
09
10
interface StudentRepository {
    var propFoo: Boolean
        get() = true
        set(value) {
           if (value) {
             // do something
           }
        }
    // …
}

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

01
02
03
04
05
06
07
08
09
10
class StudentLocalDataSource : StudentRepository {
    // …
    override var propFoo: Boolean
        get() = false
        set(value) {
            if (value) {
                 
            }
        }
}

Давайте рассмотрим случай, когда у нас есть класс, реализующий несколько интерфейсов с одинаковой сигнатурой метода. Как класс решает, какой метод интерфейса вызвать?

1
2
3
4
5
6
7
interface InterfaceA {
    fun funD() {}
}
 
interface InterfaceB {
    fun funD() {}
}

Здесь у нас есть два интерфейса, которые имеют метод с одинаковой сигнатурой funD() . Давайте создадим класс, который реализует эти два интерфейса и переопределяет метод funD() .

1
2
3
4
5
class classA : InterfaceA, InterfaceB {
    override fun funD() {
        super.funD() // Error: Many supertypes available, please specify the one you mean in angle brackets, eg ‘super<Foo>’
    }
}

Компилятор смущен вызовом super.funD() потому что два интерфейса, которые реализует класс, имеют одинаковую сигнатуру метода.

Чтобы решить эту проблему, мы заключаем имя интерфейса, для которого мы хотим вызвать метод, в угловые скобки <InterfaceName> . (IntelliJ IDEA или Android Studio подскажут, как решить эту проблему, когда она возникнет.)

1
2
3
4
5
class classA : InterfaceA, InterfaceB {
    override fun funD() {
        super<InterfaceA>.funD()
    }
}

Здесь мы собираемся вызвать метод funD() InterfaceA . Проблема решена!

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

Базовый класс для всех классов в Котлине — Any .

1
2
class Person : Any {
}

Тип Any эквивалентен типу Object который есть в Java.

1
2
3
4
5
public open class Any {
    public open operator fun equals(other: Any?): Boolean
    public open fun hashCode(): Int
    public open fun toString(): String
}

Тип Any содержит следующие члены: методы equals() , hashcode() , а также toString() (аналогично Java).

Наши классы не должны явно расширять этот тип. Если вы не укажете явно, какой класс расширяет новый класс, класс расширяет Any неявно. По этой причине вам, как правило, не нужно включать : Any в ваш код — мы делаем это в приведенном выше коде для демонстрационных целей.

Давайте теперь посмотрим на создание классов в Kotlin с учетом наследования.

1
2
3
4
5
6
7
class Student {
 
}
 
class GraduateStudent : Student() {
 
}

В приведенном выше коде класс GraduateStudent расширяет суперкласс Student . Но этот код не скомпилируется. Почему? Потому что классы и методы являются final по умолчанию в Kotlin. Другими словами, они не могут быть расширены по умолчанию — в отличие от Java, где классы и методы открыты по умолчанию.

В соответствии с передовой практикой разработки программного обеспечения вам следует начать делать final классы и методы по умолчанию, т. Е. Если они специально не предназначены для переопределения или переопределения в подклассах. Команда Kotlin (JetBrains) применила эту философию кодирования и многие другие лучшие практики разработки при разработке этого современного языка.

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

1
2
3
open class Student {
 
}

Мы просто помещаем модификатор open перед ключевым словом class . Теперь мы дали указание компилятору разрешить открывать наш класс Student для расширения.

Как указывалось ранее, члены класса Kotlin также являются окончательными по умолчанию.

1
2
3
4
5
6
open class Student {
     
    open fun schoolFees(): BigDecimal {
       // do implementation
    }
}

В предыдущем коде мы schoolFees функцию schoolFees как open чтобы ее могли переопределить подклассы.

01
02
03
04
05
06
07
08
09
10
open class GraduateStudent : Student() {
 
    override fun schoolFees(): BigDecimal {
        return super.schoolFees() + calculateSchoolFees()
    }
 
    private fun calculateSchoolFees(): BigDecimal {
        // calculate and return school fees
    }
}

Здесь функция open schoolFees из Student суперкласса переопределяется классом GraduateStudent , добавляя модификатор override перед ключевым словом fun . Обратите внимание, что если вы переопределяете элемент суперкласса или интерфейса, переопределяющий элемент также будет open по умолчанию, как в примере ниже:

01
02
03
04
05
06
07
08
09
10
class ComputerScienceStudent : GraduateStudent() {
 
    override fun schoolFees(): BigDecimal {
        return super.schoolFees() + calculateSchoolFess()
    }
 
    private fun calculateSchoolFess(): BigDecimal {
        // calculate and return school fees
    }
}

Даже если мы не schoolFees() метод schoolFees() в классе GraduateStudent модификатором open , мы все равно можем переопределить его, как мы делали в классе ComputerScienceStudent . Чтобы мы могли это предотвратить, мы должны пометить главного члена как final .

Помните, что мы можем добавить новую функциональность в класс — даже если он окончательный — с помощью функций расширения в Kotlin. Чтобы узнать о расширенных функциях, ознакомьтесь с моей публикацией « Расширенные функции в Kotlin» . Кроме того, если вам нужно освежить в памяти то, как дать даже конечному классу новые свойства, не наследуя его, прочитайте раздел о свойствах расширения в моем посте « Дополнительные свойства и классы» .

  • Котлин
    Kotlin From Scratch: расширенные функции
  • Котлин
    Kotlin From Scratch: дополнительные свойства и классы

Если у нашего суперкласса есть основной конструктор, подобный этому:

1
2
3
open class Student(val firstName: String, val lastName: String) {
    // …
}

Тогда любой подкласс должен вызывать первичный конструктор суперкласса.

1
2
3
open class GraduateStudent(firstName: String, lastName: String) : Student(firstName, lastName) {
    // …
}

Мы можем просто создать объект класса GraduateStudent как обычно:

1
2
val graduateStudent = GraduateStudent(«Jon», «Snow»)
println(graduateStudent.firstName) // Jon

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

1
2
3
4
5
6
7
8
open class GraduateStudent : Student {
    // …
    private var thesis: String = «»
     
    constructor(firstName: String, lastName: String, thesis: String) : super(firstName, lastName) {
        this.thesis = thesis
    }
}

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

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

Давайте посмотрим на пример.

1
data class Person(val firstName: String, val lastName: String, val age: Int)

В вышеприведенном классе мы можем назначить типы String и Int для псевдонимов свойств Person используя модификатор typealias в Kotlin. Этот модификатор используется для создания псевдонима любого типа в Kotlin — включая те, которые вы создали.

1
2
3
4
typealias Name = String
typealias Age = Int
 
data class Person(val firstName: Name, val lastName: Name, val age: Age)

Как видите, мы создали псевдоним Name и Age для типов String и Int соответственно. Теперь мы заменили тип свойства firstName и lastName на наш псевдоним Name а также тип Int на псевдоним Age . Обратите внимание, что мы не создали никаких новых типов — вместо этого мы создали псевдоним для типов.

Они могут быть полезны, когда вы хотите обеспечить лучшее значение или семантику для типов в вашей кодовой базе Kotlin. Так что используйте их с умом!

В этом уроке вы узнали больше об объектно-ориентированном программировании в Kotlin. Мы рассмотрели следующее:

  • абстрактные классы
  • интерфейсы
  • наследование
  • введите псевдоним

Если вы изучали Kotlin в нашей серии статей Kotlin From Scratch , убедитесь, что вы набираете код, который видите, и запускаете его в своей среде IDE. Один отличный совет, чтобы по-настоящему понять новый язык программирования (или любую концепцию программирования), который вы изучаете, — убедиться, что вы не только читаете учебный ресурс или руководство, но также набираете реальный код и запускаете его!

В следующем уроке из серии Kotlin From Scratch вы познакомитесь с обработкой исключений в Kotlin. До скорого!

Чтобы узнать больше о языке Kotlin, я рекомендую посетить документацию Kotlin . Или посмотрите некоторые другие наши посты по разработке приложений для Android здесь на Envato Tuts!

  • Android SDK
    Java против Kotlin: стоит ли использовать Kotlin для разработки под Android?
    Джессика Торнсби
  • Android SDK
    Введение в компоненты архитектуры Android
    Жестяная мегали
  • Android SDK
    Начните с RxJava 2 для Android
    Джессика Торнсби