Рефакторинг на Котлин

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

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

Чему вы научитесь

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

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

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

Вы, вероятно, уже знакомы с Java.

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

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

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

Если вы используете Android Studio, создайте новый проект без Activity.

Код

Мы создадим объект модели 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;
    }

}

В зависимости от типа вашего проекта импортируйте androidx.annotation.Nullable , если вы используете проект Android, или org.jetbrains.annotations.Nullable в противном случае.

Создайте новый файл с именем 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;
    }
}

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

Перейдите к файлу 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 явно указывает, может ли переменная принимать значение NULL. Это достигается добавлением символа ` ? ` к объявлению типа.

Поскольку мы отметили firstName и lastName как допускающие значение NULL, автоконвертер автоматически отметил свойства как допускающие значение NULL с помощью String? Если вы аннотируете свои члены Java как ненулевые (с помощью org.jetbrains.annotations.NotNull или androidx.annotation.NonNull ), конвертер распознает это и сделает поля ненулевыми и в 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

В Kotlin мы можем присваивать аргументам при вызовах функций значения по умолчанию. Значение по умолчанию используется, если аргумент опущен. В Kotlin конструкторы также являются функциями, поэтому мы можем использовать аргументы по умолчанию, чтобы указать, что значение по умолчанию для lastNamenull . Для этого мы просто присваиваем 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("John", "Doe")

Параметры функций могут быть названы при вызове функций:

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")

Прежде чем продолжить, убедитесь, что ваш класс 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)
   }
}

Давайте посмотрим, что сделал автоматический конвертер:

  • Добавлен блок init (Repository.kt#L50)
  • static поле теперь является частью блока companion object (Repository.kt#L33)
  • Список users может иметь значение NULL, поскольку объект не был создан во время объявления (Repository.kt#L7)
  • Метод getFormattedUserNames() теперь является свойством formattedUserNames (Repository.kt#L11)
  • Итерация по списку пользователей (которая изначально была частью getFormattedUserNames( )) имеет другой синтаксис, чем в Java (Repository.kt#L15)

Прежде чем продолжить, давайте немного приведём код в порядок. Видно, что конвертер превратил наш список users в изменяемый список, содержащий объекты, допускающие значение NULL. Хотя список действительно может быть пустым, допустим, что он не может содержать пустых пользователей. Итак, сделаем следующее:

  • Удалить ? в User? в объявлении типа users
  • getUsers должен возвращать List<User>?

Автоконвертер также без необходимости разбил на две строки объявления пользовательских переменных и переменных, определённых в блоке init. Давайте разместим каждое объявление переменной на одной строке. Вот как должен выглядеть наш код:

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)
    }
}

Блок инициализации

В 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 . Здесь также можно объявить статические поля и статические функции. Конвертер создал и переместил сюда поле 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 users = Repository.users

Деструктуризация

Kotlin позволяет деструктурировать объект, разбив его на несколько переменных, используя синтаксис, называемый объявлением деструктуризации . Мы создаём несколько переменных и можем использовать их независимо.

Например, классы данных поддерживают деструктуризацию, поэтому мы можем деструктурировать объект User в цикле for , преобразуя его в (firstName, lastName) . Это позволяет нам работать напрямую со значениями firstName и lastName . Обновим цикл for следующим образом:

 
for ((firstName, lastName) in users!!) {
       val name: String?

       if (lastName != null) {
          if (firstName != null) {
                name = "$firstName $lastName"
          } else {
                name = lastName
          }
       } else if (firstName != null) {
            name = firstName
       } else {
            name = "Unknown"
       }
       userNames.add(name)
}

При преобразовании класса Repository в Kotlin автоматический конвертер сделал список пользователей допускающим значение NULL, поскольку он не был инициализирован объектом при объявлении. Во всех случаях использования объекта users используется оператор утверждения not-null !! . Он преобразует любую переменную в ненулевой тип и выдаёт исключение, если значение равно NULL. Использование !! создаёт риск возникновения исключений во время выполнения.

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

  • Выполнение проверки на null ( if (users != null) {...} )
  • Использование оператора Элвиса ?: (будет рассмотрено далее в кодовой лаборатории)
  • Использование некоторых стандартных функций Kotlin (рассмотренных далее в кодовой лаборатории)

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

При создании экземпляров типов-коллекций Kotlin предоставляет несколько вспомогательных функций, которые делают код более читабельным и гибким. Здесь мы используем MutableList для users :

private var users: MutableList<User>? = null

Для простоты мы можем использовать функцию mutableListOf() , указать тип элемента списка, удалить вызов конструктора ArrayList из блока init и удалить явное объявление типа свойства users .

private val users = mutableListOf<User>()

Мы также изменили var на val, поскольку users будет содержать неизменяемую ссылку на список пользователей. Обратите внимание, что ссылка неизменяема, но сам список изменяем (вы можете добавлять и удалять элементы).

Благодаря этим изменениям наше свойство users теперь не равно NULL, и мы можем удалить все ненужные вхождения оператора !! .

val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
    ...
}

Кроме того, поскольку переменная users уже инициализирована, нам придется удалить инициализацию из блока 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)
}

