Из Балтийского моря в Индийский океан

в 11:34, , рубрики: java, kotlin

Рассказ пойдет о том, как началось мое путешествие с острова Котлин на остров Джаву.

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

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

Начать стоит с того, что Kotlin неплохо дружит с Java, видно на что ориентировались создатели данного языка, да и наличие компиляции в байткод JVM тоже радует. Это позволяет использовать различные фреймворки и библиотеки Java, а также, при желании, почти бесшовно скрестить в одном проекте эти два языка, необходимо лишь начинать с компиляции классов Kotlin. В проекте использовались TornadoFX и Exposed, первый – это аналог JavaFX, а второй – фреймворк для работы с базами данных.

Первым ощущением было, да эта та же Джава только в профиль, но с большим количеством синтаксического сахара, что позволяет писать код быстрее и компактнее, но местами гораздо сложнее.

Взглянув в первый раз в код, слегка удивляешься обилию «?», отсутствию геттеров и сеттеров, неизменным и изменяемым переменным val и var, с автоматическим выводом типов или их наличием, конечно если их инициализация отложена на потом. Поэтому разбирая код, часто ловишь себя на мысли, какой же все-таки объект имел в виду коллега, чье наследство мне довелось переписывать. Примитивных типов данных, таких как int, boolean и т.д., в языке не наблюдается, как и любимых с детства «;» и «new».

Начнем с того, что язык считается null-безопасным, вследствие чего, в коде часто попадаются такие конструкции как:

Stand?.channell?.stream?.act() 

Прямо-таки хочется воскликнуть: «Больше вопросов Богу вопросов!».
«?» вызывает легкое недоумение в первое время, понятно, что во главе угла стоит null-безопасность, в связи с чем, каждое свойство объекта приходиться проверять на null, но зачем же тогда оставлять возможность вызова методов без проверок на null, лишь заменив «?» на «!!».
Как пример, предыдущее выражение будет выглядеть как

 Stand!!.channell!!.stream!!.act() 

Компилятор не будет проверять данное выражение. Как не будет проверять и в случае компиляции совместного проекта c Java. Спрашивается, в чем тогда преимущество?!

Проверяемые исключения в языке отсутствуют, и никаких IOException при чтении файла не увидишь. Плохо это или хорошо судить вам.

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

Kotlin Java
private fun checkConnections(silently: Boolean = false): Boolean {
        var isNotFailed = true
        mqToClearTable.items.filter { isMqValid(it) }.forEach { mq -> if (mq.queue.isNotEmpty()) {
                isNotFailed = isNotFailed && checkMqConnection(MQContainer(mq.host, mq.port.toInt(), mq.channel, mq.queue, mq.manager), silently)
            }
        }
        return isNotFailed
    }
private Boolean checkConnections(Boolean tempSilently) {
        boolean silently = ObjectUtils.defaultIfNull(tempSilently, false);
        boolean isNotFailed = true;
        List<QueueToClearObservable> filteredQueue = mqToClearTable.getItems().stream().filter(this::isMqValid).collect(toList());
        for (QueueToClearObservable mq : filteredQueue) {
            if (StringUtils.isNotEmpty(mq.getQueue())) {
                isNotFailed = isNotFailed && checkMqConnection(new MQContainer(mq.getHost(), mq.getPort(), mq.getChannel(), mq.getQueue(), mq.getManager()), silently);
            }
        }
        return isNotFailed;
    }
private fun armsTable() = armsTable.apply {
        columnResizePolicy = TableView.CONSTRAINED_RESIZE_POLICY
        items = currentStand?.arms?.observable()
        
        column("Сервер", ARM::hostProperty).useTextField(DefaultStringConverter())}
private void armsTable() {
        armsTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
        if (currentStand != null) {
            if (CollectionUtils.isNotEmpty(currentStand.getArms())) {
                List<ARM>  items = FXCollections.observableList(currentStand.getArms());
                armsTable.getItems().addAll(arms);
            }
        }

        TableColumn<ARM, String> hostPropertyColumn = new TableColumn<>("Сервер");
        hostPropertyColumn.setCellValueFactory(cell -> cell.getValue().hostPropertyProperty());
        hostPropertyColumn.setCellFactory(TextFieldTableCell.forTableColumn());

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

 factory = new MQQueueConnectionFactory().applay {
		channel =  mq.channel
		targetClientMatching = true
		hostname = mq.host.activ();
 }

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

Но есть, на мой взгляд, и спорные вещи:

  • Функции – расширения, которые позволяют в любом месте кода добавить метод для класса, после чего вызов метода будет доступен из любого участка кода. С одной стороны это удобно, а с другой, глядя в код ловишь себя на мысли, это метод из используемой библиотеки или коллега просто расширил где-то функционал. В Java схожее поведение можно было бы получить написанием метода именно в том классе, где он нужен, передав дополнительный параметр.
  • Инфиксная запись функций. Глядя на запись
     block with container

    Не сразу можно понять, что имеется в виду вызов у объекта block метода with, где container используются в качестве аргумента. Ведь гораздо же следующую запись

      block.with(container)
  • Функции, что принимает функции как параметры. Не то что бы аналоги в Java отсутствовали, в ней для таких целей есть встроенный функциональные интерфейсы(Consumer, Supplier и т.д.), но данные интерфейсы должны работать с каким-либо объектом, а в Kotlin можно передать кусок кода не задумываясь об объекте, воспользовавшись примерно такой конструкцией:
    fun <T> showAndExecute(title: String, init: () -> T): T{
     alert= Alert(title)
    	{…}
    alert.showAndWait()
    if (result.isPresent() && result.get()){
    init()
    }
    

Еще немного о функциональной части Kotlin. Наличие схожих функций run, let, apply, also с различными нюансами применения, что вызывает диссонанс, когда несколько таких функций используется в рамках одной конструкции с использованием ключевого слова it (неявное имя единственного параметра), например:

testInfo.also {
           
 it.endDateProperty.onChange { it?.let { update(Tests.endDate, it) } }
     }

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

Пара комментариев по поводу конструкторов в Kotlin. Мало того, что, как и с методами, в конструкторе можно задать значение полей по умолчанию, так и паттерн Одиночка реализуется на уровне языка, как в прочем, и дата-классы, в отличие от Java, где мы снова и снова прописываем все ручками и по нескольку раз. Впрочем, на мой взгляд, реализация наследования организована лучше именно в последней, где в отличии от Kotlin все классы изначально открыты, и при наследовании от них, мы можем спокойно их переопределять. В Kotlin же у класса или метода должна быть пометка open для его дальнейшего переопределения. Также закрытые классы не получится проксировать. Реализация статических свойств и методов в языке тоже отсутствует, вместо этого предлагается реализация через объекты-компаньоны:

 class ExceptionHandler : Thread.UncaughtExceptionHandler {
 	companion object {
		fun foo{ ….}
		val interval  = 100
	} }	

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

Автор: DmitryMatrov

Источник

Поделиться

* - обязательные к заполнению поля