. Java vs. Kotlin для Android. День 2: А может ну его, этот Kotlin?
Java vs. Kotlin для Android. День 2: А может ну его, этот Kotlin?

Java vs. Kotlin для Android. День 2: А может ну его, этот Kotlin?

Необходимо сделать лирическое отступление и констатировать факт, что со времени написания оригинальной серии статей по Kotlin и публикацией их на данном ресурсе, прошло уже достаточное время. Время достаточное для того чтобы: а) вышла обнова на Kotlin 1.1; б) наш любимый Гугль решил-таки начать нативно поддерживать Java 8 под Android. Более того, второе событие произошло непосредственно в день выхода первой части этой серии статей — вот прям как звезды сошлись на небе.

Не знаю, какое из событий меня больше порадовало, но в любом случае пришлось кой-чего переписывать. к примеру, вот эти самые строки. Безусловно, я рад за платформу и благодарен Гуглу за такой ответственный шаг, НО Kotlin стал еще вкуснее и приятнее взору, пальцам и мозгам. Для тех, кого цепанула вторая новость, просим милости сюда изучать, чем нам это грозит. А для тех, кто, так же как и я, пропустил событие марта в Котляндии, рекомендую не дочитывать эту фразу, а сразу пропустить следующий абзац.

Вы все еще здесь? А может ну его, этот Kotlin, и на боковую, а?;) Ах, да, чуть не забыл — дабы сократить поток негодования на тему «ах, да как ты/вы мог(ли) вот это не упомянуть или вот это?!», сразу ответственно заявляю, что данный артефакт не является мануалом к языку ни в коем случае. Я всего лишь описываю то, что МНЕ понравилось больше всего, когда я пропустил через себя Kotlin. То, что вот я бы непосредственно практически каждый день с радостью использовал в трудовых буднях.

Так-с. на чем я там остановился в прошлый раз? Ах, да, по диагонали изучил классы и хотел посмотреть аспекты of Functional Programming. Но я думаю, что правильнее все-таки будет сначала на базовый синтаксис взглянуть и узнать, что там есть вкусного. Есть у меня предчувствие, что вечерночь не пройдет зря. Поехали.

Null Safety

Начнем, пожалуй, с такой штуки как nullability. В отличие от Java переменная в Kotlin не может содержать null, если компилятор об этом не знает. Т.е. объявляя переменную, нужно явным образом указать может она быть nullable или нет. Такое простое решение — на уровне системы типов заставить разработчика заранее подумать о том, как должна вести себя переменная, какие значения она может принимать. Все сделано для того, чтобы максимально исключить возможность появления NPE (Null Pointer Exception. хотя кому это я объясняю. ) в коде, и особенно в рантайме. Но все-таки NPE могут возникнуть в таких случаях:

  • явный вызов throw NullPointerException()
  • ошибка во внешнем Java-коде
  • использование специфичного оператора !! Как же это все работает? Возьмем к примеру следующий код:

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

Теперь все сработает. Кстати, знание того, что в переменной НИКОГДА не будет null избавляет нас от дурацких проверок каждый раз, когда, к примеру, нам нужно получить длину строки. Можно смело делать вот так:

и нам за это ничего не будет. А что же делать в случае, когда у нас может быть null в переменной? Просто использовать safe-call operator — ?.

И в данном случае в переменной length будет сохранена либо длина строки, либо null, в зависимости от состояния strNull. И тип переменной будет автоматически приведен к Int?. Если же мы не хотим возвращать null, то можно использовать Elvis operator:

Можно построить целую цепочку таких safe-calls, к примеру:

Если же нам нужно выполнить блок операций для случая, когда переменная точно не равна null, то можно использовать функцию let:

Отличнейшая альтернатива заезженным блокам if/else. Настоятельно рекомендуется к использованию.

Как по мне, то очень интересное и элегантное решение проблемы с NPE в рантайме. По факту большинство NPEs вылазит именно из-за того, что мы забываем проверить на null или думаем, что вот в этом самом месте нам никогда не придет null или даже оставляем это «на потом» в спешке. Хотел было засчитать еще одно очко в пользу Kotlin, но решил, что данное решение заслуживает двух.;)

Smart casts & type checks

Как бы так поделикатнее описать свои мысли о реализации of smart casts & type checks in Kotlin и при этом не сильно обидеть Java? Одним словом — НАКОНЕЦ-ТО. Забудьте об этих дурацких дублирующих друг друга операциях с приведением типов как в примере ниже:

Каждый раз, когда я пишу подобный код, мне хочется плакать. Ну почему компилятор не может догадаться, что если я проверяю переменную на принадлежность к какому-то типу, то при выполнении этого условия я хочу использовать именно этот интерфейс?! Ведь из-за этих ограничений Java-компилятора рождаются кошмарные конструкции и блоки кода, на который без слез смотреть невозможно.

Но ребята в JetBrains похоже услышали мольбы разработчиков и реализовали систему of smart casts. Вышеуказанный код на Kotlin выглядит следующим образом:

