Kotlin — это современный язык программирования, который компилируется в байт-код Java. Он бесплатный и с открытым исходным кодом , и обещает сделать кодирование для Android еще более увлекательным.
В предыдущей статье вы узнали о классах и объектах в Kotlin. В этом уроке мы продолжим больше узнавать о свойствах, а также рассмотрим расширенные типы классов в Kotlin, изучив следующее:
- позднеинициализированные свойства
- встроенные свойства
- свойства расширения
- классы данных, перечисления, вложенные и запечатанные
1. Поздно инициализированные свойства
Мы можем объявить ненулевое свойство в Kotlin как позднее инициализированное . Это означает, что непустое свойство не будет инициализировано во время объявления со значением — фактическая инициализация не произойдет с помощью какого-либо конструктора — но вместо этого оно будет поздно инициализировано путем внедрения метода или зависимости.
Давайте рассмотрим пример, чтобы понять этот уникальный модификатор свойства.
01
02
03
04
05
06
07
08
09
10
11
|
class Presenter {
private var repository: Repository?
fun initRepository(repo: Repository): Unit {
this.repository = repo
}
}
class Repository {
fun saveAmount(amount: Double) {}
}
|
В приведенном выше коде мы объявили изменяемое свойство Repository
нулевым значением, которое имеет тип Repository
внутри класса Presenter
— и затем мы инициализировали это свойство значением null во время объявления. У нас есть метод initRepository()
в классе Presenter
который позже initRepository()
инициализирует это свойство с фактическим экземпляром Repository
. Обратите внимание, что этому свойству также может быть присвоено значение с помощью инжектора зависимостей, такого как Dagger.
Теперь для того, чтобы мы могли вызывать методы или свойства этого свойства repository
, мы должны выполнить нулевую проверку или использовать оператор безопасного вызова. Почему? Потому что свойство repository
имеет тип NULL ( Repository?
). (Если вам нужно освежить в Nullability, циклы и условия ).
1
2
3
4
|
// Inside Presenter class
fun save(amount: Double) {
repository?.saveAmount(amount)
}
|
Чтобы избежать необходимости выполнять нулевые проверки каждый раз, когда нам нужно вызвать метод свойства, мы можем пометить это свойство модификатором lateinit
— это означает, что мы объявили это свойство (которое является экземпляром другого класса) как позднюю инициализацию (что означает свойство будет инициализировано позже).
1
2
3
4
5
|
class Presenter {
private lateinit var repository: Repository
//…
}
|
Теперь, пока мы ждем, пока свойству будет присвоено значение, мы можем безопасно получить доступ к методам свойства, не делая никаких нулевых проверок. Инициализация свойства может происходить либо в методе установки, либо через внедрение зависимостей.
1
|
repository.saveAmount(amount)
|
Обратите внимание, что если мы попытаемся получить доступ к методам свойства до его инициализации, мы получим kotlin.UninitializedPropertyAccessException
вместо NullPointerException
. В этом случае сообщением об исключении будет «Латинское хранилище свойств не было инициализировано».
Обратите внимание также на следующие ограничения, накладываемые при задержке инициализации свойства с lateinit
:
- Он должен быть изменяемым (объявлено с помощью
var
). - Тип свойства не может быть примитивным типом, например,
Int
,Double
,Float
и так далее. - Свойство не может иметь пользовательский метод получения или установки.
2. Встроенные свойства
В расширенных функциях я ввел inline
модификатор для функций более высокого порядка — это помогает оптимизировать любые функции более высокого порядка, которые принимают лямбду в качестве параметра.
В Kotlin мы также можем использовать этот inline
модификатор свойств. Использование этого модификатора оптимизирует доступ к свойству.
Давайте посмотрим на практический пример.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class Student {
val nickName: String
get() {
println(«Nick name retrieved»)
return «koloCoder»
}
}
fun main(args: Array<String>) {
val student = Student()
print(student.nickName)
}
|
В приведенном выше коде у нас есть обычное свойство nickName
, у которого нет inline
модификатора. Если мы декомпилируем фрагмент кода, используя функцию Показать байт-код Kotlin (если вы находитесь в IntelliJ IDEA или Android Studio, используйте Инструменты > Kotlin > Показать байт-код Kotlin ), мы увидим следующий код Java:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
public final class Student {
@NotNull
public final String getNickName() {
String var1 = «Nick name retrieved»;
System.out.println(var1);
return «koloCoder»;
}
}
public final class InlineFunctionKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, «args»);
Student student = new Student();
String var2 = student.getNickName();
System.out.print(var2);
}
}
|
В сгенерированном Java-коде выше (некоторые элементы сгенерированного кода были удалены для краткости), вы можете видеть, что внутри метода main()
компилятор создал объект Student
, названный getNickName()
, и затем напечатал его возвращаемое значение ,
Давайте теперь вместо этого определим свойство как inline
и сравним сгенерированный байт-код.
1
2
3
|
// …
inline val nickName: String
// …
|
Мы просто вставляем inline
модификатор перед модификатором переменной: var
или val
. Вот байт-код, сгенерированный для этого встроенного свойства:
01
02
03
04
05
06
07
08
09
10
|
// …
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, «args»);
Student student = new Student();
String var3 = «Nick name retrieved»;
System.out.println(var3);
String var2 = «koloCoder»;
System.out.print(var2);
}
// …
|
Снова был удален некоторый код, но ключевой момент, на который следует обратить внимание, — это метод main()
. Компилятор скопировал тело функции свойства get()
и вставил его в сайт вызова (этот механизм аналогичен встроенным функциям).
Наш код был оптимизирован, так как нет необходимости создавать объект и вызывать метод получения свойства. Но, как обсуждалось в посте о встроенных функциях, у нас был бы больший байт-код, чем раньше, поэтому используйте его с осторожностью.
Также обратите внимание, что этот механизм будет работать для свойств, у которых нет вспомогательного поля (помните, что вспомогательное поле — это просто поле, которое используется свойствами, когда вы хотите изменить или использовать данные этого поля).
3. Свойства расширения
В расширенных функциях я также обсуждал функции расширения — они дают нам возможность расширять класс новыми функциями без необходимости наследования от этого класса. Kotlin также предоставляет аналогичный механизм для свойств, называемых свойствами расширения .
1
2
|
val String.upperCaseFirstLetter: String
get() = this.substring(0, 1).toUpperCase().plus(this.substring(1))
|
В посте « Дополнительные функции» мы определили функцию расширения uppercaseFirstLetter()
с типом получателя String
. Здесь мы преобразовали его в свойство расширения верхнего уровня. Обратите внимание, что вы должны определить метод получения для вашего свойства, чтобы это работало.
Таким образом, благодаря новым знаниям о свойствах расширения вы будете знать, что если вы когда-нибудь хотели, чтобы класс имел свойство, которое было недоступно, вы можете создать свойство расширения этого класса.
4. Классы данных
Начнем с типичного Java-класса или POJO (Plain Old Java Object).
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
public class BlogPost {
private final String title;
private final URI url;
private final String description;
private final Date publishDate;
//.. constructor not included for brevity’s sake
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BlogPost blogPost = (BlogPost) o;
if (title != null ? !title.equals(blogPost.title) : blogPost.title != null) return false;
if (url != null ? !url.equals(blogPost.url) : blogPost.url != null) return false;
if (description != null ? !description.equals(blogPost.description) : blogPost.description != null)
return false;
return publishDate != null ?
}
@Override
public int hashCode() {
int result = title != null ?
result = 31 * result + (url != null ? url.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (publishDate != null ? publishDate.hashCode() : 0);
return result;
}
@Override
public String toString() {
return «BlogPost{» +
«title=’» + title + ‘\» +
«, url=» + url +
«, description=’» + description + ‘\» +
«, publishDate=» + publishDate +
‘}’;
}
//.. setters and getters also ignored for brevity’s sake
}
|
Как видите, нам нужно явно кодировать методы доступа к свойствам класса: методы получения и установки, а также методы hashcode
, equals
и toString
(хотя IntelliJ IDEA, Android Studio или библиотека AutoValue могут помочь нам сгенерировать их). Мы видим этот тип шаблонного кода в основном на уровне данных типичного проекта Java. (Ради краткости я удалил полевые методы доступа и конструктор).
Круто то, что команда Kotlin предоставила нам модификатор data
для классов, чтобы исключить написание этих шаблонов.
Давайте вместо этого напишем предыдущий код на Kotlin.
1
|
data class BlogPost(var title: String, var url: URI, var description: String, var publishDate: Date)
|
Потрясающие! Мы просто указываем модификатор data
перед ключевым словом class
для создания класса данных — точно так же, как мы делали в нашем классе BlogPost
Kotlin выше. Теперь для нас будут созданы методы equals
, hashcode
, toString
, copy
и множественные компоненты. Обратите внимание, что класс данных может расширять другие классы (это новая функция Kotlin 1.1).
Метод equals
Этот метод сравнивает два объекта на равенство и возвращает true, если они равны, или false в противном случае. Другими словами, он сравнивает, если два экземпляра класса содержат одинаковые данные.
1
2
3
|
student.equals(student3)
// using the == in Kotlin
student == student3 // same as using equals()
|
В Kotlin использование оператора равенства ==
вызовет метод equals
за кулисами.
Метод hashCode
Этот метод возвращает целочисленное значение, используемое для быстрого хранения и извлечения данных, хранящихся в структуре данных коллекции на основе хеша, например, в типах коллекции HashMap
и HashSet
.
Метод toString
Этот метод возвращает String
представление объекта.
1
2
3
4
|
data class Person(var firstName: String, var lastName: String)
val person = Person(«Chike», «Mgbemena»)
println(person) // prints «Person(firstName=Chike, lastName=Mgbemena)»
|
Просто вызывая экземпляр класса, мы получаем возвращаемый нам строковый объект — Kotlin вызывает для нас объект toString()
. Но если мы не введем ключевое слово data
, посмотрим, каким будет наше строковое представление объекта:
1
|
com.chike.kotlin.classes.Person@2f0e140b
|
Гораздо менее информативно!
Метод copy
Этот метод позволяет нам создать новый экземпляр объекта с одинаковыми значениями свойств. Другими словами, он создает копию объекта.
1
2
3
4
|
val person1 = Person(«Chike», «Mgbemena»)
println(person1) // Person(firstName=Chike, lastName=Mgbemena)
val person2 = person1.copy()
println(person2) // Person(firstName=Chike, lastName=Mgbemena)
|
Одна из замечательных особенностей метода copy
в Kotlin — это возможность изменять свойства во время копирования.
1
2
|
val person3 = person1.copy(lastName = «Onu»)
println(person3) //Person3(firstName=Chike, lastName=Onu)
|
Если вы Java-кодер, этот метод похож на метод clone()
вы уже знакомы. Но метод copy
Kotlin имеет более мощные функции.
Разрушительная Декларация
В классе Person
у нас также есть два метода, автоматически сгенерированных для нас компилятором из-за ключевого слова data
помещенного в класс. Эти два метода имеют префикс «component», затем следует суффикс числа: component1()
, component2()
. Каждый из этих методов представляет отдельные свойства типа. Обратите внимание, что суффикс соответствует порядку свойств, объявленных в первичном конструкторе.
Итак, в нашем примере вызов component1()
вернет имя, а вызов component2()
вернет фамилию.
1
2
|
println(person3.component1()) // Chike
println(person3.component2()) // Onu
|
Вызов свойств с использованием этого стиля трудно понять и прочитать, поэтому явный вызов имени свойства намного лучше. Однако эти неявно созданные свойства имеют очень полезную цель: они позволяют нам делать декларацию деструктурирования, в которой мы можем назначить каждый компонент локальной переменной.
1
2
|
val (firstName, lastName) = Person(«Angelina», «Jolie»)
println(firstName + » » + lastName) // Angelina Jolie
|
То, что мы сделали здесь, — это непосредственное назначение первого и второго свойств ( firstName
и lastName
) типа Person
переменным firstName
и lastName
соответственно. Я также обсуждал этот механизм, известный как объявление деструктурирования, в последнем разделе поста « Пакеты и основные функции» .
5. Вложенные классы
В посте « Больше удовольствия с функциями» я говорил вам, что Kotlin имеет поддержку локальных или вложенных функций — функции, которая объявлена внутри другой функции. Ну, Kotlin также аналогично поддерживает вложенные классы — класс, созданный внутри другого класса.
1
2
3
4
5
6
|
class OuterClass {
class NestedClass {
fun nestedClassFunc() { }
}
}
|
Мы даже вызываем открытые функции вложенного класса, как показано ниже — вложенный класс в Kotlin эквивалентен static
вложенному классу в Java. Обратите внимание, что вложенные классы не могут хранить ссылку на их внешний класс.
1
2
|
val nestedClass = OuterClass.NestedClass()
nestedClass.nestedClassFunc()
|
Мы также можем установить вложенный класс как закрытый — это означает, что мы можем создать только экземпляр NestedClass
в рамках OuterClass
.
Внутренний класс
Внутренние классы, с другой стороны, могут ссылаться на внешний класс, в котором он был объявлен. Чтобы создать внутренний класс, мы inner
ключевое слово inner
перед ключевым словом class
во вложенном классе.
01
02
03
04
05
06
07
08
09
10
11
|
class OuterClass() {
val oCPropt: String = «Yo»
inner class InnerClass {
fun innerClassFunc() {
val outerClass = this@OuterClass
print(outerClass.oCPropt) // prints «Yo»
}
}
}
|
Здесь мы OuterClass
на OuterClass
из InnerClass
, используя this@OuterClass
.
6. Перечисление классов
Тип enum объявляет набор констант, представленных идентификаторами. Этот особый вид класса создается ключевым словом enum
, которое указывается перед ключевым словом class
.
1
2
3
4
5
|
enum class Country {
NIGERIA,
GHANA,
CANADA
}
|
Чтобы получить значение перечисления на основе его имени (как в Java), мы делаем это:
1
|
Country.valueOf(«NIGERIA»)
|
Или мы можем использовать вспомогательный метод Kotlin enumValueOf<T>()
для общего доступа к константам:
1
|
enumValueOf<Country>(«NIGERIA»)
|
Кроме того, мы можем получить все значения (например, для перечисления Java) следующим образом:
1
|
Country.values()
|
Наконец, мы можем использовать вспомогательный метод Kotlin enumValues<T>()
чтобы получить все записи enum общим способом:
1
|
enumValues<Country>()
|
Это возвращает массив, содержащий записи enum.
Enum Constructors
Как и обычный класс, тип enum
может иметь свой собственный конструктор со свойствами, связанными с каждой константой enum.
1
2
3
4
5
|
enum class Country(val callingCode: Int) {
NIGERIA (234),
USA (1),
GHANA (233)
}
|
В первичном конструкторе перечислимого типа Country
мы определили неизменяемое свойство callingCodes
для каждой константы перечисления. В каждой из констант мы передали аргумент конструктору.
Затем мы можем получить доступ к свойству констант следующим образом:
1
2
|
val country = Country.NIGERIA
print(country.callingCode) // 234
|
7. Запечатанные Классы
Запечатанный класс в Kotlin — это абстрактный класс (вы никогда не собираетесь создавать объекты из него), который могут расширяться другими классами. Эти подклассы определены в теле запечатанного класса — в том же файле. Поскольку все эти подклассы определены внутри тела запечатанного класса, мы можем узнать все возможные подклассы, просто просмотрев файл.
Давайте посмотрим на практический пример.
1
2
3
4
5
6
7
|
// shape.kt
sealed class Shape
class Circle : Shape()
class Triangle : Shape()
class Rectangle: Shape()
|
Чтобы объявить класс как закрытый, мы вставляем модификатор sealed
перед модификатором класса в заголовок объявления класса — в нашем случае мы объявили класс Shape
как sealed
. Запечатанный класс неполон без своих подклассов — так же, как и типичный абстрактный класс — поэтому мы должны объявить отдельные подклассы внутри одного и того же файла (в данном случае shape.kt ). Обратите внимание, что вы не можете определить подкласс запечатанного класса из другого файла.
В нашем коде выше мы указали, что класс Shape
может быть расширен только классами Circle
, Triangle
и Rectangle
.
Запечатанные классы в Kotlin имеют следующие дополнительные правила:
- Мы можем добавить модификатор
abstract
в запечатанный класс, но это избыточно, потому что запечатанные классы являются абстрактными по умолчанию. - Запечатанные классы не могут иметь модификатор
open
илиfinal
. - Мы также можем свободно объявлять классы данных и объекты как подклассы для запечатанного класса (они все еще должны быть объявлены в том же файле).
- Запечатанным классам не разрешено иметь открытые конструкторы — их конструкторы по умолчанию являются закрытыми.
Классы, расширяющие подклассы запечатанного класса, могут быть помещены либо в тот же файл, либо в другой файл. Подкласс запечатанного класса должен быть помечен модификатором open
(вы узнаете больше о наследовании в Kotlin в следующем посте).
1
2
3
4
5
6
|
// employee.kt
sealed class Employee
open class Artist : Employee()
// musician.kt
class Musician : Artist()
|
Запечатанный класс и его подклассы действительно удобны в выражении when
. Например:
1
2
3
4
5
|
fun whatIsIt(shape: Shape) = when (shape) {
is Circle -> println(«A circle»)
is Triangle -> println(«A triangle»)
is Rectangle -> println(«A rectangle»)
}
|
Здесь компилятор умен, чтобы убедиться, что мы рассмотрели все возможные случаи. Это означает, что нет необходимости добавлять предложение else
.
Если бы мы вместо этого сделали следующее:
1
2
3
4
|
fun whatIsIt(shape: Shape) = when (shape) {
is Circle -> println(«A circle»)
is Triangle -> println(«A triangle»)
}
|
Код не будет компилироваться, потому что мы не включили все возможные варианты. У нас будет следующая ошибка:
1
|
Kotlin: ‘when’ expression must be exhaustive, add necessary ‘is Rectangle’ branch or ‘else’ branch instead.
|
Таким образом, мы можем либо включить регистр is Rectangle
либо включить условие else
для завершения выражения when
.
Вывод
В этом уроке вы узнали больше о занятиях в Котлине. Мы рассмотрели следующее о свойствах класса:
- поздняя инициализация
- встроенные свойства
- свойства расширения
Кроме того, вы узнали о некоторых классных и продвинутых классах, таких как data, enum, nested и sealed. В следующем уроке из серии Kotlin From Scratch вы познакомитесь с интерфейсами и наследованием в Kotlin. До скорого!
Чтобы узнать больше о языке Kotlin, я рекомендую посетить документацию Kotlin . Или посмотрите некоторые другие наши посты по разработке приложений для Android здесь на Envato Tuts!