Эта лабораторная работа является частью курса Kotlin Bootcamp for Programmers . Вы получите максимальную отдачу от этого курса, если будете последовательно работать с лабораториями кода. В зависимости от ваших знаний, вы можете просмотреть некоторые разделы. Этот курс ориентирован на программистов, которые знают объектно-ориентированный язык и хотят изучить Kotlin .
Введение
В этой лаборатории кода вы создадите программу на Kotlin и узнаете о классах и объектах в Kotlin. Большая часть этого контента будет вам знакома, если вы знаете другой объектно-ориентированный язык, но у Kotlin есть несколько важных отличий, позволяющих сократить объем кода, который вам нужно написать. Вы также узнаете об абстрактных классах и делегировании интерфейсов.
Вместо того, чтобы создавать один пример приложения, уроки этого курса предназначены для расширения ваших знаний, но они частично независимы друг от друга, поэтому вы можете просматривать разделы, с которыми вы знакомы. Чтобы связать их вместе, во многих примерах используется тема аквариума. А если вы хотите увидеть полную историю аквариума, ознакомьтесь с курсом Kotlin Bootcamp for Programmers Udacity.
Что вы уже должны знать
- Основы Kotlin, включая типы, операторы и циклы
- Синтаксис функций Котлина
- Основы объектно-ориентированного программирования
- Основы IDE, такие как IntelliJ IDEA или Android Studio.
Что вы узнаете
- Как создавать классы и получать доступ к свойствам в Kotlin
- Как создавать и использовать конструкторы классов в Kotlin
- Как создать подкласс и как работает наследование
- Об абстрактных классах, интерфейсах и делегировании интерфейсов
- Как создавать и использовать классы данных
- Как использовать синглтоны, перечисления и запечатанные классы
Что ты будешь делать
- Создайте класс со свойствами
- Создать конструктор для класса
- Создать подкласс
- Изучите примеры абстрактных классов и интерфейсов
- Создайте простой класс данных
- Узнайте о синглтонах, перечислениях и закрытых классах
Следующие термины программирования должны быть вам уже знакомы:
- Классы — это чертежи объектов. Например, класс «
Aquarium
» — это схема создания объекта-аквариума. - Объекты являются экземплярами классов; объект-аквариум - это один настоящий
Aquarium
. - Свойства — это характеристики классов, такие как длина, ширина и высота
Aquarium
. - Методы , также называемые функциями-членами , являются функциональностью класса. Методы — это то, что вы можете «делать» с объектом. Например, вы можете
fillWithWater()
для объектаAquarium
. - Интерфейс — это спецификация, которую может реализовать класс. Например, чистка является общей для объектов, отличных от аквариумов, и чистка обычно происходит одинаковыми способами для разных объектов. Таким образом, у вас может быть интерфейс с именем
Clean
, который определяет методclean()
. КлассAquarium
может реализовать интерфейсClean
для очистки аквариума мягкой губкой. - Пакеты — это способ группировать связанный код для его организации или для создания библиотеки кода. После создания пакета вы можете импортировать его содержимое в другой файл и повторно использовать содержащийся в нем код и классы.
В этой задаче вы создаете новый пакет и класс с некоторыми свойствами и методом.
Шаг 1. Создайте пакет
Пакеты могут помочь вам организовать ваш код.
- На панели Project в проекте Hello Kotlin щелкните правой кнопкой мыши папку src .
- Выберите « Создать» > «Пакет» и назовите его
example.myapp
».
Шаг 2: Создайте класс со свойствами
Классы определяются с помощью ключевого слова class
, а имена классов по соглашению начинаются с заглавной буквы.
- Щелкните правой кнопкой мыши пакет example.myapp .
- Выберите « Создать» > «Файл/класс Kotlin» .
- В разделе « Тип » выберите « Класс » и назовите класс «
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.kt
IntelliJ 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
. Создайте свойствоval
tank для расчетного объема аквариума в литрах на основе количества рыб. Предположим, что на одну рыбу приходится 2 литра (2000 см^3) воды плюс небольшое дополнительное пространство, чтобы вода не проливалась.
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()
под получателем, который пересчитывает высоту на основе предоставленногоvolume
воды. По соглашению имя параметра установки —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 общедоступно, а это означает, что все доступно везде, включая классы, методы, свойства и переменные-члены.
В 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
, вы можете заставить всеAquariumFish
реализовыватьFishAction
и предоставить реализацию по умолчанию для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")
}
}
С таким классом все Plecos будут золотыми, но на самом деле эти рыбы бывают разных цветов. Вы можете решить эту проблему, добавив параметр конструктора для цвета с 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()
добавьте оператор печати, который выводит результат сравнения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, в том числе о следующих:
- Одноэлементные классы
- перечисления
- Запечатанные классы
Шаг 1: Вспомните одноэлементные классы
Вспомните предыдущий пример с классом GoldColor
.
object GoldColor : FishColor {
override val color = "gold"
}
Поскольку каждый экземпляр GoldColor
делает одно и то же, он объявляется как object
, а не как class
, чтобы сделать его одноэлементным. Может быть только один экземпляр.
Шаг 2: Создайте перечисление
Kotlin также поддерживает перечисления, которые позволяют вам перечислять что-то и ссылаться на это по имени, как и в других языках. Объявите перечисление, поставив перед объявлением ключевое слово enum
. Базовое объявление 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
.
Абстрактные классы, интерфейсы и делегирование
- Абстрактные классы и интерфейсы — это два способа разделить общее поведение между классами.
- Абстрактный класс определяет свойства и поведение, но оставляет реализацию подклассам.
- Интерфейс определяет поведение и может предоставлять реализации по умолчанию для части или всего поведения.
- Когда вы используете интерфейсы для создания класса, функциональность класса расширяется за счет содержащихся в нем экземпляров класса.
- Делегирование интерфейса использует композицию, но также делегирует реализацию классам интерфейса.
- Композиция — это мощный способ добавить функциональность классу с помощью делегирования интерфейса. В целом предпочтительнее композиция, но для некоторых проблем лучше подходит наследование от абстрактного класса.
Котлин документация
Если вам нужна дополнительная информация по какой-либо теме этого курса или если вы застряли, https://kotlinlang.org — лучшая отправная точка.
- Classes and inheritance
- Constructors
- Factory functions
- Properties and fields
- Visibility modifiers
- Abstract classes
- Interfaces
- Delegation
- Data classes
- Equality
- Destructuring
- Object declarations
- Enum classes
- Sealed classes
- Handling Optional Errors Using Kotlin Sealed Classes
Kotlin tutorials
The https://try.kotlinlang.org website includes rich tutorials called Kotlin Koans, a web-based interpreter , and a complete set of reference documentation with examples.
Udacity course
To view the Udacity course on this topic, see Kotlin Bootcamp for Programmers .
IntelliJ IDEA
Documentation for the IntelliJ IDEA can be found on the JetBrains website.
В этом разделе перечислены возможные домашние задания для студентов, которые работают с этой кодовой лабораторией в рамках курса, проводимого инструктором. Инструктор должен сделать следующее:
- При необходимости задайте домашнее задание.
- Объясните учащимся, как сдавать домашние задания.
- Оценивайте домашние задания.
Преподаватели могут использовать эти предложения так мало или так часто, как они хотят, и должны свободно давать любые другие домашние задания, которые они считают подходящими.
Если вы работаете с этой кодовой лабораторией самостоятельно, не стесняйтесь использовать эти домашние задания, чтобы проверить свои знания.
Ответьте на эти вопросы
Вопрос 1
Classes have a special method that serves as a blueprint for creating objects of that class. What is the method called?
▢ A builder
▢ An instantiator
▢ A constructor
▢ A blueprint
вопрос 2
Which of the following statements about interfaces and abstract classes is NOT correct?
▢ Abstract classes can have constructors.
▢ Interfaces can't have constructors.
▢ Interfaces and abstract classes can be instantiated directly.
▢ Abstract properties must be implemented by subclasses of the abstract class.
Вопрос 3
Which of the following is NOT a Kotlin visibility modifier for properties, methods, etc.?
▢ internal
▢ nosubclass
▢ protected
▢ private
Question 4
Consider this data class:
data class Fish(val name: String, val species:String, val colors:String)
Which of the following is NOT valid code to create and destructure a Fish
object?
▢ 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")
Question 5
Let's say you own a zoo with lots of animals that all need to be taken care of. Which of the following would NOT be part of implementing caretaking?
▢ An interface
for different types of foods animals eat.
▢ An abstract Caretaker
class from which you can create different types of caretakers.
▢ An interface
for giving clean water to an animal.
▢ A data
class for an entry in a feeding schedule.
Proceed to the next lesson:
For an overview of the course, including links to other codelabs, see "Kotlin Bootcamp for Programmers: Welcome to the course."