Эта практическая работа является частью курса Kotlin Bootcamp for Programmers . Вы получите максимальную пользу от этого курса, если будете выполнять задания последовательно. В зависимости от вашего уровня знаний, вы можете пропустить некоторые разделы. Этот курс ориентирован на программистов, владеющих объектно-ориентированным языком программирования и желающих изучить Kotlin .
Введение
В этой лабораторной работе вы создадите программу на Kotlin и узнаете о классах и объектах в Kotlin. Большая часть материала будет вам знакома, если вы знаете другой объектно-ориентированный язык программирования, но в Kotlin есть несколько важных отличий, которые позволяют сократить объём кода. Вы также узнаете об абстрактных классах и делегировании интерфейсов.
Вместо создания одного примера приложения уроки этого курса направлены на углубление ваших знаний, но при этом они частично независимы друг от друга, чтобы вы могли бегло просмотреть знакомые разделы. Чтобы связать их воедино, многие примеры используют тему аквариума. А если вы хотите узнать всё об аквариуме, ознакомьтесь с курсом Kotlin Bootcamp for Programmers на Udacity.
Что вам уже следует знать
- Основы Kotlin, включая типы, операторы и циклы
- Синтаксис функций Kotlin
- Основы объектно-ориентированного программирования
- Основы IDE, такие как IntelliJ IDEA или Android Studio
Чему вы научитесь
- Как создавать классы и получать доступ к свойствам в Kotlin
- Как создавать и использовать конструкторы классов в Kotlin
- Как создать подкласс и как работает наследование
- Об абстрактных классах, интерфейсах и делегировании интерфейсов
- Как создавать и использовать классы данных
- Как использовать синглтоны, перечисления и запечатанные классы
Что ты будешь делать?
- Создать класс со свойствами
- Создать конструктор для класса
- Создать подкласс
- Изучите примеры абстрактных классов и интерфейсов.
- Создайте простой класс данных
- Узнайте больше о синглтонах, перечислениях и запечатанных классах
Следующие термины программирования должны быть вам уже знакомы:
- Классы — это чертежи объектов. Например, класс
Aquarium— это чертеж для создания объекта «Аквариум». - Объекты являются экземплярами классов; объект аквариума — это один настоящий
Aquarium. - Свойства — это характеристики классов, такие как длина, ширина и высота
Aquarium. - Методы , также называемые функциями-членами , представляют собой функциональность класса. Методы — это то, что можно «делать» с объектом. Например, можно применить
fillWithWater()к объектуAquarium. - Интерфейс — это спецификация, которую может реализовать класс. Например, очистка свойственна не только аквариумам, но и другим объектам, и для разных объектов она обычно выполняется схожим образом. Таким образом, можно создать интерфейс
Clean, определяющий методclean(). КлассAquariumможет реализовать интерфейсCleanдля очистки аквариума мягкой губкой. - Пакеты — это способ группировки связанного кода для его упорядочивания или создания библиотеки кода. После создания пакета вы можете импортировать его содержимое в другой файл и повторно использовать код и классы из него.
В этом задании вы создадите новый пакет и класс с некоторыми свойствами и методом.
Шаг 1: Создайте пакет
Пакеты помогут вам упорядочить свой код.
- На панели «Проект» в разделе «Проект Hello Kotlin» щелкните правой кнопкой мыши папку src .
- Выберите «Создать» > «Пакет» и назовите его
example.myapp.
Шаг 2: Создайте класс со свойствами
Классы определяются с помощью ключевого слова class , а имена классов по соглашению начинаются с заглавной буквы.
- Щелкните правой кнопкой мыши по пакету example.myapp .
- Выберите Создать > Файл/Класс Kotlin .
- В разделе Kind выберите Class и назовите класс
Aquarium. IntelliJ IDEA добавит имя пакета в файл и создаст для вас пустой классAquarium. - В классе
Aquariumопределите и инициализируйтеvarсвойства ширины, высоты и длины (в сантиметрах). Инициализируйте свойства значениями по умолчанию.
package example.myapp
class Aquarium {
var width: Int = 20
var height: Int = 40
var length: Int = 100
} Под капотом Kotlin автоматически создает геттеры и сеттеры для свойств, определенных вами в классе Aquarium , поэтому вы можете получить к свойствам прямой доступ, например, myAquarium.length .
Шаг 3: Создайте функцию main()
Создайте новый файл с именем main.kt для хранения функции main() .
- На панели «Проект» слева щелкните правой кнопкой мыши пакет example.myapp .
- Выберите Создать > Файл/Класс Kotlin .
- В раскрывающемся списке «Вид» выберите « Файл » и назовите файл
main.ktIntelliJ IDEA включает имя пакета, но не включает определение класса для файла. - Определите функцию
buildAquarium()и внутри неё создайте экземпляр классаAquarium. Чтобы создать экземпляр, обратитесь к классу, как к функцииAquarium(). Это вызовет конструктор класса и создаст экземпляр классаAquarium, аналогично использованиюnewв других языках программирования. - Определите функцию
main()и вызовитеbuildAquarium().
package example.myapp
fun buildAquarium() {
val myAquarium = Aquarium()
}
fun main() {
buildAquarium()
}Шаг 4: Добавьте метод
- В классе
Aquariumдобавьте метод для печати свойств размеров аквариума.
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
}- В
main.kt, вbuildAquarium(), вызовите методprintSize()дляmyAquarium.
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
}- Запустите программу, щёлкнув по зелёному треугольнику рядом с функцией
main(). Наблюдайте за результатом.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm
- В
buildAquarium()добавьте код для установки высоты 60 и выведите измененные свойства размеров.
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
myAquarium.height = 60
myAquarium.printSize()
}- Запустите программу и посмотрите на результат.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm Width: 20 cm Length: 100 cm Height: 60 cm
В этом задании вы создадите конструктор для класса и продолжите работу со свойствами.
Шаг 1: Создание конструктора
На этом этапе вы добавляете конструктор к классу Aquarium , созданному в первом задании. В предыдущем примере каждый экземпляр Aquarium создаётся с одинаковыми размерами. Вы можете изменить размеры после создания, настроив свойства, но проще изначально задать правильный размер.
В некоторых языках программирования конструктор определяется путём создания метода внутри класса с тем же именем, что и у класса. В Kotlin конструктор определяется непосредственно в объявлении класса, указывая параметры в скобках, как если бы класс был методом. Как и функции в Kotlin, эти параметры могут включать значения по умолчанию.
- В созданном ранее классе
Aquariumизмените определение класса, включив в него три параметра конструктора со значениями по умолчанию дляlength,widthиheight, и назначьте их соответствующим свойствам.
class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
// Dimensions in cm
var length: Int = length
var width: Int = width
var height: Int = height
...
}- Более компактный подход в Kotlin — определение свойств непосредственно в конструкторе с помощью
varилиval, при этом Kotlin также автоматически создаёт геттеры и сеттеры. В этом случае определения свойств можно удалить из тела класса.
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}- При создании объекта
Aquariumс помощью этого конструктора вы можете не указывать аргументы и получить значения по умолчанию, либо указать только некоторые из них, либо указать все аргументы и создатьAquariumсовершенно произвольного размера. В функцииbuildAquarium()попробуйте различные способы создания объектаAquariumс использованием именованных параметров.
fun buildAquarium() {
val aquarium1 = Aquarium()
aquarium1.printSize()
// default height and length
val aquarium2 = Aquarium(width = 25)
aquarium2.printSize()
// default width
val aquarium3 = Aquarium(height = 35, length = 110)
aquarium3.printSize()
// everything custom
val aquarium4 = Aquarium(width = 25, height = 35, length = 110)
aquarium4.printSize()
}- Запустите программу и посмотрите на результат.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm Width: 25 cm Length: 100 cm Height: 40 cm Width: 20 cm Length: 110 cm Height: 35 cm Width: 25 cm Length: 110 cm Height: 35 cm
Обратите внимание, что вам не пришлось перегружать конструктор и писать отдельную версию для каждого из этих случаев (плюс ещё несколько для других комбинаций). Kotlin создаёт всё необходимое из значений по умолчанию и именованных параметров.
Шаг 2: Добавьте блоки инициализации
В примерах конструкторов, представленных выше, просто объявляются свойства и присваиваются им значения выражений. Если вашему конструктору требуется больше кода инициализации, его можно разместить в одном или нескольких блоках init . На этом этапе вы добавляете несколько блоков init в класс Aquarium .
- В классе
Aquariumдобавьте блокinitдля вывода информации об инициализации объекта и второй блок для вывода объема в литрах.
class Aquarium (var length: Int = 100, var width: Int = 20, var height: Int = 40) {
init {
println("aquarium initializing")
}
init {
// 1 liter = 1000 cm^3
println("Volume: ${width * length * height / 1000} l")
}
}- Запустите программу и посмотрите на результат.
aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 40 cm
aquarium initializing
Volume: 100 l
Width: 25 cm Length: 100 cm Height: 40 cm
aquarium initializing
Volume: 77 l
Width: 20 cm Length: 110 cm Height: 35 cm
aquarium initializing
Volume: 96 l
Width: 25 cm Length: 110 cm Height: 35 cm Обратите внимание, что блоки init выполняются в том порядке, в котором они указаны в определении класса, и все они выполняются при вызове конструктора.
Шаг 3: Узнайте о вторичных конструкторах
На этом этапе вы узнаете о вторичных конструкторах и добавите один в свой класс. Помимо основного конструктора, который может содержать один или несколько блоков init , класс Kotlin также может иметь один или несколько вторичных конструкторов, что позволяет перегружать конструкторы, то есть конструкторы с разными аргументами.
- В классе
Aquariumдобавьте вторичный конструктор, принимающий в качестве аргумента количество рыб, используя ключевое словоconstructor. Создайте свойствоvaltank для расчётного объёма аквариума в литрах, исходя из количества рыб. Предположим, что на каждую рыбу приходится 2 литра (2000 см³) воды, плюс небольшое дополнительное пространство, чтобы вода не проливалась.
constructor(numberOfFish: Int) : this() {
// 2,000 cm^3 per fish + extra room so water doesn't spill
val tank = numberOfFish * 2000 * 1.1
}- Внутри вторичного конструктора сохраните длину и ширину (которые были заданы в первичном конструкторе) такими же и рассчитайте высоту, необходимую для того, чтобы резервуар получил заданный объем.
// calculate the height needed
height = (tank / (length * width)).toInt()- В функцию
buildAquarium()добавьте вызов для созданияAquariumс помощью нового вторичного конструктора. Выведите размер и объём.
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}- Запустите программу и посмотрите на результат.
⇒ aquarium initializing Volume: 80 l Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
Обратите внимание, что объем выводится дважды: один раз блоком init в первичном конструкторе перед выполнением вторичного конструктора и один раз кодом в buildAquarium() .
Вы могли бы также включить ключевое слово constructor в основной конструктор, но в большинстве случаев это не обязательно.
Шаг 4: Добавьте новый получатель свойств
На этом этапе вы добавляете явный метод получения значения свойства. Kotlin автоматически определяет методы получения и установки значения при определении свойств, но иногда значение свойства необходимо скорректировать или рассчитать. Например, выше вы вывели объём Aquarium . Вы можете сделать объём доступным как свойство, определив переменную и метод получения значения. Поскольку volume необходимо рассчитать, метод получения значения должен возвращать вычисленное значение, что можно сделать с помощью однострочной функции.
- В классе
Aquariumопределите свойствоIntс именемvolumeи определите методget(), который вычисляет объем в следующей строке.
val volume: Int
get() = width * height * length / 1000 // 1000 cm^3 = 1 l- Удалите блок
init, который печатает громкость. - Удалите код в
buildAquarium(), который выводит объем. - В методе
printSize()добавьте строку для печати объема.
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm "
)
// 1 l = 1000 cm^3
println("Volume: $volume l")
}- Запустите программу и посмотрите на результат.
⇒ aquarium initializing Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
Размеры и объем те же, что и раньше, но объем выводится только один раз после полной инициализации объекта как первичным, так и вторичным конструктором.
Шаг 5: Добавьте установщик свойств
На этом этапе вы создаете новый установщик свойств для тома.
- В классе
Aquariumизменитеvolumeнаvar, чтобы ее можно было задать более одного раза. - Добавьте сеттер для свойства
volume, добавив методset()под геттером, который пересчитывает высоту на основе указанного количества воды. По соглашению, имя сеттер-параметра —value, но вы можете изменить его при желании.
var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}- В
buildAquarium()добавьте код для установки объёма аквариума 70 литров. Выведите новый размер.
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
aquarium6.volume = 70
aquarium6.printSize()
}- Запустите программу еще раз и посмотрите, как изменилась высота и объем.
⇒ aquarium initialized
Width: 20 cm Length: 100 cm Height: 31 cm
Volume: 62 l
Width: 20 cm Length: 100 cm Height: 35 cm
Volume: 70 l До сих пор в коде не было модификаторов видимости, таких как public или private . Это связано с тем, что по умолчанию всё в Kotlin является public, а это означает, что ко всему можно получить доступ отовсюду, включая классы, методы, свойства и переменные-члены.
В Kotlin классы, объекты, интерфейсы, конструкторы, функции, свойства и их сеттеры могут иметь модификаторы видимости :
-
publicозначает «видимо за пределами класса». По умолчанию всё открыто, включая переменные и методы класса. -
internalозначает, что он будет виден только внутри этого модуля. Модуль — это набор файлов Kotlin, скомпилированных вместе, например, библиотека или приложение. -
privateозначает, что он будет виден только в этом классе (или исходном файле, если вы работаете с функциями). -
protected— это то же самое, что иprivate, но он также будет виден всем подклассам.
Дополнительную информацию см. в разделе Модификаторы видимости в документации Kotlin.
Переменные-члены
Свойства внутри класса, или переменные-члены, по умолчанию являются public . Если они определены с помощью var , они становятся изменяемыми, то есть доступными для чтения и записи. Если они определены с помощью val , после инициализации они доступны только для чтения.
Если вам нужно свойство, которое ваш код может читать или записывать, а внешний код может только читать, вы можете оставить свойство и его геттер как публичные и объявить сеттер закрытым, как показано ниже.
var volume: Int
get() = width * height * length / 1000
private set(value) {
height = (value * 1000) / (width * length)
}В этом задании вы узнаете, как работают подклассы и наследование в Kotlin. Они похожи на то, что вы видели в других языках, но есть и некоторые отличия.
В Kotlin по умолчанию классы не могут быть подклассифицированы. Аналогично, свойства и переменные-члены не могут быть переопределены подклассами (хотя к ним можно получить доступ).
Чтобы разрешить создание подклассов, необходимо пометить класс как open . Аналогично, необходимо пометить свойства и переменные-члены как open , чтобы переопределить их в подклассе. Ключевое слово open необходимо для предотвращения случайной утечки деталей реализации в рамках интерфейса класса.
Шаг 1: Откройте класс «Аквариум».
На этом этапе вы делаете класс Aquarium open , чтобы иметь возможность переопределить его на следующем этапе.
- Пометьте класс
Aquariumи все его свойства ключевым словомopen.
open class Aquarium (open var length: Int = 100, open var width: Int = 20, open var height: Int = 40) {
open var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}- Добавьте свойство открытой
shapeсо значением"rectangle".
open val shape = "rectangle"- Добавьте свойство открытой
waterс геттером, который возвращает 90% объемаAquarium.
open var water: Double = 0.0
get() = volume * 0.9- Добавьте код в метод
printSize(), чтобы вывести форму и количество воды в процентах от объема.
fun printSize() {
println(shape)
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
// 1 l = 1000 cm^3
println("Volume: $volume l Water: $water l (${water/volume*100.0}% full)")
}- В
buildAquarium()измените код, чтобы создатьAquariumсwidth = 25,length = 25иheight = 40.
fun buildAquarium() {
val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
aquarium6.printSize()
}- Запустите программу и посмотрите на новый результат.
⇒ aquarium initializing rectangle Width: 25 cm Length: 25 cm Height: 40 cm Volume: 25 l Water: 22.5 l (90.0% full)
Шаг 2: Создайте подкласс
- Создайте подкласс
Aquariumс именемTowerTank, который реализует резервуар с закруглённой цилиндрической формой вместо прямоугольного. Вы можете добавитьTowerTankнижеAquarium, поскольку вы можете добавить другой класс в том же файле, что и классAquarium. - В
TowerTankпереопределите свойствоheight, определенное в конструкторе. Чтобы переопределить свойство, используйте ключевое словоoverrideв подклассе.
- Сделайте так, чтобы конструктор
TowerTankпринимал значение типаdiameter. Используйте значениеdiameterкак дляlength, так и дляwidthпри вызове конструктора в суперклассеAquarium.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {- Переопределите свойство объёма для вычисления цилиндра. Формула для цилиндра: пи, умноженное на квадрат радиуса, умноженный на высоту. Вам необходимо импортировать константу
PIизjava.lang.Math.
override var volume: Int
// ellipse area = π * r1 * r2
get() = (width/2 * length/2 * height / 1000 * PI).toInt()
set(value) {
height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
}- В
TowerTankизмените свойствоwaterна 80% от объема.
override var water = volume * 0.8- Измените
shapeна"cylinder".
override val shape = "cylinder"- Ваш окончательный класс
TowerTankдолжен выглядеть примерно так, как показано в коде ниже.
Aquarium.kt :
package example.myapp
import java.lang.Math.PI
... // existing Aquarium class
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
override var volume: Int
// ellipse area = π * r1 * r2
get() = (width/2 * length/2 * height / 1000 * PI).toInt()
set(value) {
height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
}
override var water = volume * 0.8
override val shape = "cylinder"
}- В
buildAquarium()создайтеTowerTankдиаметром 25 см и высотой 45 см. Выведите размеры.
main.kt:
package example.myapp
fun buildAquarium() {
val myAquarium = Aquarium(width = 25, length = 25, height = 40)
myAquarium.printSize()
val myTower = TowerTank(diameter = 25, height = 40)
myTower.printSize()
}- Запустите программу и посмотрите на результат.
⇒ aquarium initializing rectangle Width: 25 cm Length: 25 cm Height: 40 cm Volume: 25 l Water: 22.5 l (90.0% full) aquarium initializing cylinder Width: 25 cm Length: 25 cm Height: 40 cm Volume: 18 l Water: 14.4 l (80.0% full)
Иногда требуется определить общее поведение или свойства, общие для нескольких связанных классов. Kotlin предлагает два способа сделать это: интерфейсы и абстрактные классы. В этой задаче вы создадите абстрактный класс AquariumFish для свойств, общих для всех рыб. Вы создадите интерфейс FishAction для определения поведения, общего для всех рыб.
- Ни абстрактный класс, ни интерфейс не могут быть созданы сами по себе, что означает, что вы не можете создавать объекты этих типов напрямую.
- Абстрактные классы имеют конструкторы.
- Интерфейсы не могут иметь никакой конструкторской логики или хранить какое-либо состояние.
Шаг 1. Создание абстрактного класса
- В example.myapp создайте новый файл
AquariumFish.kt. - Создайте класс, также называемый
AquariumFish, и обозначьте его какabstract. - Добавьте одно
Stringсвойство,colorи пометьте его какabstract.
package example.myapp
abstract class AquariumFish {
abstract val color: String
}- Создайте два подкласса
AquariumFish:SharkиPlecostomus. - Поскольку
colorабстрактный, подклассы должны его реализовывать. СделайтеSharkсерым, аPlecostomusзолотым.
class Shark: AquariumFish() {
override val color = "gray"
}
class Plecostomus: AquariumFish() {
override val color = "gold"
}- В файле main.kt создайте функцию
makeFish()для тестирования классов. Создайте экземплярыSharkиPlecostomus, а затем выведите цвет каждого из них. - Удалите предыдущий тестовый код в
main()и добавьте вызовmakeFish(). Ваш код должен выглядеть примерно так, как показано ниже.
main.kt :
package example.myapp
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
println("Plecostomus: ${pleco.color}")
}
fun main () {
makeFish()
}- Запустите программу и посмотрите на результат.
⇒ Shark: gray Plecostomus: gold
На следующей диаграмме представлены классы Shark и Plecostomus , которые являются подклассами абстрактного класса AquariumFish .

