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!



