(Устарело) Преобразование в Kotlin

1. Добро пожаловать!

В этом практическом занятии вы научитесь переводить свой код с Java на Kotlin. Вы также узнаете, что представляют собой языковые соглашения Kotlin и как убедиться, что ваш код им соответствует.

Этот практический урок подойдет любому разработчику, использующему Java и рассматривающему возможность миграции своего проекта на Kotlin. Мы начнем с нескольких Java-классов, которые вы преобразуете в Kotlin с помощью IDE. Затем мы рассмотрим преобразованный код и посмотрим, как его можно улучшить, сделав более идиоматичным и избежав распространенных ошибок.

Что вы узнаете

Вы научитесь переводить код с Java на Kotlin. При этом вы освоите следующие особенности и концепции языка Kotlin:

  • Обработка возможности получения значения null
  • Реализация синглтонов
  • Классы данных
  • Обработка строк
  • Оператор Элвиса
  • Деструктуризация
  • Свойства и базовые свойства
  • Аргументы по умолчанию и именованные параметры
  • Работа с коллекциями
  • Дополнительные функции
  • Функции и параметры верхнего уровня
  • let , apply , with и run ключевые слова

Предположения

Вы уже должны быть знакомы с Java.

Что вам понадобится

2. Настройка

Создать новый проект

Если вы используете IntelliJ IDEA, создайте новый Java-проект с Kotlin/JVM.

Если вы используете Android Studio, создайте новый проект с шаблоном «Без активности» . Выберите Kotlin в качестве языка проекта. Минимальный размер SDK может быть любым, это не повлияет на результат.

Код

Мы создадим объект модели User и класс-синглтон Repository , который будет работать с объектами User и предоставлять списки пользователей и отформатированные имена пользователей.

Создайте новый файл с именем User.java в папке app/java/ <yourpackagename> и вставьте в него следующий код:

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. Объявление возможности присвоения значения null, классов val, var и data.

Наша IDE неплохо справляется с автоматическим преобразованием кода Java в код Kotlin, но иногда ей нужна небольшая помощь. Давайте позволим нашей IDE выполнить первоначальное преобразование. Затем мы проанализируем полученный код, чтобы понять, как и почему он был преобразован именно таким образом.

Перейдите к файлу User.java и преобразуйте его в Kotlin: Панель меню -> Код -> Преобразовать файл Java в файл Kotlin .

Если после преобразования ваша IDE запросит исправление, нажмите «Да» .

e6f96eace5dabe5f.png

Вы должны увидеть следующий код на Kotlin:

class User(var firstName: String?, var lastName: String?)

Обратите внимание, что User.java был переименован в User.kt Файлы Kotlin имеют расширение .kt.

В нашем Java-классе User было два свойства: firstName и lastName . Каждое имело методы getter и setter, что делало его значение изменяемым. Ключевое слово Kotlin для изменяемых переменных — var , поэтому конвертер использует var для каждого из этих свойств. Если бы наши Java-свойства имели только методы getter, они были бы только для чтения и были бы объявлены как переменные val . val аналогичен ключевому слову final в Java.

Одно из ключевых отличий Kotlin от Java заключается в том, что Kotlin явно указывает, может ли переменная принимать значение null. Это делается путем добавления знака " ? к объявлению типа.

Поскольку мы пометили firstName и lastName как допускающие значение null, автоконвертер автоматически пометил эти свойства как допускающие значение null с помощью String? Если вы аннотируете ваши Java-члены как не допускающие значение null (используя org.jetbrains.annotations.NotNull или androidx.annotation.NonNull ), конвертер распознает это и сделает поля не допускающими значение null и в Kotlin.

Базовое преобразование уже выполнено. Но мы можем написать это более идиоматичным способом. Давайте посмотрим, как.

Класс данных

Наш класс User содержит только данные. В Kotlin есть ключевое слово для классов с такой ролью: data . Пометив этот класс как класс data , компилятор автоматически создаст для нас геттеры и сеттеры. Он также унаследует функции 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")

Равенство

В Kotlin существует два типа равенства:

  • Для проверки структурного равенства используется оператор == , а для определения равенства двух экземпляров вызывается функция 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 . Для этого мы просто присваиваем null переменной lastName .

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. Хотя список действительно может быть null, давайте предположим, что он не может содержать пользователей со значением 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 мы видим, что свойство users было инициализировано в объявлении.