Шаг 2. Создание интерфейса
- В AquariumFish.kt создайте интерфейс
FishActionс методомeat().
interface FishAction {
fun eat()
}- Добавьте
FishActionк каждому подклассу и реализуйтеeat(), заставив его выводить на экран то, что делает рыба.
class Shark: AquariumFish(), FishAction {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
class Plecostomus: AquariumFish(), FishAction {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}- В функции
makeFish()заставьте каждую созданную вами рыбу что-нибудь съесть, вызвавeat().
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
shark.eat()
println("Plecostomus: ${pleco.color}")
pleco.eat()
}- Запустите программу и посмотрите на результат.
⇒ Shark: gray hunt and eat fish Plecostomus: gold eat algae
На следующей диаграмме представлены классы Shark и Plecostomus , оба из которых состоят из интерфейса FishAction и реализуют его.

Когда следует использовать абстрактные классы, а когда интерфейсы
Приведенные выше примеры просты, но если у вас много взаимосвязанных классов, абстрактные классы и интерфейсы могут помочь сделать ваш дизайн более понятным, организованным и простым в обслуживании.
Как отмечалось выше, абстрактные классы могут иметь конструкторы, а интерфейсы — нет, но в остальном они очень похожи. Итак, когда следует использовать каждый из них?
При использовании интерфейсов для создания класса функциональность класса расширяется за счёт содержащихся в нём экземпляров классов. Композиция, как правило, упрощает повторное использование и анализ кода, чем наследование от абстрактного класса. Кроме того, в классе можно использовать несколько интерфейсов, но создавать подклассы можно только от одного абстрактного класса.
Композиция часто приводит к лучшей инкапсуляции , меньшей связанности (взаимозависимости), более чистым интерфейсам и более удобному коду. По этим причинам композиция с интерфейсами является предпочтительным решением. С другой стороны, наследование от абстрактного класса, как правило, естественным образом подходит для решения некоторых задач. Поэтому следует отдавать предпочтение композиции, но когда наследование имеет смысл, Kotlin позволяет делать и это!
- Используйте интерфейс, если у вас много методов и одна или две реализации по умолчанию, например, как в
AquariumActionниже.
interface AquariumAction {
fun eat()
fun jump()
fun clean()
fun catchFish()
fun swim() {
println("swim")
}
}- Используйте абстрактный класс всякий раз, когда не удаётся завершить класс. Например, возвращаясь к классу
AquariumFish, можно реализовать всеFishActionAquariumFishи предоставить реализацию по умолчанию дляeat, оставивcolorабстрактным, поскольку для рыбы цвета по умолчанию не существует.
interface FishAction {
fun eat()
}
abstract class AquariumFish: FishAction {
abstract val color: String
override fun eat() = println("yum")
} В предыдущем задании были представлены абстрактные классы, интерфейсы и идея композиции. Делегирование интерфейса — это продвинутый метод, при котором методы интерфейса реализуются вспомогательным объектом (или делегатом), который затем используется классом. Этот метод может быть полезен при использовании интерфейса в серии несвязанных классов: необходимая функциональность интерфейса добавляется в отдельный вспомогательный класс, и каждый из классов использует экземпляр этого вспомогательного класса для реализации этой функциональности.
В этой задаче вы используете делегирование интерфейса для добавления функциональности в класс.
Шаг 1: Создайте новый интерфейс
- В AquariumFish.kt удалите класс
AquariumFish. Вместо наследования от классаAquariumFish,PlecostomusиSharkреализуют интерфейсы как для действий рыб, так и для их цвета. - Создайте новый интерфейс
FishColor, который определяет цвет как строку.
interface FishColor {
val color: String
}- Измените
Plecostomusтак, чтобы он реализовал два интерфейса:FishActionиFishColor. Вам нужно переопределитьcolorизFishColorиeat()изFishAction.
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}- Измените класс
Sharkтак, чтобы он также реализовал два интерфейса,FishActionиFishColor, вместо наследования отAquariumFish.
class Shark: FishAction, FishColor {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}- Ваш готовый код должен выглядеть примерно так:
package example.myapp
interface FishAction {
fun eat()
}
interface FishColor {
val color: String
}
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
class Shark: FishAction, FishColor {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}Шаг 2: Создайте класс-синглтон
Затем вы реализуете настройку для делегирования, создав вспомогательный класс, реализующий FishColor . Вы создаёте базовый класс GoldColor , реализующий FishColor — он просто сообщает, что его цвет — золотой.
Создавать несколько экземпляров GoldColor не имеет смысла, поскольку все они будут делать одно и то же. Поэтому Kotlin позволяет объявить класс, в котором можно создать только один экземпляр, используя ключевое слово object вместо class . Kotlin создаст этот экземпляр, и на него будет ссылаться имя класса. После этого все остальные объекты смогут использовать только этот экземпляр — создать другие экземпляры этого класса невозможно. Если вы знакомы с шаблоном Singleton , то именно так реализуются синглтоны в Kotlin.
- В AquariumFish.kt создайте объект
GoldColorи переопределите цвет.
object GoldColor : FishColor {
override val color = "gold"
}Шаг 3: Добавьте делегирование интерфейса для FishColor
Теперь вы готовы использовать делегирование интерфейса.
- В AquariumFish.kt удалите переопределение
colorуPlecostomus. - Измените класс
Plecostomusтак, чтобы его цвет получался изGoldColor. Это делается путём добавленияby GoldColorк объявлению класса, создавая делегирование. Это означает, что вместо реализацииFishColorследует использовать реализацию, предоставляемуюGoldColor. Таким образом, каждый раз при обращении кcolorон делегируетсяGoldColor.
class Plecostomus: FishAction, FishColor by GoldColor {
override fun eat() {
println("eat algae")
}
}При заданном классе все рыбы рода Pleco будут золотистыми, но на самом деле эти рыбы бывают самых разных цветов. Решить эту проблему можно, добавив параметр конструктора для цвета, указав GoldColor в качестве цвета по умолчанию для Plecostomus .
- Измените класс
Plecostomusтак, чтобы он принималfishColorв конструкторе, и установите значение по умолчаниюGoldColor. Измените делегирование сby GoldColorнаby fishColor.
class Plecostomus(fishColor: FishColor = GoldColor): FishAction,
FishColor by fishColor {
override fun eat() {
println("eat algae")
}
}Шаг 4: Добавьте делегирование интерфейса для FishAction
Таким же образом можно использовать делегирование интерфейса для FishAction .
- В AquariumFish.kt создайте класс
PrintingFishAction, который реализуетFishAction, который принимаетString,food, а затем печатает то, что ест рыба.
class PrintingFishAction(val food: String) : FishAction {
override fun eat() {
println(food)
}
}- В классе
Plecostomusудалите переопределяющую функциюeat(), поскольку вы замените ее делегированием. - В объявлении
PlecostomusделегируйтеFishActionфункцииPrintingFishAction, передав"eat algae". - При всем этом делегировании в теле класса
Plecostomusнет кода, поэтому удалите{}, поскольку все переопределения обрабатываются делегированием интерфейса.
class Plecostomus (fishColor: FishColor = GoldColor):
FishAction by PrintingFishAction("eat algae"),
FishColor by fishColorНа следующей диаграмме представлены классы Shark и Plecostomus , оба состоящие из интерфейсов PrintingFishAction и FishColor , но с делегированной им реализацией.

