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

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 запрашивает исправление после преобразования, нажмите «Да» .

e6f96eace5dabe5f.png

Вы должны увидеть следующий код 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 ) и возвращать значение.

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

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, которые теперь выглядят так:

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

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:

Ява

Котлин

final объект

объект val

equals()

==

==

===

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

класс data

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

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

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

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

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

object

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