private var users: MutableList<User>? = null

static свойства и методы Kotlin

В Java мы используем ключевое слово static для полей или функций, чтобы указать, что они принадлежат классу, но не экземпляру этого класса. Именно поэтому мы создали статическое поле ` INSTANCE в нашем классе Repository . Эквивалентом этого в Kotlin является блок ` companion object . Здесь также объявляются статические поля и статические функции. Конвертер создал блок `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. Обработка возможности присвоения значения null.

При преобразовании класса Repository в Kotlin автоматический конвертер сделал список пользователей допускающим значение null, поскольку он не был инициализирован объектом при объявлении. В результате для всех случаев использования объекта users необходимо использовать оператор проверки на ненулевое значение !! (Вы увидите users!! и user!! по всему преобразованному коду.) Оператор !! преобразует любую переменную в ненулевой тип, поэтому вы можете обращаться к ее свойствам или вызывать функции. Однако, если значение переменной действительно равно null, будет выброшено исключение. Использование !! увеличивает риск возникновения исключений во время выполнения.

Вместо этого предпочтительнее обрабатывать возможность получения значения NULL, используя один из следующих методов:

  • Проверка на 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 будет содержать ссылку только для чтения на список пользователей. Обратите внимание, что ссылка является только для чтения, поэтому она никогда не сможет указывать на новый список, но сам список по-прежнему изменяем (вы можете добавлять или удалять элементы).

Поскольку переменная 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 теперь не равно null, и мы можем удалить все ненужные операторы !! . Обратите внимание, что вы по-прежнему будете видеть ошибки компиляции в 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 , нам необходимо обработать возможность значения null при формировании списка отформатированных имен пользователей. Мы хотим отображать "Unknown" , если какое-либо из имен отсутствует. Поскольку переменная name не будет изменяться после однократного присвоения значения, мы можем использовать val вместо var . Внесите это изменение в первую очередь.

val name: String

Взгляните на код, который присваивает значение переменной name. Возможно, вам покажется новым увидеть, как переменной присваивается значение в блоке кода if / else . Это допустимо, потому что в Kotlin 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"
}

Оператор Элвиса

Этот код можно написать более идиоматично, используя оператор Элвиса ?: . Оператор Элвиса вернет выражение в левой части, если оно не равно null, или выражение в правой части, если левая часть равна null.

Таким образом, в следующем коде возвращается firstName , если оно не равно null. Если firstName равно null, выражение возвращает значение справа, "Unknown" :

name = if (lastName != null) {
    ...
} else {
    firstName ?: "Unknown"
}

7. Строковые шаблоны

Kotlin упрощает работу со 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" если отсутствуют имя и фамилия, поэтому мы не поддерживаем объекты со значением null. Таким образом, для типа данных 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 вы можете объявлять функции верхнего уровня, не создавая класс. Однако 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 использует функции расширения для расширения функциональности ряда Java API; многие функции объектов Iterable и Collection реализованы как функции расширения. Например, функция map , которую мы использовали на предыдущем шаге, является функцией расширения для объекта Iterable .

11. Функции области видимости: let, apply, with, run, also

В коде нашего класса Repository мы добавляем несколько объектов User в список _users . Эти вызовы можно сделать более идиоматичными с помощью функций области видимости Kotlin.

Для выполнения кода только в контексте конкретного объекта, без необходимости доступа к объекту по его имени, Kotlin предлагает 5 функций области видимости: let , apply , with , run и also . Эти функции делают ваш код более читабельным и лаконичным. Все функции области видимости имеют получателя ( this ), могут иметь аргумент ( it ) и могут возвращать значение.

Вот удобная шпаргалка, которая поможет вам запомнить, когда использовать каждую функцию:

6b9283d411fb6e7b.png

Поскольку мы настраиваем объект _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-классам, которые теперь выглядят так:

Пользователь.kt

data class User(var firstName: String?, var lastName: String?)

Repository.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:

Java

Котлин

final объект

объект val

equals()

==

==

===

Класс, который просто хранит данные.

класс data

Инициализация в конструкторе

Инициализация в блоке init

static поля и функции

поля и функции, объявленные в companion object

Класс Синглтон

object

Чтобы узнать больше о Kotlin и о том, как использовать его на вашей платформе, ознакомьтесь с этими ресурсами: