https://trends.google.com/trends/explore?q=%2Fm%2F0_lcrx4 Выше приведён скриншот Google Trends, когда я искал по слову «kotlin». Внезапный всплеск — это когда Google объявила, что Kotlin становится главным языком в Android. Произошло это на конференции Google I/O несколько недель назад. На сегодняшний день вы либо уже использовали этот язык раньше, либо заинтересовались им, потому что все вокруг вдруг начали о нём говорить.  Одно из главных свойств Kotlin — его взаимная совместимость с Java: вы можете вызывать из Java код Kotlin, а из Kotlin код Java. Это, пожалуй, важнейшая черта, благодаря которой язык широко распространяется. Вам не нужно сразу всё мигрировать: просто возьмите кусок имеющейся кодовой базы и начните добавлять код Kotlin, и это будет работать. Если вы поэкспериментируете с Kotlin и вам не понравится, то всегда можно от него отказаться (хотя я рекомендую попробовать). Когда я впервые использовал Kotlin после пяти лет работы на Java, некоторые вещи казались мне настоящим волшебством.  «Погодите, что? Я могут просто писать data class, чтобы избежать шаблонного кода?»
«Стоп, так если я пишу apply, то мне уже не нужно определять объект каждый раз, когда я хочу вызвать метод применительно к нему?» После первого вздоха облегчения от того, что наконец-то появился язык, который не выглядит устаревшим и громоздким, я начал ощущать некоторый дискомфорт. Если требуется взаимная совместимость с Java, то как именно в Kotlin реализованы все эти прекрасные возможности? В чём подвох? Этому и посвящена статья. Мне было очень интересно узнать, как компилятор Kotlin преобразует конкретные конструкции, чтобы они стали взаимосовместимы с Java. Для своих исследований я выбрал четыре наиболее востребованных метода из стандартной библиотеки Kotlin: Когда вы прочитаете эту статью, вам больше не надо будет опасаться. Сейчас я чувствую себя гораздо увереннее, потому что понял, как всё работает, и я знаю, что могу доверять языку и компилятору. apply проста: это функция-расширение, которая выполняет параметр block применительно к экземпляру расширенного типа (extended type) (он называется «получатель») и возвращает самого получателя. Есть много способов применения этой функции. Можно привязать создание объекта к его начальной конфигурации: val layout = LayoutStyle().apply { orientation = VERTICAL } Как видите, мы предоставляем конфигурацию для нового LayoutStyle прямо при создании, что способствует чистоте кода и реализации, гораздо менее подверженной ошибкам. Случалось вызывать метод применительно к неправильному экземпляру, потому что он имел то же наименование? Или, ещё хуже, когда рефакторинг был полностью ошибочным? С вышеуказанным подходом будет куда сложнее столкнуться с такими неприятностями. Также обратите внимание, что необязательно определять параметр this: мы находимся в той же области видимости, что и сам класс. Это как если бы мы расширяли сам класс, поэтому this задаётся неявно. Но как это работает? Давайте рассмотрим короткий пример. Благодаря инструменту IntelliJ IDEA «Show Kotlin bytecode» (Tools > Kotlin > Show Kotlin Bytecode) мы можем посмотреть, как компилятор преобразует наш код в JVM-байткод: Если вы не слишком хорошо ориентируетесь в байткоде, то предлагаю почитать эти замечательные статьи, после них вы станете разбираться гораздо лучше (важно помнить, что при вызове каждого метода происходит обращение к стеку, так что компилятору нужно каждый раз загружать объект). Разберём по пунктам: Здесь отметим пару вещей. Во-первых, не задействовано никакой магии, всё происходит так, как ожидается: применительно к созданному нами экземпляру LayoutStyle вызывается метод setOrientation. Кроме того, нигде не видно функции apply, потому что компилятор инлайнит её. Более того, байткод почти идентичен тому, который генерируется при использовании одного лишь Java! Судите сами: Совет: вы могли заметить большое количество операций ASTORE/ALOAD. Они вставлены компилятором Kotlin, так что отладчик работает и для лямбд! Об этом мы поговорим в последнем разделе статьи. with выглядит аналогичным apply, но есть некоторые важные отличия. Во-первых, with не является функцией-расширением типа: получатель должен явным образом передаваться в качестве параметра. Более того, with возвращает результат функции block, а apply — самого получателя. Поскольку мы можем возвращать что угодно, этот пример выглядит очень правдоподобно: Здесь можно опустить префикс contextWrapper. для context и attrs, потому что contextWrapper — получатель функции with. Но даже в этом случае способы применения вовсе не так очевидны по сравнению с apply, эта функция может оказаться полезна при определённых условиях.  Учитывая это, вернёмся к нашему примеру и посмотрим, что будет, если воспользоваться with: Получатель with — синглтон SharedState, он содержит параметр ориентации (orientation parameter), который мы хотим задать для нашего макета. Внутри функции block создаём экземпляр LayoutStyle, и благодаря apply мы можем просто задать ориентацию, считав её из SharedState. Посмотрим снова на сгенерированный байткод: Ничего особенного. Извлечён синглтон, реализованный в виде статичного поля в классе SharedState; экземпляр LayoutStyle создаётся так же, как и раньше, вызывается конструктор, ещё одно обращение для получения значения previousOrientation внутри SharedState и последнее обращение для присвоения значения экземпляру LayoutStyle. Совет: при использовании «Show Kotlin Bytecode» можно нажать «Decompile» и посмотреть Java-представление байткода, созданного для компилятора Kotlin. Спойлер: оно будет именно таким, как вы ожидаете! let очень полезен при работе с объектами, которые могут принимать значение null. Вместо того чтобы создавать бесконечные цепочки выражений if-else, можно просто скомбинировать оператор ? (называется «оператор безопасного вызова») с let: в результате вы получите лямбду, у которой аргумент it является не-nullable-версией исходного объекта. Рассмотрим пример целиком: Теперь previousOrientation может быть null. Если мы попробуем напрямую присвоить его нашему макету, то компилятор возмутится, потому что nullable-тип нельзя присваивать не-nullable-типу. Конечно, можно написать выражение if, но это приведёт к двойной ссылке на выражение SharedState.previousOrientation. А если воспользоваться let, то получим не-nullable-ссылку на тот же самый параметр, которую можно безопасно присвоить нашему макету.
С точки зрения байткода всё очень просто: Здесь используется простой условный переход IFNULL, который, по сути, вам бы пришлось делать вручную, за исключением этого раза, когда компилятор эффективно выполняет его за вас, а язык предлагает приятный способ написания такого кода. Думаю, это замечательно! Есть две версии run: первая — простая функция, вторая — функция-расширение обобщённого типа (generic type). Поскольку первая всего лишь вызывает функцию block, которая передаётся как параметр, мы будем анализировать вторую. Пожалуй, run — самая простая из рассмотренных функций. Она определена как функция-расширение типа, чей экземпляр затем передаётся в качестве получателя и возвращает результат исполнения функции block. Может показаться, что run — некий гибрид let и apply, и это действительно так. Единственное отличие заключается в возвращаемом значении: в случае с apply мы возвращаем самого получателя, а в случае с run — результат функции block (как и у let). В этом примере подчёркивается тот факт, что run возвращает результат функции block, в данном случае это присваивание (Unit): Эквивалентный байткод: run была инлайнена, как и другие функции, и всё сводится к простым вызовам методов. Здесь тоже нет ничего странного! Мы отмечали, что между функциями стандартной библиотеки есть много сходств: это сделано умышленно, чтобы покрыть как можно больше вариантов применения. С другой стороны, не так просто понять, какая из функций лучше всего подходит для конкретной задачи, учитывая незначительность отличий между ними. Чтобы помочь вам разобраться со стандартной библиотекой, я нарисовал таблицу, в которой сведены все отличия между основными рассмотренными функциями (за исключением also):  Я ещё кое-что не мог до конца понять при сравнении «Java-байткода» и «Kotlin-байткода». Как я уже говорил, в Kotlin, в отличие от Java, были дополнительные операции astore/aload. Я знал, что это как-то связано с лямбдами, но мог разобраться, зачем они нужны. Похоже, эти дополнительные операции необходимы отладчику для обработки лямбд как стековых фреймов, что позволяет нам вмешиваться (step into) в их работу. Мы можем видеть, чем являются локальные переменные, кто вызывает лямбду, кто будет вызван из лямбды и т. д. Но когда мы передаём APK в production, нас не волнуют возможности отладчика, верно? Так что можно считать эти функции избыточными и подлежащими удалению, несмотря на их небольшой размер и незначительность. Для этого может подойти ProGuard, инструмент всем известный и всеми «любимый». Он работает на уровне байткода и, помимо запутывания и урезания, также выполняет оптимизационные проходы, чтобы сделать байткод компактнее. Я написал одинаковый кусок кода на Java и Kotlin, применил к обеим версиям ProGuard с одним набором правил и сравнил результаты. Вот что обнаружилось. Java: Kotlin: Java: Kotlin: Выводы после сравнения двух листингов байткода: Замечательно получить новый язык, предлагающий разработчикам настолько много возможностей. Но также важно знать, что мы можем полагаться на используемые инструменты, и чувствовать уверенность при работе с ними. Я рад, что могу сказать: «Я доверяю Kotlin», в том смысле, что я знаю: компилятор не делает ничего лишнего или рискованного. Он делает только то, что в Java нам нужно делать вручную, экономя нам время и ресурсы (и возвращает давно утраченную радость от кодинга для JVM). В какой-то мере это приносит пользу и конечным пользователям, потому что благодаря более строгой типобезопасности мы оставим меньше багов в приложениях. Кроме того, компилятор Kotlin постоянно улучшается, так что генерируемый код становится всё эффективнее. Так что не нужно пытаться оптимизировать Kotlin-код с помощью компилятора, лучше сосредоточиться на том, чтобы писать более эффективный и идиоматичный код, оставляя всё остальное на откуп компилятору. ¯\_(ツ)_/¯