Поскольку и lastName , и firstName могут иметь null , нам необходимо учитывать возможность значения NULL при построении списка отформатированных имён пользователей. Поскольку мы хотим, чтобы при отсутствии любого из имён отображалось "Unknown" , мы можем сделать имя ненулевым, удалив вопросительный знак ? из объявления типа.

val name: String

Если lastName равно null, name будет либо firstName , либо "Unknown" :

if (lastName != null) {
    if (firstName != null) {
        name = "$firstName $lastName"
    } else {
        name = lastName
    }
} else if (firstName != null) {
    name = firstName
} else {
    name = "Unknown"
}

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

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

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

Kotlin упрощает работу со String с помощью шаблонов строк . Шаблоны строк позволяют ссылаться на переменные внутри объявлений строк.

Автоматический конвертер обновил конкатенацию имени и фамилии, чтобы ссылаться на имя переменной непосредственно в строке с помощью символа $ и поместил выражение между { } .

// Java
name = user.getFirstName() + " " + user.getLastName();

// Kotlin
name = "${user.firstName} ${user.lastName}"

В коде замените конкатенацию строк на:

name = "$firstName $lastName"

В Kotlin if , when , for и while — это выражения, которые возвращают значение. Ваша IDE даже выводит предупреждение о том, что присваивание следует вынести из if :

Давайте последуем совету IDE и вынесем присваивание для обоих операторов if . Последняя строка оператора if будет назначена. Таким образом, становится яснее, что единственная цель этого блока — инициализация значения name:

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

Далее мы получим предупреждение о том, что объявление name можно объединить с присваиванием. Применим и это. Поскольку тип переменной имени можно вывести, мы можем удалить явное объявление типа. Теперь наш formattedUserNames выглядит так:

val formattedUserNames: List<String?>
   get() {
       val userNames: MutableList<String?> = ArrayList(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
   }

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

Чтобы еще больше упростить это, мы можем полностью удалить переменную 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"
                }
            }
        }

Мы увидели, что автоматический конвертер заменил функцию getFormattedUserNames() свойством formattedUserNames , имеющим собственный геттер. В действительности Kotlin по-прежнему генерирует метод getFormattedUserNames() , возвращающий List .

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

Логика, которая запускалась в функции Java getFormattedUserNames() теперь запускается при вызове геттера свойства formattedUserNames Kotlin.

Хотя у нас нет явного поля, соответствующего свойству formattedUserNames , Kotlin предоставляет нам автоматическое резервное поле с именем field к которым мы можем получить доступ при необходимости из пользовательских геттеров и сеттеров.

Однако иногда нам нужны дополнительные функции, которые автоматическое поле резервирования не предоставляет. Рассмотрим пример ниже.

Внутри нашего класса Repository имеется изменяемый список пользователей, который предоставляется в функции getUsers() , сгенерированной из нашего кода Java:

fun getUsers(): List<User>? {
    return users
}

Проблема здесь в том, что, возвращая users , любой потребитель класса Repository может изменить наш список пользователей — плохая идея! Давайте исправим это с помощью резервного свойства.

Для начала переименуем users в _users . Теперь добавим публичное неизменяемое свойство, возвращающее список пользователей. Назовём его users :

private val _users = mutableListOf<User>()
val users: List<User>
      get() = _users

