Утилитные классы в Kotlin с точки зрения Java-разработчика

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

Одним из достоинств Kotlin, отличающих его от других языков, в том числе Java, является подход к реализации утилитных классов. Утилиты могут быть полезными инструментами в разработке программного обеспечения, хотя и не всегда являются лучшим выбором при проектировании объектно-ориентированных систем.

Как Java-разработчику, вам будет полезно рассмотреть различные способы реализации утилитных классов в Kotlin и узнать, что делается в этом языке “под капотом”. Kotlin предлагает целый ряд различных способов, упрощающих переход от Java, начиная от сокращения стереотипного кода и заканчивая улучшением читаемости кода.

Понимание различий в реализации утилитных классов в Kotlin и Java — важный шаг в освоении такого мощного языка, как Kotlin. Будь вы опытный Java-разработчик, желающий расширить спектр своих навыков, или новичок в программировании, уделите внимание этому аспекту.

Как вам известно, создать утилитный класс в Java довольно просто. Процесс включает следующие шаги.

  • Создание терминального класса с ключевым словом final (потому что не предполагается определение подкласса)
  • Добавление закрытого конструктора, чтобы избежать ненужного инстанцирования класса
  • Добавление статических утилитных методов (в данном случае foo).
public final class UtilClass {
private UtilClass(){}

public static String foo() {
return "bar";
}
}

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

3 способа реализации утилитных классов в Kotlin

Объявление объектов

Шаблон “одиночка”, полезный в ряде скриптов, реализуется в Kotlin удобным способом.

object UtilObject {
fun foo(): String = "bar"
}

Этот блок кода создаст (лениво инициализированный) объект-одиночку, который доступен из любого места в коде без необходимости дополнительного инстанциирования. 

Перед рассмотрением Java-эквивалента этого кода стоит упомянуть интероперабельность Kotlin и Java.

Когда вы пишете код Kotlin, “под капотом” он компилируется в байт-код Java, который может выполняться на виртуальной Java-машине (JVM) так же, как и код Java. Это означает, что Kotlin можно использовать для проектирования приложений, которые работают на любой платформе, поддерживающей JVM, включая настольные, веб- и мобильные платформы.

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

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

Посмотрим, как компилятор Kotlin сгенерировал Java-эквивалент кода:

public final class UtilObject {
@NotNull
public static final UtilObject INSTANCE;

@NotNull
public final String foo() {
return "bar";
}

private UtilObject() {
}

static {
UtilObject var0 = new UtilObject();
INSTANCE = var0;
}
}

Java-класс создается с использованием переменной экземпляра и закрытого конструктора. Благодаря этому класс инстанцируется только один раз и становится доступным в любом месте в коде.

Применение будет выглядеть следующим образом:

UtilObject.foo()

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

Объекты-компаньоны

Объекты-компаньоны (companion objects)  —  это также объекты-одиночки, у которых есть переменные и функции, связанные с классом, но не с конкретными его экземплярами.

Вместо экземпляра класса создается другой класс, получающий имя Companion (или любое другое), и в него помещаются все свойства и функции.

class UtilClass {
companion object{
fun foo(): String = "bar"
}
}

Посмотрим на Java-эквивалент кода, сгенерированный компилятором Kotlin:

public final class UtilClass {
@NotNull
public static final Companion Companion = new Companion(null);

public static final class Companion {
@NotNull
public final String foo() {
return "bar";
}

private Companion() {
}

public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}

Как видим, создаются два отдельных Java-класса. Первый из них хранит экземпляр объекта-компаньона в статической переменной.

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

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

UtilClass.foo()

Функции внутри пакета

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

Utils.kt:

package com.yourdomain.utils

fun foo(): String = "bar"

Посмотрим Java-эквивалент кода, созданный компилятором Kotlin:

public final class UtilPackageKt {
@NotNull
public static final String foo() {
return "bar";
}
}

Итак, сгенерирован терминальный Java-класс, а утилитная функция foo стала статической функцией в отличие от других вариантов.

Согласно документации Kotlin, функции верхнего уровня рекомендуются как решение для большинства случаев, когда необходимы статические функции.

Использование будет выглядеть следующим образом:

foo()

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

Из-за отсутствия пространства имен может показаться, будто вы используете функции-призраки, источник которых неизвестен, а также ухудшиться читаемость кода.

Заключение

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

Итак, вы ознакомились с различными возможностями языка Kotlin и узнали, как Java обрабатывает их “под капотом”. Теперь у вас будет более четкое представление о сходствах и различиях вариативных реализаций утилитных функций в Kotlin.

Выделим главное:

  • Если нужно создать потокобезопасные, лениво инициализируемые объекты-одиночки, может подойти объявление объектов.
  • Если нужно создать быстро инициализируемое, “Java static-подобное” одиночное расширение класса, могут подойти объекты-компаньоны.
  • Если нужно создать “реальные Java static” функции (без экземпляра) без необходимости использования пространства имен, могут подойти функции внутри пакета.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Baturay Ucer: Utility classes in Kotlin: A Java Developer’s Perspective

Предыдущая статьяCначала Vue, потом React: совет начинающим разработчикам
Следующая статьяЛегко и быстро: автоматизация развертывания AWS EC2 с GitHub Actions и Docker Hub