1. Добро пожаловать!
В этой лаборатории кода вы узнаете, как конвертировать код из Java в Kotlin. Вы также узнаете, что такое соглашения языка Kotlin и как гарантировать, что код, который вы пишете, им соответствует.
Эта лаборатория кода подходит любому разработчику, использующему Java и рассматривающему возможность переноса своего проекта на Kotlin. Мы начнем с пары классов Java, которые вы преобразуете в Kotlin с помощью IDE. Затем мы посмотрим на преобразованный код и посмотрим, как его можно улучшить, сделав более идиоматичным и избежав распространенных ошибок.
Что вы узнаете
Вы узнаете, как конвертировать Java в Kotlin. При этом вы изучите следующие функции и концепции языка Kotlin:
- Обработка возможности обнуления
- Реализация синглтонов
- Классы данных
- Обработка строк
- Оператор Элвиса
- Деструктуризация
- Свойства и свойства поддержки
- Аргументы по умолчанию и именованные параметры
- Работа с коллекциями
- Функции расширения
- Функции и параметры верхнего уровня
-
let
,apply
,with
иrun
ключевые слова
Предположения
Вы уже должны быть знакомы с Java.
Что вам понадобится
2. Приступаем к настройке
Создать новый проект
Если вы используете IntelliJ IDEA, создайте новый проект Java с помощью Kotlin/JVM.
Если вы используете Android Studio, создайте новый проект с шаблоном «Нет активности» . Выберите Kotlin в качестве языка проекта. Минимальный SDK может иметь любое значение, на результат это не повлияет.
Код
Мы создадим объект модели User
и одноэлементный класс Repository
, который работает с объектами User
и предоставляет списки пользователей и отформатированные имена пользователей.
Создайте новый файл с именем User.java
в папке app/java/< имя_вашего_пакета > и вставьте следующий код:
public class User {
@Nullable
private String firstName;
@Nullable
private String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
Вы заметите, что ваша IDE сообщает вам, @Nullable
не определен. Поэтому импортируйте androidx.annotation.Nullable
, если вы используете Android Studio, или org.jetbrains.annotations.Nullable
если вы используете IntelliJ.
Создайте новый файл с именем Repository.java
и вставьте следующий код:
import java.util.ArrayList;
import java.util.List;
public class Repository {
private static Repository INSTANCE = null;
private List<User> users = null;
public static Repository getInstance() {
if (INSTANCE == null) {
synchronized (Repository.class) {
if (INSTANCE == null) {
INSTANCE = new Repository();
}
}
}
return INSTANCE;
}
// keeping the constructor private to enforce the usage of getInstance
private Repository() {
User user1 = new User("Jane", "");
User user2 = new User("John", null);
User user3 = new User("Anne", "Doe");
users = new ArrayList();
users.add(user1);
users.add(user2);
users.add(user3);
}
public List<User> getUsers() {
return users;
}
public List<String> getFormattedUserNames() {
List<String> userNames = new ArrayList<>(users.size());
for (User user : users) {
String name;
if (user.getLastName() != null) {
if (user.getFirstName() != null) {
name = user.getFirstName() + " " + user.getLastName();
} else {
name = user.getLastName();
}
} else if (user.getFirstName() != null) {
name = user.getFirstName();
} else {
name = "Unknown";
}
userNames.add(name);
}
return userNames;
}
}
3. Объявление нулевых значений, val, var и классов данных
Наша IDE может неплохо справляться с автоматическим преобразованием кода Java в код Kotlin, но иногда требуется небольшая помощь. Давайте позволим нашей IDE выполнить первоначальный этап преобразования. Затем мы пройдемся по полученному коду, чтобы понять, как и почему он был преобразован таким образом.
Перейдите к файлу User.java
и преобразуйте его в Kotlin: Строка меню -> Код -> Преобразовать файл Java в файл Kotlin .
Если ваша IDE запрашивает исправление после преобразования, нажмите «Да» .
Вы должны увидеть следующий код Kotlin:
class User(var firstName: String?, var lastName: String?)
Обратите внимание, что User.java
был переименован в User.kt
Файлы Kotlin имеют расширение .kt.
В нашем классе Java User
у нас было два свойства: firstName
и lastName
. Каждый из них имел методы получения и установки, что делало его значение изменяемым. Ключевое слово Kotlin для изменяемых переменных — var
, поэтому преобразователь использует var
для каждого из этих свойств. Если бы в наших свойствах Java были только геттеры, они были бы доступны только для чтения и были бы объявлены как переменные val
. val
аналогичен ключевому слову final
в Java.
Одно из ключевых отличий между Kotlin и Java заключается в том, что Kotlin явно указывает, может ли переменная принимать нулевое значение. Это делается путем добавления ?
в объявление типа.
Поскольку мы пометили firstName
и lastName
как допускающие значение NULL, автоконвертер автоматически пометил свойства как допускающие значение NULL с помощью String?
. Если вы пометите свои элементы Java как ненулевые (используя org.jetbrains.annotations.NotNull
или androidx.annotation.NonNull
), конвертер распознает это и также сделает поля ненулевыми в Kotlin.
Базовое преобразование уже выполнено. Но мы можем написать это более идиоматично. Давайте посмотрим, как это сделать.
Класс данных
Наш класс User
содержит только данные. В Kotlin есть ключевое слово для классов с этой ролью: data
. Пометив этот класс как класс data
, компилятор автоматически создаст для нас геттеры и сеттеры. Он также будет производным от функцийquals equals()
, hashCode()
и toString()
.
Давайте добавим ключевое слово data
в наш класс User
:
data class User(var firstName: String?, var lastName: String?)
Kotlin, как и Java, может иметь первичный конструктор и один или несколько вторичных конструкторов. Конструктор в приведенном выше примере является основным конструктором класса User
. Если вы конвертируете класс Java, имеющий несколько конструкторов, конвертер также автоматически создаст несколько конструкторов в Kotlin. Они определяются с помощью ключевого слова constructor
.
Если мы хотим создать экземпляр этого класса, мы можем сделать это следующим образом:
val user1 = User("Jane", "Doe")
Равенство
В Котлине есть два типа равенства:
- Структурное равенство использует оператор
==
и вызываетequals()
чтобы определить, равны ли два экземпляра. - Ссылочное равенство использует оператор
===
и проверяет, указывают ли две ссылки на один и тот же объект.
Свойства, определенные в основном конструкторе класса данных, будут использоваться для проверки структурного равенства.
val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false
4. Аргументы по умолчанию, именованные аргументы
В Kotlin мы можем присваивать значения по умолчанию аргументам при вызове функций. Значение по умолчанию используется, когда аргумент опущен. В Kotlin конструкторы также являются функциями, поэтому мы можем использовать аргументы по умолчанию, чтобы указать, что значение по умолчанию для lastName
равно null
. Для этого мы просто присваиваем значению lastName
null
.
data class User(var firstName: String?, var lastName: String? = null)
// usage
val jane = User("Jane") // same as User("Jane", null)
val joe = User("Joe", "Doe")
Kotlin позволяет вам маркировать аргументы при вызове функций:
val john = User(firstName = "John", lastName = "Doe")
В качестве другого варианта использования предположим, что значением по умолчанию для firstName
является null
, а lastName
— нет. В этом случае, поскольку параметр по умолчанию будет предшествовать параметру без значения по умолчанию, вы должны вызвать функцию с именованными аргументами:
data class User(var firstName: String? = null, var lastName: String?)
// usage
val jane = User(lastName = "Doe") // same as User(null, "Doe")
val john = User("John", "Doe")
Значения по умолчанию — важная и часто используемая концепция в коде Kotlin. В нашей кодовой лаборатории мы хотим всегда указывать имя и фамилию в объявлении объекта User
, поэтому нам не нужны значения по умолчанию.
5. Инициализация объекта, сопутствующий объект и синглтоны
Прежде чем продолжить работу над кодом, убедитесь, что ваш класс User
является классом data
. Теперь давайте преобразуем класс Repository
в Kotlin. Результат автоматического преобразования должен выглядеть следующим образом:
import java.util.*
class Repository private constructor() {
private var users: MutableList<User?>? = null
fun getUsers(): List<User?>? {
return users
}
val formattedUserNames: List<String?>
get() {
val userNames: MutableList<String?> =
ArrayList(users!!.size)
for (user in users) {
var name: String
name = if (user!!.lastName != null) {
if (user!!.firstName != null) {
user!!.firstName + " " + user!!.lastName
} else {
user!!.lastName
}
} else if (user!!.firstName != null) {
user!!.firstName
} else {
"Unknown"
}
userNames.add(name)
}
return userNames
}
companion object {
private var INSTANCE: Repository? = null
val instance: Repository?
get() {
if (INSTANCE == null) {
synchronized(Repository::class.java) {
if (INSTANCE == null) {
INSTANCE =
Repository()
}
}
}
return INSTANCE
}
}
// keeping the constructor private to enforce the usage of getInstance
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users = ArrayList<Any?>()
users.add(user1)
users.add(user2)
users.add(user3)
}
}
Посмотрим, что сделал автоматический конвертер:
- Список
users
имеет значение NULL, поскольку экземпляр объекта не был создан во время объявления. - Функции в Kotlin, такие как
getUsers()
объявляются с модификаторомfun
. - Метод
getFormattedUserNames()
теперь является свойством с именемformattedUserNames
- Итерация по списку пользователей (которая изначально была частью
getFormattedUserNames(
)) имеет другой синтаксис, чем синтаксис Java. -
static
поле теперь является частьюcompanion object
- Добавлен блок
init
.
Прежде чем идти дальше, давайте немного очистим код. Если мы заглянем в конструктор, то заметим, что преобразователь включил список наших users
в изменяемый список, содержащий объекты, допускающие значение NULL. Хотя список действительно может быть нулевым, давайте предположим, что он не может содержать нулевых пользователей. Итак, давайте сделаем следующее:
- Удалить
?
вUser?
в объявлении типаusers
- Удалить
?
вUser?
для возвращаемого типаgetUsers()
, чтобы он возвращалList<User>?
Блок инициализации
В Kotlin основной конструктор не может содержать никакого кода, поэтому код инициализации помещается в блоки init
. Функционал тот же.
class Repository private constructor() {
...
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users = ArrayList<Any?>()
users.add(user1)
users.add(user2)
users.add(user3)
}
}
Большая часть кода init
обрабатывает инициализацию свойств. Это также можно сделать в декларации о собственности. Например, в версии нашего класса Repository
на Kotlin мы видим, что свойство пользователей было инициализировано в объявлении.
private var users: MutableList<User>? = null
static
свойства и методы Kotlin
В Java мы используем ключевое слово static
для полей или функций, чтобы сказать, что они принадлежат классу, а не экземпляру класса. Вот почему мы создали статическое поле INSTANCE
в нашем классе Repository
. Эквивалентом этого в Котлине является companion object
блок. Здесь вы также можете объявить статические поля и статические функции. Конвертер создал блок-компаньон и переместил сюда поле INSTANCE
.
Обработка синглтонов
Поскольку нам нужен только один экземпляр класса Repository
, мы использовали шаблон Singleton в Java. С помощью Kotlin вы можете применить этот шаблон на уровне компилятора, заменив ключевое слово class
на object
.
Удалите частный конструктор и замените определение класса object Repository
. Также удалите сопутствующий объект.
object Repository {
private var users: MutableList<User>? = null
fun getUsers(): List<User>? {
return users
}
val formattedUserNames: List<String>
get() {
val userNames: MutableList<String> =
ArrayList(users!!.size)
for (user in users) {
var name: String
name = if (user!!.lastName != null) {
if (user!!.firstName != null) {
user!!.firstName + " " + user!!.lastName
} else {
user!!.lastName
}
} else if (user!!.firstName != null) {
user!!.firstName
} else {
"Unknown"
}
userNames.add(name)
}
return userNames
}
// keeping the constructor private to enforce the usage of getInstance
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users = ArrayList<Any?>()
users.add(user1)
users.add(user2)
users.add(user3)
}
}
При использовании класса object
мы просто вызываем функции и свойства непосредственно объекта, например:
val formattedUserNames = Repository.formattedUserNames
Обратите внимание: если у свойства нет модификатора видимости, оно по умолчанию является общедоступным, как в случае со свойством formattedUserNames
в объекте Repository
.
6. Обработка возможности обнуления
При преобразовании класса Repository
в Kotlin автоматический преобразователь сделал список пользователей обнуляемым, поскольку он не был инициализирован объектом при его объявлении. В результате для всех случаев использования объекта users
ненулевой оператор утверждения !!
необходимо использовать. (Вы увидите users!!
и user!!
в преобразованном коде.) Символ !!
Оператор преобразует любую переменную в непустой тип, поэтому вы можете получить доступ к ее свойствам или вызвать функции. Однако исключение будет выдано, если значение переменной действительно равно нулю. Используя !!
, вы рискуете вызвать исключения во время выполнения.
Вместо этого предпочтите обрабатывать значение NULL, используя один из этих методов:
- Выполнение проверки на нулевое значение (
if (users != null) {...}
) - Использование оператора Элвиса
?:
(описано позже в кодовой лаборатории) - Использование некоторых стандартных функций Kotlin (описанных далее в кодовой лаборатории)
В нашем случае мы знаем, что список пользователей не обязательно должен иметь значение NULL, поскольку он инициализируется сразу после создания объекта (в блоке init
). Таким образом, мы можем напрямую создать экземпляр объекта users
при его объявлении.
При создании экземпляров типов коллекций Kotlin предоставляет несколько вспомогательных функций, которые сделают ваш код более читабельным и гибким. Здесь мы используем MutableList
для users
:
private var users: MutableList<User>? = null
Для простоты мы можем использовать функцию mutableListOf()
и указать тип элемента списка. mutableListOf<User>()
создает пустой список, который может содержать объекты User
. Поскольку тип данных переменной теперь может быть определен компилятором, удалите явное объявление типа свойства users
.
private val users = mutableListOf<User>()
Мы также изменили var
на val
, поскольку пользователи будут содержать ссылку на список пользователей, доступную только для чтения. Обратите внимание, что ссылка доступна только для чтения, поэтому она никогда не может указывать на новый список, но сам список по-прежнему изменчив (вы можете добавлять или удалять элементы).
Поскольку переменная users
уже инициализирована, удалите эту инициализацию из блока init
:
users = ArrayList<Any?>()
Тогда блок init
должен выглядеть так:
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users.add(user1)
users.add(user2)
users.add(user3)
}
Благодаря этим изменениям наше свойство users
теперь не равно нулю, и мы можем удалить все ненужное !!
появление оператора. Обратите внимание, что вы по-прежнему будете видеть ошибки компиляции в Android Studio, но продолжайте выполнять следующие несколько шагов лаборатории кода, чтобы устранить их.
val userNames: MutableList<String?> = ArrayList(users.size)
for (user in users) {
var name: String
name = if (user.lastName != null) {
if (user.firstName != null) {
user.firstName + " " + user.lastName
} else {
user.lastName
}
} else if (user.firstName != null) {
user.firstName
} else {
"Unknown"
}
userNames.add(name)
}
Кроме того, для значения userNames
, если вы укажете тип ArrayList
как содержащий Strings
, вы можете удалить явный тип в объявлении, поскольку он будет выведен.
val userNames = ArrayList<String>(users.size)
Деструктуризация
Kotlin позволяет деструктурировать объект на несколько переменных, используя синтаксис, называемый объявлением деструктуризации . Мы создаем несколько переменных и можем использовать их независимо.
Например, классы data
поддерживают деструктуризацию, поэтому мы можем деструктурировать объект User
в цикле for
на (firstName, lastName)
. Это позволяет нам работать напрямую со значениями firstName
и lastName
. Обновите цикл for
как показано ниже. Замените все экземпляры user.firstName
на firstName
и замените user.lastName
на lastName
.
for ((firstName, lastName) in users) {
var name: String
name = if (lastName != null) {
if (firstName != null) {
firstName + " " + lastName
} else {
lastName
}
} else if (firstName != null) {
firstName
} else {
"Unknown"
}
userNames.add(name)
}
если выражение
Имена в списке userNames пока не совсем в том формате, который нам нужен. Поскольку и lastName
, и firstName
могут иметь null
, нам необходимо учитывать возможность обнуления при построении списка отформатированных имен пользователей. Мы хотим отображать "Unknown"
если какое-либо имя отсутствует. Поскольку переменная name
не будет изменена после того, как она будет установлена один раз, мы можем использовать val
вместо var
. Сначала внесите это изменение.
val name: String
Взгляните на код, который устанавливает переменную имени. Вам может показаться новым видеть, что переменная устанавливается равной блоку кода if
/ else
. Это разрешено, поскольку в Котлине if
и when
являются выражениями — они возвращают значение. Последняя строка оператора if
будет присвоена name
. Единственная цель этого блока — инициализировать значение name
.
По сути, эта логика, представленная здесь, заключается в том, что если lastName
имеет значение NULL, для name
устанавливается значение firstName
или "Unknown"
.
name = if (lastName != null) {
if (firstName != null) {
firstName + " " + lastName
} else {
lastName
}
} else if (firstName != null) {
firstName
} else {
"Unknown"
}
Оператор Элвиса
Этот код можно написать более идиоматично, используя оператор Элвиса ?:
. Оператор elvis вернет выражение в левой части, если оно не равно нулю, или выражение в правой части, если левая часть равна нулю.
Итак, в следующем коде возвращается firstName
, если оно не равно нулю. Если firstName
имеет значение null, выражение возвращает значение справа "Unknown"
:
name = if (lastName != null) {
...
} else {
firstName ?: "Unknown"
}
7. Строковые шаблоны
Kotlin упрощает работу со String
с помощью шаблонов String . Строковые шаблоны позволяют ссылаться на переменные внутри строковых объявлений, используя символ $ перед переменной. Вы также можете поместить выражение в объявление строки, поместив выражение в { } и используя перед ним символ $. Пример: ${user.firstName}
.
В настоящее время ваш код использует конкатенацию строк для объединения firstName
и lastName
в имя пользователя.
if (firstName != null) {
firstName + " " + lastName
}
Вместо этого замените конкатенацию строк на:
if (firstName != null) {
"$firstName $lastName"
}
Использование строковых шаблонов может упростить ваш код.
Ваша IDE покажет вам предупреждения, если существует более идиоматический способ написания кода. Вы заметите волнистую линию в коде, и когда наведете на нее курсор, вы увидите предложение о том, как реорганизовать ваш код.
В настоящее время вы должны увидеть предупреждение о том, что объявление name
можно объединить с назначением. Давайте применим это. Поскольку тип переменной name
можно определить, мы можем удалить явное объявление типа String
. Теперь наши formattedUserNames
выглядят так:
val formattedUserNames: List<String?>
get() {
val userNames = ArrayList<String>(users.size)
for ((firstName, lastName) in users) {
val name = if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName
}
} else {
firstName ?: "Unknown"
}
userNames.add(name)
}
return userNames
}
Мы можем внести еще одну поправку. Наша логика пользовательского интерфейса отображает "Unknown"
, если имя и фамилия отсутствуют, поэтому мы не поддерживаем нулевые объекты. Таким образом, для типа данных formattedUserNames
замените List<String?>
на List<String>
.
val formattedUserNames: List<String>
8. Операции над коллекциями
Давайте подробнее рассмотрим метод получения formattedUserNames
и посмотрим, как можно сделать его более идиоматичным. Прямо сейчас код делает следующее:
- Создает новый список строк
- Перебирает список пользователей
- Создает форматированное имя для каждого пользователя на основе имени и фамилии пользователя.
- Возвращает вновь созданный список
val formattedUserNames: List<String>
get() {
val userNames = ArrayList<String>(users.size)
for ((firstName, lastName) in users) {
val name = if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName
}
} else {
firstName ?: "Unknown"
}
userNames.add(name)
}
return userNames
}
Kotlin предоставляет обширный список преобразований коллекций , которые делают разработку быстрее и безопаснее за счет расширения возможностей API коллекций Java. Одна из них — функция map
. Эта функция возвращает новый список, содержащий результаты применения данной функции преобразования к каждому элементу исходного списка. Таким образом, вместо того, чтобы создавать новый список и вручную перебирать список пользователей, мы можем использовать функцию map
и переместить логику, которая у нас была в цикле for
внутрь тела map
. По умолчанию имя текущего элемента списка, используемого в map
, — it
, но для удобства чтения вы можете заменить it
собственным именем переменной. В нашем случае назовем его user
:
val formattedUserNames: List<String>
get() {
return users.map { user ->
val name = if (user.lastName != null) {
if (user.firstName != null) {
"${user.firstName} ${user.lastName}"
} else {
user.lastName ?: "Unknown"
}
} else {
user.firstName ?: "Unknown"
}
name
}
}
Обратите внимание, что мы используем оператор Элвиса для возврата "Unknown"
, если user.lastName
имеет значение null, поскольку user.lastName
имеет тип String?
и для name
требуется String
.
...
else {
user.lastName ?: "Unknown"
}
...
Чтобы еще больше упростить задачу, мы можем полностью удалить переменную name
:
val formattedUserNames: List<String>
get() {
return users.map { user ->
if (user.lastName != null) {
if (user.firstName != null) {
"${user.firstName} ${user.lastName}"
} else {
user.lastName ?: "Unknown"
}
} else {
user.firstName ?: "Unknown"
}
}
}
9. Свойства и свойства поддержки
Мы увидели, что автоматический преобразователь заменил функцию getFormattedUserNames()
свойством formattedUserNames
, которое имеет собственный метод получения. Под капотом Kotlin по-прежнему генерирует метод getFormattedUserNames()
который возвращает List
.
В Java мы бы раскрывали свойства нашего класса через функции получения и установки. Kotlin позволяет нам лучше различать свойства класса, выраженные с помощью полей, и функциональные возможности, действия, которые может выполнять класс, выраженные с помощью функций. В нашем случае класс Repository
очень прост и не выполняет никаких действий, поэтому имеет только поля.
Логика, которая запускалась в функции Java getFormattedUserNames()
теперь срабатывает при вызове метода получения свойства formattedUserNames
Kotlin.
Хотя у нас нет явного поля, соответствующего свойству formattedUserNames
, Kotlin предоставляет нам автоматическое резервное поле с именем field
, к которому мы можем получить доступ при необходимости из пользовательских геттеров и сеттеров.
Однако иногда нам нужны дополнительные функции, которых не обеспечивает поле автоматического резервного копирования.
Давайте рассмотрим пример.
Внутри нашего класса Repository
у нас есть изменяемый список пользователей, который отображается в функции getUsers()
, сгенерированной из нашего Java-кода:
fun getUsers(): List<User>? {
return users
}
Поскольку мы не хотели, чтобы вызывающие класс Repository
изменяли список пользователей, мы создали функцию getUsers()
, которая возвращает List<User>
только для чтения. В Kotlin в таких случаях мы предпочитаем использовать свойства, а не функции. Точнее, мы бы предоставили доступный только для чтения List<User>
, поддерживаемый mutableListOf<User>
.
Сначала давайте переименуем users
в _users
. Выделите имя переменной, щелкните правой кнопкой мыши, чтобы выбрать «Рефакторинг» > «Переименовать переменную». Затем добавьте общедоступное свойство только для чтения, которое возвращает список пользователей. Назовем это users
:
private val _users = mutableListOf<User>()
val users: List<User>
get() = _users
На этом этапе вы можете удалить метод getUsers()
.
Благодаря вышеуказанному изменению частное свойство _users
становится вспомогательным свойством для общедоступного свойства users
. За пределами класса Repository
список _users
не подлежит изменению, поскольку потребители класса могут получить доступ к списку только через users
.
Когда users
вызываются из кода Kotlin, используется реализация List
из стандартной библиотеки Kotlin, где список нельзя изменить. Если users
вызываются из Java, используется реализация java.util.List
, где список можно изменять и доступны такие операции, как add() и Remove().
Полный код:
object Repository {
private val _users = mutableListOf<User>()
val users: List<User>
get() = _users
val formattedUserNames: List<String>
get() {
return _users.map { user ->
if (user.lastName != null) {
if (user.firstName != null) {
"${user.firstName} ${user.lastName}"
} else {
user.lastName ?: "Unknown"
}
} else {
user.firstName ?: "Unknown"
}
}
}
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
_users.add(user1)
_users.add(user2)
_users.add(user3)
}
}
10. Функции и свойства верхнего уровня и расширения.
На данный момент класс Repository
знает, как вычислить форматированное имя пользователя для объекта User
. Но если мы хотим повторно использовать ту же логику форматирования в других классах, нам нужно либо скопировать и вставить ее, либо переместить в класс User
.
Kotlin предоставляет возможность объявлять функции и свойства вне любого класса, объекта или интерфейса. Например, функция mutableListOf()
которую мы использовали для создания нового экземпляра List
, уже определена в Collections.kt
из стандартной библиотеки Kotlin.
В Java всякий раз, когда вам нужны какие-то служебные функции, вы, скорее всего, создадите класс Util
и объявите эту функциональность как статическую функцию. В Котлине вы можете объявлять функции верхнего уровня, не имея класса. Однако Kotlin также предоставляет возможность создавать функции расширения . Это функции, расширяющие определенный тип, но объявленные вне этого типа.
Видимость функций и свойств расширения можно ограничить с помощью модификаторов видимости. Они ограничивают использование только классами, которым нужны расширения, и не загрязняют пространство имен.
Для класса User
мы можем либо добавить функцию расширения, которая вычисляет форматированное имя, либо хранить форматированное имя в свойстве расширения. Его можно добавить вне класса Repository
, в том же файле:
// extension function
fun User.getFormattedName(): String {
return if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName ?: "Unknown"
}
} else {
firstName ?: "Unknown"
}
}
// extension property
val User.userFormattedName: String
get() {
return if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName ?: "Unknown"
}
} else {
firstName ?: "Unknown"
}
}
// usage:
val user = User(...)
val name = user.getFormattedName()
val formattedName = user.userFormattedName
Затем мы можем использовать функции и свойства расширения, как если бы они были частью класса User
.
Поскольку форматированное имя является свойством класса User
, а не функциональностью класса Repository
, давайте воспользуемся свойством расширения. Наш файл Repository
теперь выглядит так:
val User.formattedName: String
get() {
return if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName ?: "Unknown"
}
} else {
firstName ?: "Unknown"
}
}
object Repository {
private val _users = mutableListOf<User>()
val users: List<User>
get() = _users
val formattedUserNames: List<String>
get() {
return _users.map { user -> user.formattedName }
}
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
_users.add(user1)
_users.add(user2)
_users.add(user3)
}
}
Стандартная библиотека Kotlin использует функции расширения для расширения функциональности нескольких API Java; многие функции Iterable
и Collection
реализованы как функции расширения. Например, функция map
, которую мы использовали на предыдущем шаге, является функцией расширения Iterable
.
11. Функции области действия: let, apply, with, run, также
В коде нашего класса Repository
мы добавляем несколько объектов User
в список _users
. Эти вызовы можно сделать более идиоматичными с помощью функций области видимости Kotlin.
Чтобы выполнять код только в контексте определенного объекта, без необходимости доступа к объекту на основе его имени, Kotlin предлагает 5 функций области действия: let
, apply
, with
, run
, а also
. Эти функции делают ваш код более простым для чтения и более кратким. Все функции области видимости имеют получателя ( this
), могут иметь аргумент ( it
) и возвращать значение.
Вот удобная шпаргалка, которая поможет вам запомнить, когда использовать каждую функцию:
Поскольку мы настраиваем наш объект _users
в нашем Repository
, мы можем сделать код более идиоматичным, используя функцию apply
:
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
_users.apply {
// this == _users
add(user1)
add(user2)
add(user3)
}
}
12. Подведение итогов
В этой лаборатории кода мы рассмотрели основы, необходимые для начала преобразования кода из Java в Kotlin. Это преобразование не зависит от вашей платформы разработки и помогает гарантировать, что написанный вами код будет идиоматическим Kotlin.
Идиоматический Kotlin делает написание кода коротким и приятным. Благодаря всем возможностям Kotlin существует множество способов сделать ваш код более безопасным, кратким и читабельным. Например, мы можем даже оптимизировать наш класс Repository
, создав экземпляр списка _users
с пользователями непосредственно в объявлении, избавившись от блока init
:
private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
Мы рассмотрели широкий спектр тем: от обработки значений NULL, одиночных элементов, строк и коллекций до таких тем, как функции расширения, функции верхнего уровня, свойства и функции области видимости. Мы перешли от двух классов Java к двум классам Kotlin, которые теперь выглядят так:
Пользователь.кт
data class User(var firstName: String?, var lastName: String?)
Репозиторий.kt
val User.formattedName: String
get() {
return if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName ?: "Unknown"
}
} else {
firstName ?: "Unknown"
}
}
object Repository {
private val _users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
val users: List<User>
get() = _users
val formattedUserNames: List<String>
get() = _users.map { user -> user.formattedName }
}
Вот краткий обзор функций Java и их сопоставления с Kotlin:
Ява | Котлин |
| объект |
| |
| |
Класс, который просто хранит данные | класс |
Инициализация в конструкторе | Инициализация в блоке |
| поля и функции, объявленные в |
Класс синглтона | |
Чтобы узнать больше о Kotlin и о том, как его использовать на вашей платформе, посетите эти ресурсы:
- Котлин Коанс
- Учебники по Котлину
- Основы Android Kotlin
- Kotlin Bootcamp для программистов
- Kotlin для Java-разработчиков — бесплатный курс в режиме аудита