Делегирование интерфейсов — мощный инструмент, и вам следует тщательно продумать его применение при каждом использовании абстрактного класса в другом языке. Оно позволяет использовать композицию для подключения поведений, вместо того чтобы создавать множество подклассов, каждый из которых имеет свою специализацию.
Класс данных похож на struct в некоторых других языках программирования — он существует в основном для хранения данных, но объект класса данных всё равно остаётся объектом. Объекты классов данных в Kotlin обладают рядом дополнительных преимуществ, например, функциями печати и копирования. В этом задании вы создадите простой класс данных и узнаете о поддержке классов данных в Kotlin.
Шаг 1: Создайте класс данных
- Добавьте новый пакет
decorв пакет example.myapp для хранения нового кода. Щёлкните правой кнопкой мыши по example.myapp на панели «Проект» и выберите «Файл» > «Создать» > «Пакет» . - В пакете создайте новый класс с названием
Decoration.
package example.myapp.decor
class Decoration {
}- Чтобы сделать
Decorationклассом данных, добавьте к объявлению класса ключевое словоdata. - Добавьте
Stringсвойство с именемrocks, чтобы предоставить классу некоторые данные.
data class Decoration(val rocks: String) {
}- В файле, за пределами класса, добавьте функцию
makeDecorations()для создания и печати экземпляраDecorationс"granite".
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
}- Добавьте функцию
main()для вызоваmakeDecorations()и запустите программу. Обратите внимание на логичный вывод, поскольку это класс данных.
⇒ Decoration(rocks=granite)
- В
makeDecorations()создайте еще два объектаDecoration, которые оба являются «сланцами», и распечатайте их.
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
val decoration2 = Decoration("slate")
println(decoration2)
val decoration3 = Decoration("slate")
println(decoration3)
}
- В
makeDecorations()добавьте оператор print, который выводит результат сравненияdecoration1сdecoration2, а также второй оператор сравненияdecoration3сdecoration2. Используйте метод equals(), предоставляемый классами данных.
println (decoration1.equals(decoration2))
println (decoration3.equals(decoration2))- Запустите свой код.
⇒ Decoration(rocks=granite) Decoration(rocks=slate) Decoration(rocks=slate) false true
Шаг 2. Используйте деструктуризацию
Чтобы получить свойства объекта данных и назначить их переменным, вы можете назначать их по одному, вот так.
val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diverВместо этого вы можете создать переменные, по одной для каждого свойства, и назначить объект данных группе переменных. Kotlin помещает значение свойства в каждую переменную.
val (rock, wood, diver) = decorationЭто называется деструктуризацией и является полезным сокращённым вариантом. Количество переменных должно соответствовать количеству свойств, а переменные назначаются в том порядке, в котором они объявлены в классе. Вот полный пример, который вы можете попробовать в Decoration.kt .
// Here is a data class with 3 properties.
data class Decoration2(val rocks: String, val wood: String, val diver: String){
}
fun makeDecorations() {
val d5 = Decoration2("crystal", "wood", "diver")
println(d5)
// Assign all properties to variables.
val (rock, wood, diver) = d5
println(rock)
println(wood)
println(diver)
}⇒ Decoration2(rocks=crystal, wood=wood, diver=diver) crystal wood diver
Если вам не нужны одно или несколько свойств, вы можете пропустить их, используя _ вместо имени переменной, как показано в коде ниже.
val (rock, _, diver) = d5В этом задании вы узнаете о некоторых специализированных классах в Kotlin, включая следующие:
- Классы Singleton
- Перечисления
- Запечатанные классы
Шаг 1: Вспомните классы-одиночки
Вспомните предыдущий пример с классом GoldColor .
object GoldColor : FishColor {
override val color = "gold"
}Поскольку каждый экземпляр GoldColor выполняет одно и то же действие, он объявлен как object , а не как class , чтобы сделать его синглтоном. Может существовать только один экземпляр.
Шаг 2: Создайте перечисление
Kotlin также поддерживает перечисления, которые позволяют перечислять объекты и ссылаться на них по имени, как в других языках. Чтобы объявить перечисление, добавьте к объявлению ключевое слово enum . Для простого объявления перечисления требуется только список имён, но вы также можете определить одно или несколько полей, связанных с каждым именем.
- В Decoration.kt попробуйте пример перечисления.
enum class Color(val rgb: Int) {
RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}Перечисления немного похожи на одиночные элементы — в перечислении может быть только одно значение каждого типа, причем только одно. Например, может быть только одно Color.RED , одно Color.GREEN и одно Color.BLUE . В этом примере значения RGB присваиваются свойству rgb для представления компонентов цвета. Вы также можете получить порядковый номер перечисления, используя свойство ordinal , и его имя, используя свойство name .
- Попробуйте другой пример перечисления.
enum class Direction(val degrees: Int) {
NORTH(0), SOUTH(180), EAST(90), WEST(270)
}
fun main() {
println(Direction.EAST.name)
println(Direction.EAST.ordinal)
println(Direction.EAST.degrees)
}⇒ EAST 2 90
Шаг 3: Создайте запечатанный класс
Запечатанный класс — это класс, подклассы которого можно создать только внутри файла, в котором он объявлен. При попытке создать подкласс этого класса в другом файле возникнет ошибка.
Поскольку классы и подклассы находятся в одном файле, Kotlin статически распознаёт все подклассы. То есть, во время компиляции компилятор видит все классы и подклассы и понимает, что это всё, поэтому он может выполнить дополнительные проверки.
- В AquariumFish.kt попробуйте пример запечатанного класса, придерживаясь водной тематики.
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()
fun matchSeal(seal: Seal): String {
return when(seal) {
is Walrus -> "walrus"
is SeaLion -> "sea lion"
}
}Класс Seal не может быть подклассифицирован в другом файле. Если вы хотите добавить больше типов Seal , их необходимо добавить в тот же файл. Это делает запечатанные классы безопасным способом представления фиксированного количества типов. Например, запечатанные классы отлично подходят для возврата успешного или ошибочного выполнения из сетевого API .
В этом уроке мы рассмотрели много материала. Хотя многое из этого вам знакомо по другим объектно-ориентированным языкам программирования, Kotlin добавляет ряд функций, которые делают код лаконичным и удобочитаемым.
Классы и конструкторы
- Определите класс в Kotlin с помощью
class. - Kotlin автоматически создает сеттеры и геттеры для свойств.
- Определите основной конструктор непосредственно в определении класса. Например:
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) - Если основному конструктору требуется дополнительный код, напишите его в одном или нескольких блоках
init. - Класс может определить один или несколько вторичных конструкторов с помощью
constructor, но в стиле Kotlin вместо этого используется фабричная функция.
Модификаторы видимости и подклассы
- Все классы и функции в Kotlin по умолчанию являются
public, но вы можете использовать модификаторы, чтобы изменить видимость наinternal,privateилиprotected. - Чтобы создать подкласс, родительский класс должен быть помечен как
open. - Чтобы переопределить методы и свойства в подклассе, методы и свойства должны быть помечены как
openв родительском классе. - Запечатанный класс можно создать только в том же файле, где он определён. Чтобы создать запечатанный класс, добавьте к объявлению префикс
sealed.
Классы данных, одиночные объекты и перечисления
- Создайте класс данных, добавив к объявлению префикс
data. - Деструктуризация — это сокращенная форма присвоения свойств объекта
dataотдельным переменным. - Создайте класс-синглтон, используя
objectвместоclass. - Определите перечисление с помощью
enum class.
Абстрактные классы, интерфейсы и делегирование
- Абстрактные классы и интерфейсы — это два способа обеспечения общего поведения между классами.
- Абстрактный класс определяет свойства и поведение, но оставляет реализацию подклассам.
- Интерфейс определяет поведение и может предоставлять реализации по умолчанию для некоторых или всех поведений.
- При использовании интерфейсов для создания класса функциональность класса расширяется за счет экземпляров классов, которые он содержит.
- Делегирование интерфейса использует композицию, но также делегирует реализацию классам интерфейса.
- Композиция — это мощный способ добавить функциональность классу с помощью делегирования интерфейса. В целом композиция предпочтительнее, но для некоторых задач лучше подходит наследование от абстрактного класса.
Документация Kotlin
Если вам нужна дополнительная информация по какой -либо теме в этом курсе, или если вы застряли, https://kotlinlang.org - ваша лучшая отправная точка.
- Классы и наследство
- Конструкторы
- Заводские функции
- Свойства и поля
- Модификаторы видимости
- Абстрактные классы
- Интерфейсы
- Делегация
- Классы данных
- Равенство
- Деструктуризация
- Объектные объявления
- Enum Clasess
- Запечатанные занятия
- Обработка дополнительных ошибок с использованием герметичных классов Kotlin
Учебники Котлина
Веб-сайт https://try.kotlinlang.org включает в себя богатые учебники под названием Kotlin Koans, веб-интерпретатор и полный набор справочной документации с примерами.
Курс Udacity
Чтобы просмотреть курс Udacity по этой теме, см. Kotlin Bootcamp для программистов .
IntelliJ IDEA
Документация для идеи IntelliJ можно найти на веб -сайте Jetbrains.
В этом разделе перечислены возможные домашние задания для студентов, работающих над этой лабораторной работой в рамках курса, проводимого преподавателем. Преподаватель должен выполнить следующие действия:
- При необходимости задавайте домашнее задание.
- Объясните учащимся, как следует сдавать домашние задания.
- Оцените домашние задания.
Преподаватели могут использовать эти предложения так часто или редко, как пожелают, и могут свободно задавать любые другие домашние задания, которые они сочтут подходящими.
Если вы работаете с этой лабораторной работой самостоятельно, можете использовать эти домашние задания для проверки своих знаний.
Ответьте на эти вопросы
Вопрос 1
У классов есть особый метод, который служит планом для создания объектов этого класса. Как называется метод?
▢ Строитель
▢ Желатель
▢ Конструктор
▢ План
Вопрос 2
Какое из следующих утверждений о интерфейсах и абстрактных классах неверно?
▢ Абстрактные классы могут иметь конструкторы.
▢ Интерфейсы не могут иметь конструкторы.
▢ Интерфейсы и абстрактные классы могут быть созданы напрямую.
▢ Абстрактные свойства должны быть реализованы подклассами абстрактного класса.
Вопрос 3
Что из следующего не является модификатором видимости котлина для свойств, методов и т. Д.?
▢ internal
▢ nosubclass
▢ protected
▢ private
Вопрос 4
Рассмотрим этот класс данных:
data class Fish(val name: String, val species:String, val colors:String)
Что из следующего не является действительным кодом для создания и разрушения объекта Fish ?
▢ val (name1, species1, colors1) = Fish("Pat", "Plecostomus", "gold")
▢ val (name2, _, colors2) = Fish("Bitey", "shark", "gray")
▢ val (name3, species3, _) = Fish("Amy", "angelfish", "blue and black stripes")
▢ val (name4, species4, colors4) = Fish("Harry", "halibut")
Вопрос 5
Допустим, у вас есть зоопарк с большим количеством животных, о которых нужно позаботиться. Что из следующего не будет частью реализации заботы?
▢ interface для различных видов пищевых продуктов, которые едят животные.
▢ Класс abstract Caretaker , из которого вы можете создавать различные типы опекунов.
▢ interface для подачи чистой воды животному.
▢ Класс data для записи в графике кормления.
Перейдите к следующему уроку:
Для обзора курса, включая ссылки на другие коделаб, см. «Kotlin Bootcamp для программистов: добро пожаловать на курс».