Благодаря этому изменению приватное свойство _users становится резервным свойством для публичного свойства users . За пределами класса Repository список _users не подлежит изменению, поскольку пользователи класса могут получить к нему доступ только через users .

Полный код:

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)
    }
}

Сейчас класс Repository умеет вычислять отформатированное имя пользователя для объекта User . Но если мы хотим использовать ту же логику форматирования в других классах, нам нужно либо скопировать и вставить её, либо перенести в класс User .

Kotlin предоставляет возможность объявлять функции и свойства вне любого класса, объекта или интерфейса. Например, функция mutableListOf() которую мы использовали для создания нового экземпляра List , определена непосредственно в Collections.kt из Стандартной библиотеки.

В 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 .

Поскольку отформатированное имя является свойством пользователя, а не функциональностью класса 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 .

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

Для выполнения кода только в контексте конкретного объекта, без необходимости доступа к объекту по его имени, в Kotlin было создано пять функций области видимости: 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)
    }
 }

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

Идиоматический Kotlin делает написание кода коротким и приятным. Благодаря всем возможностям Kotlin существует множество способов сделать код более безопасным, лаконичным и читабельным. Например, мы можем даже оптимизировать класс Repository , создав экземпляр списка _users с пользователями непосредственно в объявлении, избавившись от блока init :

private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))

Мы рассмотрели широкий спектр тем: от обработки допустимости значений NULL, синглтонов, строк и коллекций до таких тем, как функции расширения, функции верхнего уровня, свойства и функции области видимости. Мы перешли от двух классов Java к двум классам Kotlin, которые теперь выглядят так:

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

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:

Ява

Котлин

final объект

объект val

equals()

==

==

===

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

класс data

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

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

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

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

Класс Singleton

object

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

,

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

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

Чему вы научитесь

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

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

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

Вы, вероятно, уже знакомы с Java.

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

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

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

Если вы используете Android Studio, создайте новый проект без Activity.

Код

Мы создадим объект модели 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;
    }

}

В зависимости от типа вашего проекта импортируйте androidx.annotation.Nullable , если вы используете проект Android, или org.jetbrains.annotations.Nullable в противном случае.

Создайте новый файл с именем 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;
    }
}

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

Перейдите к файлу 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 явно указывает, может ли переменная принимать значение NULL. Это достигается добавлением символа ` ? ` к объявлению типа.

Поскольку мы отметили firstName и lastName как допускающие значение NULL, автоконвертер автоматически отметил свойства как допускающие значение NULL с помощью String? Если вы аннотируете свои члены Java как ненулевые (с помощью org.jetbrains.annotations.NotNull или androidx.annotation.NonNull ), конвертер распознает это и сделает поля ненулевыми и в 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

В Kotlin мы можем присваивать аргументам при вызовах функций значения по умолчанию. Значение по умолчанию используется, если аргумент опущен. В Kotlin конструкторы также являются функциями, поэтому мы можем использовать аргументы по умолчанию, чтобы указать, что значение по умолчанию для lastNamenull . Для этого мы просто присваиваем 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("John", "Doe")

Параметры функций могут быть названы при вызове функций:

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")

Прежде чем продолжить, убедитесь, что ваш класс 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)
   }
}

