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
with
let
run
Когда вы прочитаете эту статью, вам больше не надо будет опасаться. Сейчас я чувствую себя гораздо увереннее, потому что понял, как всё работает, и я знаю, что могу доверять языку и компилятору.
Apply
/**
* Вызывает определённую функцию [block] со значением `this` в качестве своего получателя и возвращает значение `this`.
*/
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
apply
проста: это функция-расширение, которая выполняет параметр block применительно к экземпляру расширенного типа (extended type) (он называется «получатель») и возвращает самого получателя.
Есть много способов применения этой функции. Можно привязать создание объекта к его начальной конфигурации:
val layout = LayoutStyle().apply { orientation = VERTICAL }
Как видите, мы предоставляем конфигурацию для нового LayoutStyle
прямо при создании, что способствует чистоте кода и реализации, гораздо менее подверженной ошибкам. Случалось вызывать метод применительно к неправильному экземпляру, потому что он имел то же наименование? Или, ещё хуже, когда рефакторинг был полностью ошибочным? С вышеуказанным подходом будет куда сложнее столкнуться с такими неприятностями. Также обратите внимание, что необязательно определять параметр this
: мы находимся в той же области видимости, что и сам класс. Это как если бы мы расширяли сам класс, поэтому this
задаётся неявно.
Но как это работает? Давайте рассмотрим короткий пример.
enum class Orientation {
VERTICAL, HORIZONTAL
}
class LayoutStyle {
var orientation = HORIZONTAL
}
fun main(vararg args: Array<String>) {
val layout = LayoutStyle().apply { orientation = VERTICAL }
}
Благодаря инструменту IntelliJ IDEA «Show Kotlin bytecode» (Tools > Kotlin > Show Kotlin Bytecode
) мы можем посмотреть, как компилятор преобразует наш код в JVM-байткод:
NEW kotlindeepdive/LayoutStyle
DUP
INVOKESPECIAL kotlindeepdive/LayoutStyle.<init> ()V
ASTORE 2
ALOAD 2
ASTORE 3
ALOAD 3
GETSTATIC kotlindeepdive/Orientation.VERTICAL : Lkotlindeepdive/Orientation;
INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (Lkotlindeepdive/Orientation;)V
ALOAD 2
ASTORE 1
Если вы не слишком хорошо ориентируетесь в байткоде, то предлагаю почитать эти замечательные статьи, после них вы станете разбираться гораздо лучше (важно помнить, что при вызове каждого метода происходит обращение к стеку, так что компилятору нужно каждый раз загружать объект).
Разберём по пунктам:
- Создаётся новый экземпляр
LayoutStyle
и дублируется в стек. - Вызывается конструктор с нулевыми параметрами.
- Выполняются операции store/load (об этом — ниже).
- В стек передаётся значение
Orientation.VERTICAL
. - Вызывается
setOrientation
, который поднимает из стека объект и значение.
Здесь отметим пару вещей. Во-первых, не задействовано никакой магии, всё происходит так, как ожидается: применительно к созданному нами экземпляру LayoutStyle
вызывается метод setOrientation
. Кроме того, нигде не видно функции apply
, потому что компилятор инлайнит её.
Более того, байткод почти идентичен тому, который генерируется при использовании одного лишь Java! Судите сами:
// Java
enum Orientation {
VERTICAL, HORIZONTAL;
}
public class LayoutStyle {
private Orientation orientation = HORIZONTAL;
public Orientation getOrientation() {
return orientation;
}
public void setOrientation(Orientation orientation) {
this.orientation = orientation;
}
public static void main(String[] args) {
LayoutStyle layout = new LayoutStyle();
layout.setOrientation(VERTICAL);
}
}
// Bytecode
NEW kotlindeepdive/LayoutStyle
DUP
ASTORE 1
ALOAD 1
GETSTATIC kotlindeepdive/Orientation.VERTICAL : kotlindeepdive/Orientation;
INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (kotlindeepdive/Orientation;)V
Совет: вы могли заметить большое количество операций ASTORE/ALOAD
. Они вставлены компилятором Kotlin, так что отладчик работает и для лямбд! Об этом мы поговорим в последнем разделе статьи.
With
/**
* Вызывает определённую функцию [block] с данным [receiver] в качестве своего получателя и возвращает результат.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
with
выглядит аналогичным apply
, но есть некоторые важные отличия. Во-первых, with
не является функцией-расширением типа: получатель должен явным образом передаваться в качестве параметра. Более того, with
возвращает результат функции block, а apply
— самого получателя.
Поскольку мы можем возвращать что угодно, этот пример выглядит очень правдоподобно:
val layout = with(contextWrapper) {
// `this` is the contextWrapper
LayoutStyle(context, attrs).apply { orientation = VERTICAL }
}
Здесь можно опустить префикс contextWrapper
. для context
и attrs
, потому что contextWrapper
— получатель функции with
. Но даже в этом случае способы применения вовсе не так очевидны по сравнению с apply
, эта функция может оказаться полезна при определённых условиях.
Учитывая это, вернёмся к нашему примеру и посмотрим, что будет, если воспользоваться with
:
enum class Orientation {
VERTICAL, HORIZONTAL
}
class LayoutStyle {
var orientation = HORIZONTAL
}
object SharedState {
val previousOrientation = VERTICAL
}
fun main() {
val layout = with(SharedState) {
LayoutStyle().apply { orientation = previousOrientation }
}
}
Получатель with
— синглтон SharedState
, он содержит параметр ориентации (orientation parameter), который мы хотим задать для нашего макета. Внутри функции block создаём экземпляр LayoutStyle
, и благодаря apply
мы можем просто задать ориентацию, считав её из SharedState
.
Посмотрим снова на сгенерированный байткод:
GETSTATIC kotlindeepdive/SharedState.INSTANCE : Lkotlindeepdive/SharedState;
ASTORE 1
ALOAD 1
ASTORE 2
NEW kotlindeepdive/LayoutStyle
DUP
INVOKESPECIAL kotlindeepdive/LayoutStyle.<init> ()V
ASTORE 3
ALOAD 3
ASTORE 4
ALOAD 4
ALOAD 2
INVOKEVIRTUAL kotlindeepdive/SharedState.getPreviousOrientation ()Lkotlindeepdive/Orientation;
INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (Lkotlindeepdive/Orientation;)V
ALOAD 3
ASTORE 0
RETURN
Ничего особенного. Извлечён синглтон, реализованный в виде статичного поля в классе SharedState
; экземпляр LayoutStyle
создаётся так же, как и раньше, вызывается конструктор, ещё одно обращение для получения значения previousOrientation
внутри SharedState
и последнее обращение для присвоения значения экземпляру LayoutStyle
.
Совет: при использовании «Show Kotlin Bytecode» можно нажать «Decompile» и посмотреть Java-представление байткода, созданного для компилятора Kotlin. Спойлер: оно будет именно таким, как вы ожидаете!
Let
/**
* Вызывает заданную функцию [block] со значением `this` в качестве аргумента и возвращает результат.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
let
очень полезен при работе с объектами, которые могут принимать значение null. Вместо того чтобы создавать бесконечные цепочки выражений if-else, можно просто скомбинировать оператор ?
(называется «оператор безопасного вызова») с let
: в результате вы получите лямбду, у которой аргумент it
является не-nullable-версией исходного объекта.
val layout = LayoutStyle()
SharedState.previousOrientation?.let { layout.orientation = it }
Рассмотрим пример целиком:
enum class Orientation {
VERTICAL, HORIZONTAL
}
class LayoutStyle {
var orientation = HORIZONTAL
}
object SharedState {
val previousOrientation: Orientation? = VERTICAL
}
fun main() {
val layout = LayoutStyle()
// layout.orientation = SharedState.previousOrientation -- this would NOT work!
SharedState.previousOrientation?.let { layout.orientation = it }
}
Теперь previousOrientation
может быть null. Если мы попробуем напрямую присвоить его нашему макету, то компилятор возмутится, потому что nullable-тип нельзя присваивать не-nullable-типу. Конечно, можно написать выражение if, но это приведёт к двойной ссылке на выражение SharedState.previousOrientation
. А если воспользоваться let
, то получим не-nullable-ссылку на тот же самый параметр, которую можно безопасно присвоить нашему макету.
С точки зрения байткода всё очень просто:
NEW kotlindeepdive/let/LayoutStyle
DUP
INVOKESPECIAL kotlindeepdive/let/LayoutStyle.<init> ()V
GETSTATIC kotlindeepdive/let/SharedState.INSTANCE : Lkotlindeepdive/let/SharedState;
INVOKEVIRTUAL kotlindeepdive/let/SharedState.getPreviousOrientation ()Lkotlindeepdive/let/Orientation;
DUP
IFNULL L2
ASTORE 1
ALOAD 1
ASTORE 2
ALOAD 0
ALOAD 2
INVOKEVIRTUAL kotlindeepdive/let/LayoutStyle.setOrientation (Lkotlindeepdive/let/Orientation;)V
GOTO L9
L2
POP
L9
RETURN
Здесь используется простой условный переход IFNULL
, который, по сути, вам бы пришлось делать вручную, за исключением этого раза, когда компилятор эффективно выполняет его за вас, а язык предлагает приятный способ написания такого кода. Думаю, это замечательно!
Run
Есть две версии run: первая — простая функция, вторая — функция-расширение обобщённого типа (generic type). Поскольку первая всего лишь вызывает функцию block, которая передаётся как параметр, мы будем анализировать вторую.
/**
* Вызывает определённую функцию [block] со значением `this` в качестве получателя и возвращает результат.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R = block()
Пожалуй, run
— самая простая из рассмотренных функций. Она определена как функция-расширение типа, чей экземпляр затем передаётся в качестве получателя и возвращает результат исполнения функции block
. Может показаться, что run
— некий гибрид let
и apply
, и это действительно так. Единственное отличие заключается в возвращаемом значении: в случае с apply
мы возвращаем самого получателя, а в случае с run
— результат функции block
(как и у let
).
В этом примере подчёркивается тот факт, что run
возвращает результат функции block
, в данном случае это присваивание (Unit
):
enum class Orientation {
VERTICAL, HORIZONTAL
}
class LayoutStyle {
var orientation = HORIZONTAL
}
object SharedState {
val previousOrientation = VERTICAL
}
fun main() {
val layout = LayoutStyle()
layout.run { orientation = SharedState.previousOrientation } // returns Unit
}
Эквивалентный байткод:
NEW kotlindeepdive/LayoutStyle
DUP
INVOKESPECIAL kotlindeepdive/LayoutStyle.<init> ()V
ASTORE 0
ALOAD 0
ASTORE 1
ALOAD 1
ASTORE 2
ALOAD 2
GETSTATIC kotlindeepdive/SharedState.INSTANCE : Lkotlindeepdive/SharedState;
INVOKEVIRTUAL kotlindeepdive/SharedState.getPreviousOrientation ()Lkotlindeepdive/Orientation;
INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (Lkotlindeepdive/Orientation;)V
RETURN
run
была инлайнена, как и другие функции, и всё сводится к простым вызовам методов. Здесь тоже нет ничего странного!
Мы отмечали, что между функциями стандартной библиотеки есть много сходств: это сделано умышленно, чтобы покрыть как можно больше вариантов применения. С другой стороны, не так просто понять, какая из функций лучше всего подходит для конкретной задачи, учитывая незначительность отличий между ними.
Чтобы помочь вам разобраться со стандартной библиотекой, я нарисовал таблицу, в которой сведены все отличия между основными рассмотренными функциями (за исключением also):
Приложение: дополнительные операции store/load
Я ещё кое-что не мог до конца понять при сравнении «Java-байткода» и «Kotlin-байткода». Как я уже говорил, в Kotlin, в отличие от Java, были дополнительные операции astore/aload
. Я знал, что это как-то связано с лямбдами, но мог разобраться, зачем они нужны.
Похоже, эти дополнительные операции необходимы отладчику для обработки лямбд как стековых фреймов, что позволяет нам вмешиваться (step into) в их работу. Мы можем видеть, чем являются локальные переменные, кто вызывает лямбду, кто будет вызван из лямбды и т. д.
Но когда мы передаём APK в production, нас не волнуют возможности отладчика, верно? Так что можно считать эти функции избыточными и подлежащими удалению, несмотря на их небольшой размер и незначительность.
Для этого может подойти ProGuard, инструмент всем известный и всеми «любимый». Он работает на уровне байткода и, помимо запутывания и урезания, также выполняет оптимизационные проходы, чтобы сделать байткод компактнее. Я написал одинаковый кусок кода на Java и Kotlin, применил к обеим версиям ProGuard с одним набором правил и сравнил результаты. Вот что обнаружилось.
Конфигурация ProGuard
-dontobfuscate
-dontshrink
-verbose
-keep,allowoptimization class kotlindeepdive.apply.LayoutStyle
-optimizationpasses 2
-keep,allowoptimization class kotlindeepdive.LayoutStyleJ
Исходный код
Java:
package kotlindeepdive
enum OrientationJ {
VERTICAL, HORIZONTAL;
}
class LayoutStyleJ {
private OrientationJ orientation = HORIZONTAL;
public OrientationJ getOrientation() {
return orientation;
}
public LayoutStyleJ() {
if (System.currentTimeMillis() < 1) { main(); }
}
public void setOrientation(OrientationJ orientation) {
this.orientation = orientation;
}
public OrientationJ main() {
LayoutStyleJ layout = new LayoutStyleJ();
layout.setOrientation(VERTICAL);
return layout.orientation;
}
}
Kotlin:
package kotlindeepdive.apply
enum class Orientation {
VERTICAL, HORIZONTAL
}
class LayoutStyle {
var orientation = Orientation.HORIZONTAL
init {
if (System.currentTimeMillis() < 1) { main() }
}
fun main() {
val layout = LayoutStyle().apply { orientation = Orientation.VERTICAL }
layout.orientation
}
}
Байткод
Java:
sgotti@Sebastianos-MBP ~/Desktop/proguard5.3.3/lib/PD/kotlindeepdive > javap -c LayoutStyleJ.class
Compiled from "SimpleJ.java"
final class kotlindeepdive.LayoutStyleJ {
public kotlindeepdive.LayoutStyleJ();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: aload_0
5: getstatic #6 // Field kotlindeepdive/OrientationJ.HORIZONTAL$5c1d747f:I
8: putfield #5 // Field orientation$5c1d747f:I
11: invokestatic #9 // Method java/lang/System.currentTimeMillis:()J
14: lconst_1
15: lcmp
16: ifge 34
19: new #3 // class kotlindeepdive/LayoutStyleJ
22: dup
23: invokespecial #10 // Method "<init>":()V
26: getstatic #7 // Field kotlindeepdive/OrientationJ.VERTICAL$5c1d747f:I
29: pop
30: iconst_1
31: putfield #5 // Field orientation$5c1d747f:I
34: return
}
Kotlin:
sgotti@Sebastianos-MBP ~/Desktop/proguard5.3.3/lib/PD/kotlindeepdive > javap -c apply/LayoutStyle.class
Compiled from "Apply.kt"
public final class kotlindeepdive.apply.LayoutStyle {
public kotlindeepdive.apply.LayoutStyle();
Code:
0: aload_0
1: invokespecial #13 // Method java/lang/Object."<init>":()V
4: aload_0
5: getstatic #11 // Field kotlindeepdive/apply/Orientation.HORIZONTAL:Lkotlindeepdive/apply/Orientation;
8: putfield #10 // Field orientation:Lkotlindeepdive/apply/Orientation;
11: invokestatic #14 // Method java/lang/System.currentTimeMillis:()J
14: lconst_1
15: lcmp
16: ifge 32
19: new #8 // class kotlindeepdive/apply/LayoutStyle
22: dup
23: invokespecial #16 // Method "<init>":()V
26: getstatic #12 // Field kotlindeepdive/apply/Orientation.VERTICAL:Lkotlindeepdive/apply/Orientation;
29: putfield #10 // Field orientation:Lkotlindeepdive/apply/Orientation;
32: return
}
Выводы после сравнения двух листингов байткода:
- Дополнительные операции
astore/aload
в «Kotlin-байткоде» исчезли, потому что ProGuard счёл их избыточными и сразу удалил (любопытно, что для этого понадобилось сделать два оптимизационных прохода, после одного они не были удалены). - «Java-байткод» и «Kotlin-байткод» почти идентичны. В первом есть интересные/странные моменты при работе с enum-значением, а в Kotlin ничего подобного нет.
Заключение
Замечательно получить новый язык, предлагающий разработчикам настолько много возможностей. Но также важно знать, что мы можем полагаться на используемые инструменты, и чувствовать уверенность при работе с ними. Я рад, что могу сказать: «Я доверяю Kotlin», в том смысле, что я знаю: компилятор не делает ничего лишнего или рискованного. Он делает только то, что в Java нам нужно делать вручную, экономя нам время и ресурсы (и возвращает давно утраченную радость от кодинга для JVM). В какой-то мере это приносит пользу и конечным пользователям, потому что благодаря более строгой типобезопасности мы оставим меньше багов в приложениях.
Кроме того, компилятор Kotlin постоянно улучшается, так что генерируемый код становится всё эффективнее. Так что не нужно пытаться оптимизировать Kotlin-код с помощью компилятора, лучше сосредоточиться на том, чтобы писать более эффективный и идиоматичный код, оставляя всё остальное на откуп компилятору.
Комментарии (24)
Alexeyco
23.06.2017 14:40-1Одно из главных свойств Kotlin — его взаимная совместимость с Java: вы можете вызывать из Java код Kotlin, а из Kotlin код Java
Нашел силы превозмочь пассаж про главность языка Kotlin в Android, как вот оно… дальше читать не буду. И это пишет человек, который 5 лет разрабатывает на Java?
ov7a
23.06.2017 16:38+5Я программирую на Kotlin больше года, но вашу табличку с apply, run/with, let, also вообще не понял.
VolCh
23.06.2017 23:46-1Может ли что интересного предложить Kotlin для разработчиков на PHP, что не смогла предложить Java?
RockPresident
26.06.2017 00:10+3Одно из главных свойств Kotlin — его взаимная совместимость с Java: вы можете вызывать из Java код Kotlin, а из Kotlin код Java.
Поскольку эту статью могут читать новички мира Java, я считаю нужным (и лучше всего было бы записать это в самой статье) — что это не является таким уж и редким достоинством. Языков которые имеют такой же уровень совместимости с Java — и так не мало (Groovy, Scala, Clojure, Jython), и если для вас это является важным фактором — то по крайней мере стоит их изучить. Многие из них имеют гораздо более развитые комьюнити, историю и стабильный набор инструментов.
mayorovp
26.06.2017 10:19Jython в этом списке лишний — его классы не могут использоваться в Java напрямую, максимум на что он способен — реализовать интерфейс, объявленный в Java.
C4ET4uK
26.06.2017 10:59-1По-поводу Scala — категорически не согласен. Интероп с Java там заметное менее удобен. Как-минимум за счет не совместимых классов-коллекций.
grossws
26.06.2017 17:00+1import scala.collection.JavaConverters._ val arr: Array[(String, AnyRef)] = Array("a" -> "b", "c" -> 1) val m: java.util.Map[String, Object] = arr.toMap.asJava
ivan_petroff
26.06.2017 10:59-1Неплохая статья, особенно для изучающих Котлин минуя язык программирования Java.
mihnik
26.06.2017 10:59+4With
/**
* Вызывает определённую функцию [block] с данным [receiver] в качестве своего получателя и возвращает результат.
*/
Бог мой, если бы я не знал что делает With, неужели по этому описанию это можно понять…
SirEdvin
Как можно было так перевести фразу "Android Announces Support for Kotlin"?
Через несколько месяцев нас ждут статьи с заголовками "На Google I/O сказали, что весь андроид будет переписан на Kotlin и будет поддерживать только его"?
Alexeyco
Это автор еще забыл уточнить, что язык — российский. И компания JetBrains — тоже российская. Он на Ленте.ру, интересно, не подрабатывает.
VolCh
А мне всю жизнь казалось, что чешская с исторически сложившимся аутстаффом разработки в Питере.
Nakosika
Это бизнес по-русски. Делаешь компанию, а потом регистрируешь за границей чтобы органы не отжали.
GreenStore
А я всегда думал, что регистрацию за границей делают, если целевой рынок находится за границей или что бы упростить взаимодействие с западными контрагентами.
А тут вот, оказывается, что получается…
Nakosika
Маски шоу приедут и конфискуют сервера «для экспертизы» — в самом простом случае. Почитайте про Дурова что бывает в умеренном. Про крайний случай можете додумать.
GreenStore
Дуров — та еще балаболка.
Какое отношение имеет «конфискация» серверов к «отжатию»? Чтобы отжать, нужно сменить руководство, а не выносить серверы. Кроме того, в ИТ руководитель уводит с собой команду и нет больше старой компании…
Вот тут тоже кричали: «захват, захват», а читаешь комментарии, и не «все так однозначно» получается.
avost
Руководство-то зачем? Владельцев. В общем случае это не одно и то же.
Тут ИТ ни чем не отличается от других компаний, кроме, разве что, каких-нибудь космодромов и автоконцернов с тяжёлой недвижимостью. Команде нет особого резона уходить за руководителем за исключением, разве что мелких стартапов. Но их ни кто и отжимать не будет.
tundrawolf_kiba
Сейчас вот почитал историю владения «Вконтактом», и пришел к выводу, что Павел упустил владение созданной сетью практически с самого начала. Уже в 2007 году он владел только 20% акций, то есть, как я понимаю, не было даже блокирующего пакета.К тому времени, как контакт «отжали» — у него было 12 процентов акций, а у Mail.ru, и продающегося ему UCP — 88%. Поэтому по моему мнению главная ошибка была совершена еще в тот момент, когда он не оставил себе меньше контрольного пакета при основании своей фирмы. Если в чем-то неправ — прошу поправить.
Nakosika
Отношение простое — выносом серверов сливают конкурента или оказывают давление пока не отдашь/продашь по дешевке.
Уход команды — это в идеальном случае, а обычно не так. К тому же у бандитов незаменимых нет, в замен нескольких ушедших легко нанять других, которые не в курсе или которые не принципиальные.
Regis
Как жаль, что горе-переводчика минусануть нельзя :(