А теперь представьте, что вам нужно выполнить несколько вызовов переменной, и посмотрите, как это красиво можно сделать с помощью функции with:

Последний раз такую вкусность я использовал в Pascal/Delphi, и это было чертовски удобно, за такими вещами скучаешь. Особенно часто я вспоминаю об этом, когда инициализирую какой-нибудь объект типа Paint, и надо сделать несколько вызовов подряд, чтобы задать шрифт, цвет, тип заливки, антиалиасинг и прочее.

Тем не менее нужно помнить, что automatic smart cast работает в том случае, если компилятор точно уверен, что между проверкой и использованием переменной она не поменяет значение.

Ну а как быть, если мы хотим привести переменную к какому-то типу? Kotlin предлагает на выбор две опции:

  • unsafe cast (небезопасныйое секс, пардон, приведение) с помощью infix operator as
  • safe cast с помощью infix operator as?

В случае первого при попытке привести null value к какому-то типу выкинет исключение. Второй же оператор просто вернет null в результирующую переменную. Я бы рекомендовал использовать именно его.

Что мы имеем в итоге? Красивый, элегантный, читабельный код, не перегруженный синтаксисом. Прямо вот не могу удержаться и накидываю еще 2 очка в пользу Kotlin. Прости, Java, сегодня, видимо, не твой счастливый день.

Type aliases (доступно в версии 1.1)

Раз уж мы поговорили про приведение типов, то целесообразным будет упомянуть возможность объявления альтернативных имен для существующих типов. Что-то подобное я встречал давно в Pascal. Но в Kotlin эта фича работает и для функций, что делает ее особенно интересной! Что конкретно нам это дает?

    мы можем объявлять «свои типы данных», к примеру, сократить длинный женерик до короткого дружелюбного имени

Да, знаю, имя тут вышло не совсем короткое, но вы меня поняли.;)

Следует заметить, что type alias не генерирует нового типа данных. На уровне компилятора он расценивается как тот самый основной тип, что делает их взаимозаменяемыми. Я бы предложил следующие ситуации, когда можно использовать type aliases:

  • меняющиеся структуры данных;
  • меняющиеся сигнатуры функций;
  • тестирование;
  • создание flavour-билдов;
  • смысловое разделение типов данных и функций по именам/категориям.

Не могу сказать, что вот прямо конкретно этой возможностью я бы пользовался каждый день, но отторжения она у меня не вызвала, а скорее наоборот. Я использовал ее в Pascal/Delphi и не вижу, что может мне помешать сейчас! Однозначно +1 в корзинку Kotlin.

Ranges & Controlling Flow

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

Я специально привел самый простой пример, на Kotlin он будет выглядеть вот так:

Чертовски удобно ведь! Если надо выполнить больше кода в блоках условия — нет проблем, но последняя строка должна возвращать результат работы в переменную. Этот же самый принцип работает для when expression. when — это тот же switch из Java и других С-подобных языков. Но вот только when дает намного больше свобод и вариативности использования. В сравнении с ним switch выглядит мелковато и абсолютно не юзабельно. Да, и забудьте про ненавистный break для каждой из веток условий.

Я не случайно объединил ranges и Flow Control Structures в один раздел. Нельзя рассказывать про блоки условий и циклы и не затронуть ranges, ведь именно они являются еще одним замечательным бонусом, который будет облегчать вам жизнь. Признаться, я еще со времен Паскаля очень любил ranges и считал их очень удобным решением, и все никак не мог понять, почему их нет как отдельного типа данных в том же С/С++ или в РНР(с которым в обнимку провел 9 лет) или в JS и, конечно же, в Java. И вот в Kotlin я снова с ними повстречался, но вот только варианты их использования стали шире и удобнее по сравнению с Паскалем. К примеру, range можно сохранить в переменную и использовать в блоке условия:

Естественно, что вместо чисел 1 и 10, могут быть использованы переменные или даже вызовы функций. При использовании в цикле for можно указывать направление движения и величину шага:

Ах, да, еще в циклах можно использовать break & continue с метками для прыжков между циклами как и в Java.

Ниже приведен кусочек кода, где я попытался вынести все возможные варианты использования when:

Сколько бы вы баллов накинули Kotlin за этот раздел?

Изначально на этом месте я подводил итоги по базовым вещам и говорил о том, какой же все-таки Kotlin классный, как лаконичнее код выглядит с применением вышеизложенного и прочее бла-бла-бла, НО, как мы знаем, марта вышло обновление 1.1., и оно нам принесло:

Coroutines (доступно в версии 1.1)

Правильнее даже сказать: «доступно с версией 1.1», потому что корутины не входят в сам язык на данный момент, а являются экспериментальной функциональностью и вынесены в отдельный артефакт. Для использования оных нужно в build.gradle файле проекта указать:

И теперь нам становятся доступны корутины. Что же это такое? В каком-то смысле — это легковесная нить, но очень дешевая в обслуживании, которую можно приостанавливать и возобновлять. Она может состоять из вызовов других корутин, которые в свою очередь могут выполняться в совершенно разных нитях. Одна нить (Thread) может обслуживать множество корутин. Kotlin предоставляет несколько механизмов использования корутин, но я рассмотрю только парочку наиболее интересных в контексте Android-разработки.