Давайте посмотрим, что сделал автоматический конвертер:

  • Добавлен блок init (Repository.kt#L50)
  • static поле теперь является частью блока companion object (Repository.kt#L33)
  • Список users может иметь значение NULL, поскольку объект не был создан во время объявления (Repository.kt#L7)
  • Метод getFormattedUserNames() теперь является свойством formattedUserNames (Repository.kt#L11)
  • Итерация по списку пользователей (которая изначально была частью getFormattedUserNames( )) имеет другой синтаксис, чем в Java (Repository.kt#L15)

Прежде чем продолжить, давайте немного приведём код в порядок. Видно, что конвертер превратил наш список users в изменяемый список, содержащий объекты, допускающие значение NULL. Хотя список действительно может быть пустым, допустим, что он не может содержать пустых пользователей. Итак, сделаем следующее:

  • Удалить ? в User? в объявлении типа users
  • getUsers должен возвращать List<User>?

Автоконвертер также без необходимости разбил на две строки объявления пользовательских переменных и переменных, определённых в блоке init. Давайте разместим каждое объявление переменной на одной строке. Вот как должен выглядеть наш код:

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)
    }
}

Блок инициализации

В 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 . Здесь также можно объявить статические поля и статические функции. Конвертер создал и переместил сюда поле 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 users = Repository.users

Деструктуризация

Kotlin позволяет деструктурировать объект, разбив его на несколько переменных, используя синтаксис, называемый объявлением деструктуризации . Мы создаём несколько переменных и можем использовать их независимо.

Например, классы данных поддерживают деструктуризацию, поэтому мы можем деструктурировать объект User в цикле for , преобразуя его в (firstName, lastName) . Это позволяет нам работать напрямую со значениями firstName и lastName . Обновим цикл for следующим образом:

 
for ((firstName, lastName) in users!!) {
       val name: String?

       if (lastName != null) {
          if (firstName != null) {
                name = "$firstName $lastName"
          } else {
                name = lastName
          }
       } else if (firstName != null) {
            name = firstName
       } else {
            name = "Unknown"
       }
       userNames.add(name)
}

При преобразовании класса Repository в Kotlin автоматический конвертер сделал список пользователей допускающим значение NULL, поскольку он не был инициализирован объектом при объявлении. Во всех случаях использования объекта users используется оператор утверждения not-null !! . Он преобразует любую переменную в ненулевой тип и выдаёт исключение, если значение равно NULL. Использование !! создаёт риск возникновения исключений во время выполнения.

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

  • Выполнение проверки на null ( if (users != null) {...} )
  • Использование оператора Elvis ?:
  • Используя некоторые из стандартных функций Kotlin (охватываемые позже в CodeLab)

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

При создании экземпляров типов сбора Kotlin предоставляет несколько вспомогательных функций, чтобы сделать ваш код более читаемым и гибким. Здесь мы используем MutableList для users :

private var users: MutableList<User>? = null

Для простоты мы можем использовать функцию mutableListOf() , предоставить тип элемента списка, удалить вызов конструктора ArrayList из блока init и удалить явное объявление о свойстве users .

private val users = mutableListOf<User>()

Мы также изменили VAR в VAL, потому что пользователи будут содержать неизменную ссылку на список пользователей. Обратите внимание, что ссылка неизменна, но сам список изменен (вы можете добавить или удалить элементы).

С этими изменениями наша собственность users теперь не ноль, и мы можем удалить все ненужные !! Вопросы оператора.

val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
    ...
}

Кроме того, поскольку переменная пользователей уже инициализирована, мы должны удалить инициализацию из блока 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)
}

Поскольку как lastName , так и firstName могут быть null , нам нужно обрабатывать нуль, когда мы создаем список форматированных имен пользователей. Поскольку мы хотим отобразить "Unknown" , если любое имя отсутствует, мы можем сделать имя не нулевым, удалив ? от типа объявления.

val name: String

Если lastName имя новое, name является либо firstName , либо "Unknown" :

if (lastName != null) {
    if (firstName != null) {
        name = "$firstName $lastName"
    } else {
        name = lastName
    }
} else if (firstName != null) {
    name = firstName
} else {
    name = "Unknown"
}

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

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

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

Котлин облегчает работу со String с шаблонами строк . Шаблоны строки позволяют ссылаться на переменные внутри строковых объявлений.

Автоматический преобразователь обновил конкатенацию первого и фамилии, чтобы ссылаться на имя переменной непосредственно в строке, используя символ $ и поместите выражение между { } .

// Java
name = user.getFirstName() + " " + user.getLastName();

// Kotlin
name = "${user.firstName} ${user.lastName}"

В коде замените конкатенацию строки:

name = "$firstName $lastName"

В Котлине, if , when , for и while выражения - они возвращают значение. Ваша IDE даже показывает предупреждение о том, что задание должно быть снято из if :

Давайте следим за предложением IDE и поднимете задание для обоих операторов if . Последняя строка оператора IF будет назначена. Как это, ясно, что единственная цель этого блока - инициализировать значение имени:

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

Далее мы получим предупреждение о том, что объявление name может быть связано с заданием. Давайте применим это тоже. Поскольку тип переменной имени может быть выведен, мы можем удалить явное объявление типа. Теперь наши formattedUserNames выглядит так:

val formattedUserNames: List<String?>
   get() {
       val userNames: MutableList<String?> = ArrayList(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
   }

Давайте внимательно посмотрим на Getter 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 API с коллекциями Java. Одним из них является функция map . Эта функция возвращает новый список, содержащий результаты применения заданной функции преобразования к каждому элементу в исходном списке. Таким образом, вместо того, чтобы создавать новый список и итерацию через список пользователей вручную, мы можем использовать функцию map и переместить логику it которую мы имели в цикле for корпуса map . По умолчанию имя текущего элемента списка, используемого на map , но для чтения вы можете заменить 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
            }
        }

Чтобы упростить это еще больше, мы можем полностью удалить переменную 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"
                }
            }
        }

Мы увидели, что автоматический преобразователь заменил функцию getFormattedUserNames() на свойство, называемое formattedUserNames , которая имеет пользовательский Getter. Под капотом Kotlin по -прежнему генерирует метод getFormattedUserNames() , который возвращает List .

В Java мы разоблачили бы наши свойства класса с помощью функций Getter и Setter. Котлин позволяет нам иметь лучшую дифференциацию между свойствами класса, выраженные с областями и функциональными возможностями, действиями, которые может совершать класс, выраженные с функциями. В нашем случае класс Repository очень прост и не выполняет никаких действий, поэтому в нем есть только поля.

Логика, которая была вызвана в функции Java getFormattedUserNames() теперь запускается при вызове Getter of formattedUserNames Kotlin.

Несмотря на то, что у нас явно не есть поле, соответствующее свойству formattedUserNames , Kotlin предоставляет нам поле для автоматической поддержки с именем с именем field к которому мы можем получить доступ, если это необходимо, от пользовательских сеттеров и сеттеров.

Иногда, однако, нам нужна дополнительная функциональность, которую не предоставляет поле автоматического поддержки. Давайте проведем пример ниже.

В нашем классе Repository у нас есть измененный список пользователей, который выявляется в функции getUsers() , которая была сгенерирована из нашего кода Java:

fun getUsers(): List<User>? {
    return users
}

Проблема здесь заключается в том, что, возвращая users , любой потребитель класса репозитория может изменить наш список пользователей - не хорошая идея! Давайте исправим это, используя свойство поддержки.

Во -первых, давайте переименовать users в _users . Теперь добавьте публичное неизменное свойство, которое возвращает список пользователей. Назовем это users :

private val _users = mutableListOf<User>()
val users: List<User>
      get() = _users

С этим изменением частное свойство _users становится поддерживающей собственностью общественных users . За пределами класса Repository список _users не модифицируется, поскольку потребители класса могут получить доступ только к списку только через users .

Полный код:

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)
    }
}

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

Котлин предоставляет возможность объявлять функции и свойства вне любого класса, объекта или интерфейса. Например, функция mutableListOf() которую мы использовали для создания нового экземпляра List , определяется непосредственно в Collections.kt из стандартной библиотеки.

В 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 класса.

Поскольку форматированное имя является свойством пользователя, а не функциональностью класса 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 .

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

Чтобы выполнить код только в контексте конкретного объекта, без необходимости доступа к объекту на основе его имени, 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)
    }
 }

В этом коделабе мы рассмотрели основы, которые вам необходимы, чтобы начать рефакторирование вашего кода из Java в Котлин. Этот рефакторинг не зависит от вашей платформы разработки и помогает гарантировать, что код, который вы пишете, является идиоматическим.

Идиоматический котлин делает написание кода коротким и сладким. Со всеми функциями, которые предоставляет Kotlin, есть так много способов сделать ваш код более безопасным, более кратким и более читаемым. Например, мы можем даже оптимизировать наш класс Repository , создав список _users с пользователями непосредственно в объявлении, избавляясь от блока init :

private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))

Мы рассмотрели большой массив тем, от обработки нуля, синглетов, строк и коллекций до таких тем, как функции расширения, функции верхнего уровня, свойства и функции масштаба. Мы прошли от двух классов Java до двух котлин, которые теперь выглядят так:

User.kt

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 }
}

Вот TL; DR из функциональных возможностей Java и их отображение с Котлином:

Ява

Котлин

final объект

val объект

equals()

==

==

===

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

класс data

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

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

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

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

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

object

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