Статьи Kotlin. Абстрактные классы и интерфейсы.
Post
Cancel

Kotlin. Абстрактные классы и интерфейсы.

Абстрактные классы и интерфейсы объединены мной в одну тему, так как по своей сути они очень похожи. И те и другие имеют отношение к “моделированию” классов. С их помощью мы можем показать, что у определённой группы классов есть что-то общее: то, что их отличает от всех остальных. Ключевая разница между ними лишь в том, как их применять.

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

Абстрактный класс - это класс, представляющий из себя “заготовку” для целого семейства классов, который описывает для них общий шаблон поведения. Такой класс не может быть создан. Т.е. нельзя создать его экземпляр. Он используются исключительно в качестве суперкласса, а его цель - моделирование поведения своего семейства, а также предоставление функционала для повторного использования. Другими словами абстрактный класс - это средство, для повторного использования кода.

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

Объявляется абстрактный класс при помощи ключевого слова abstract.

1
2
3
abstract class Tree {
  ...
}

Наследование от такого класса осуществляется с помощью оператора :. При этом абстрактному классу не нужен модификатор open, потому что он “открыт” для наследования по умолчанию.

1
2
3
class Pine : Tree() {
  ...
}

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

1
2
3
4
5
abstract class Tree {
  abstract val name: String
  abstract val description: String
  abstract fun info()
}

Каждый наследник обязан переопределять их все.

1
2
3
4
5
class Pine : Tree() {
  override val name = "Сосна"
  override val description = "Хвойное дерево с длинными иглами и округлыми шишками"
  override fun info() = "$name - ${description.toLowerCase()}."  
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
abstract class Tree {
  abstract val name: String
  abstract val description: String
  fun info(): String = "$name - ${description.toLowerCase()}."
}

...

class Pine : Tree() {
  override val name = "Сосна"
  override val description = "Хвойное дерево с длинными иглами и округлыми шишками"
}

...

val pine = Pine()
println(pine.info())

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

1
2
3
4
5
6
7
class Pine : Tree() {
  override val name = "Сосна"
  override val description = "Хвойное дерево с длинными иглами и округлыми шишками"

  // ошибка: функция "info" является "final" и не может быть переопределена
  override fun info() = description  
}

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

1
2
3
4
5
6
abstract class Tree {
  abstract val name: String
  abstract val description: String

  open fun info(): String = "$name - ${description.toLowerCase()}."
}

У абстрактного класса может быть конструктор.

1
2
3
abstract class Tree(val name: String, val description: String) {
  open fun info(): String = "$name - ${description.toLowerCase()}."
}

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

1
2
3
4
5
6
class Pine(name: String, description: String) : Tree(name, description)

...

val pine = Pine("Сосна", "Хвойное дерево с длинными иглами и округлыми шишками")
println(pine.info())

Интерфейсы

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

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

Объявляется интерфейс ключевым словом interface.

1
2
3
interface Cultivable {
  ...
}

Реализация интерфейса осуществляется аналогично наследованию: после имени класса ставится оператор : и название интерфейса.

1
2
3
4
5
6
7
abstract class Tree : Cultivable {
  ...
}

class Seaweed : Cultivable {
  ...
}

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

1
2
3
4
interface Cultivable {
  val bloom: Boolean
  fun startPhotosynthesis()
}

Класс должен реализовывать все абстрактные свойства и функции, определённые в интерфейсе.

1
2
3
4
5
6
7
8
9
10
abstract class Tree : Cultivable {
  abstract val name: String
  abstract val description: String
  open fun info(): String = "$name - ${description.toLowerCase()}."

  override val bloom = false
  override fun startPhotosynthesis() {
    ...
  }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
abstract class Tree : Cultivable {
  abstract val name: String
  abstract val description: String
  open fun info(): String = "$name - ${description.toLowerCase()}."

  override fun startPhotosynthesis() {
    ...
  }
}

class Pine : Tree() {
  override val name = "Сосна"
  override val description = "Хвойное дерево с длинными иглами и округлыми шишками"

  override val bloom = false
}

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

1
2
3
4
5
6
7
8
interface Cultivable {
  val bloom: Boolean
    get() = false

  fun startPhotosynthesis() {
    ...
  }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Fruitable {
  val fruit: String
    get() = "неплодоносный"
}

interface Cultivable : Fruitable {
  ...

  fun isFruitable() : Boolean {
    if(fruit == "неплодоносный") return false
    return true
  }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AppleTree() : Tree() {
  override val name = "Яблоня"
  override val description = "Фруктовое дерево"
  override val fruit = "яблоко"
}

...

val appleTree = AppleTree()
if(appleTree.isFruitable()) {
  println("Плод - ${appleTree.fruit}.")
} else {
  println("${appleTree.name} не плодоносит.")
}

Шпаргалка: абстрактный класс или интерфейс?

  • У вас есть семейство классов, из которых можно выделить общую сущность? Определите эту сущность в качестве абстрактного класса и она будет “заготовкой” для всего семейства.
  • Вам нужно создать более конкретную версию класса? Создайте подкласс этого класса и добавьте недостающее поведение.
  • Требует определить общее поведение для группы независимых друг от друга классов? Создайте интерфейс и реализуйте его теми классами, которым необходимо это поведение.

Ключевые моменты

  • Абстрактный класс - это “заготовка” для целого семейства классов.
  • Нельзя создать экземпляр абстрактного класса.
  • Абстрактный класс может содержать как абстрактные, так и конкретные реализации свойств и функций.
  • Класс, который содержит абстрактное свойство или функцию, должен быть объявлен абстрактным.
  • Абстрактный класс может быть без единого абстрактного свойства или функции.
  • У класса может быть только один суперкласс.
  • Наследники абстрактного класса должны переопределять все его абстрактные свойства и функции.
  • Чтобы наследники могли переопределять конкретные реализации свойств и функций, для них в абстрактном классе должен быть явно указан модификатор open.
  • У абстрактного класса может быть конструктор.
  • Интерфейс определяет поведение класса или общее поведение для группы независимых друг от друга классов.
  • Нельзя создать экземпляр интерфейса.
  • Интерфейс может содержать как абстрактные, так и конкретные реализации функций.
  • Свойства интерфейсов могут быть абстрактными, а могут иметь get() методы.
  • Класс может реализовывать несколько интерфейсов.
  • Класс должен реализовывать все абстрактные свойства и функции, определённые в интерфейсе.
  • Если интерфейс реализовывается абстрактным классом, то переопределение его абстрактных свойств и функций может быть передана наследникам абстрактного класса.
  • Интерфейс может реализовывать другой интерфейс.

Полезные ссылки

Abstract classes - официальная документация.
Абстрактные классы - перевод на русский (об абстрактных классах в самом низу статьи).
Interfaces - официальная документация.
Интерфейсы - перевод статьи про интерфейсы на русский.

This post is licensed under CC BY 4.0 by the author.

Activity (Активность, Операция)

RecyclerView