Первый вариант использования — это функция launch():

CommonPool представляет собой пул повторно используемых нитей и под капотом использует java.util.concurrent.ForkJoinPool. Ниже приведены два варианта одного и того же решения: блокирующие вызовы (никогда так не делайте!) и неблокирующие на основе корутин.

И в консоли результат выполнения у нас будет такой:

А вот неблокирующий вызов:

И в этом случае результат выполнения у нас будет такой:

Зоркий глаз и пытливый ум уже заметили, что для задержки в 100 миллисекунд внутри функции launch() использовался вызов delay(). Он аналогичен Thread.sleep(), но используется в контексте корутины и блокирует ее, а не основной поток. Важно запомнить, что все конструкции, специфичные для корутин, могут быть использованы только в их пределах.

Ахх, я вижу как у вас загорелись глаза и начали роиться мысли о том, как с помощью одной такой конструкции можно заменить связку Thread-Handler(UI)? Как же нам данные из корутины корректно передать в UI-поток? Самые невнимательные могут предложить такое решение:

и сразу получат по рукам — мы же находимся в рабочей нити. А внимательные сделают так:

и это будет работать, но выглядит оно как-то не очень, согласитесь. И прежде чем показать, как это можно сделать «по красоте», необходимо разобраться с функцией async(). Она очень похожа на launch(), с одним лишь отличием, что возвращает результат работы экземпляром Deferred, а у него есть метод await(), который собственно и возвращает результат работы функции.

Давайте смотреть, как это выглядит. Предположим, нам нужно посчитать факториалы двух чисел и получить их сумму. Не спрашивайте меня, кому это может понадобиться, просто мне так захотелось.;) Для этого опишем следующую корявенькую функцию подсчета факториала (+1 в карму первому комментатору данной реализации, а все желающие углубить свои познания о факториале милости просим сюдой):

Внутри функции запускается async-корутина, которая вернет нам значение типа Long. Я поставил задержку в цикле, чтобы показать одну примечательную деталь работы корутин (о ней чуток позже). Теперь сделаем вызов этой функции и посмотрим, как оно отработает:

Результаты работы следующие:

А теперь о том, на чем я бы хотел заострить ваше внимание. Если вы заметили, то разница между последними двумя выводами в консоли — около 2.5 секунд. Хотя 5! будет посчитан за

0.5 секунды. А вот 25! как раз просчитается где-то за 2.5 секунды. Таким образом функция вывода println() не будет выполнена пока не вернутся результаты из всех корутин, т.е. по факту — пока не отработает самая долгая из них.

Отлично, с этим разобрались, и, собственно, теперь можем вернуться к поставленной выше задаче: как результаты работы корутины правильно передать в UI-поток. И в этом нам поможет модуль kotlinx-coroutines-android, который предоставляет UI контекст для корутин — замену тому самому CommonPool, что мы использовали в примерах выше. Для этого закидываем этот модуль в зависимости:

Теперь мы смело можем сделать так:

Первым делом я проверил, как это дело работает в случае, если контекст умер — работает отлично, утечек я не пронаблюдал! Честно признаться: я и до этого времени AsyncTask совсем не часто использовал, а теперь не вижу необходимости вообще.

Отдельным пунктом хочу заметить, что с применением корутин отпадает необходимость использования колбеков для асинхронных вызовов. Я на дух не переношу ситуации, когда у меня флоу состоит из нескольких последовательных API-коллов. Эти громоздкие конструкции/ветвления переходящие одно в другое — бррр. Теперь же мы просто можем закинуть пачку API-коллов в нужной нам последовательности в корутину, обернуть их, к примеру, в try/catch (чтобы отлавливать различные исключения) и voilà, все готово! И, если я еще не сильно надоел, то хочу подкинуть еще чуток на корутинный вентилятор:

Я специально остановился только на двух механизмах работы с корутинами, там их больше, и даже питонисты найдут свой любимый yield.;)

Пытливый ум может упрекнуть меня, что корутины не совсем тянут на базовые вещи в Kotlin, и отвести бы им место в следующей статье рядом с лямбдами, функциями и прочей вкусняшкой. и где-то я с ними соглашусь. Но уж больно хороши они мне показались, и я не смог не поделиться этой информацией с вами!;) Слишком уж сильно они упрощают жизнь и сокращают время от точки А (создание proof of concept) до точки Б (получение готовых результатов), а это дорогого стоит, не так ли?

Лично для себя я сделал уже выбор, и, боюсь, он не сильно в пользу Java. Так что можно дальше баллы не считать. Но о том, ЧТО еще Kotlin умеет, я постараюсь вменяемо рассказать в следующей (последней) части. А теперь можно и на боковую. и только бы не Йода мне приснился, только бы не эта зеленая марты.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

📎📎📎📎📎📎📎📎📎📎