Главная » Хабрахабр » Пишем Java-френдли Kotlin-код

Пишем Java-френдли Kotlin-код

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

На нашей конференции Mobius Сергей Рябов рассказал, как писать на Kotlin такой код, к которому будет комфортно обращаться из Java. Разумеется, это не означает «всё плохо и использовать Kotlin вместе с Java не надо», а означает, что стоит знать о нюансах и учитывать их. И доклад так понравился зрителям, что мы не только решили разместить видеозапись, но и сделали для Хабра текстовую версию:

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

Зачастую при добавлении в проект Kotlin можно увидеть, как вот это…

compile 'rxbinding:x.y.x' compile 'rxbinding-appcompat-v7:x.y.x' compile 'rxbinding-design:x.y.x' compile 'autodispose:x.y.z'
compile 'autodispose-android:x.y.z'
compile 'autodispose-android-archcomponents:x.y.z'

… превращается в это:

compile 'rxbinding:x.y.x'
compile 'rxbinding-kotlin:x.y.x'
compile 'rxbinding-appcompat-v7:x.y.x' compile 'rxbinding-appcompat-v7-kotlin:x.y.x' compile 'rxbinding-design:x.y.x'
compile 'rxbinding-design-kotlin:x.y.x' compile 'autodispose:x.y.z'
compile 'autodispose-kotlin:x.y.z'
compile 'autodispose-android:x.y.z'
compile 'autodispose-android-kotlin:x.y.z'
compile 'autodispose-android-archcomponents:x.y.z' compile 'autodispose-android-archcomponents-kotlin:x.y.z'

Специфика последней пары лет: самые популярные библиотеки обзаводятся «обёртками» для того, чтобы можно было использовать их из Kotlin более идиоматично.

И это круто, это притягивает нас к Kotlin, но возникает вопрос. Если вы писали на Kotlin, то знаете, что есть классные extension-функции, inline-функции, лямбда-выражения, которые доступны из Java 6. Если принять во внимание все перечисленные фичи, то почему бы тогда просто не писать библиотеки на Kotlin? Одна из самых больших, самых разрекламированных фич языка — interoperability с Java. Они все будут отлично работать из коробки с Java, и не нужно будет поддерживать все эти обёртки, все будут счастливы и довольны.

Но, конечно, на практике не всё так радужно, как в рекламных проспектах, всегда есть «приписочка мелким шрифтом», есть острые грани на стыке Kotlin и Java, и сегодня мы об этом немного поговорим.

Острые грани

Начнём с различий. Например, в курсе ли вы, что в Kotlin нет ключевых слов volatile, synchronized, strictfp, transient? Они заменены одноимёнными аннотациями, находящимися в пакете kotlin.jvm. Так вот, о содержимом этого пакета и пойдёт большая часть разговора.

Она позволяет вам в вашем приложении везде использовать её, а всё, куда вы хотите отправить логи (в logcat, или на ваш сервер для анализа, или в crash reporting, и так далее), оборачивается в плагинчики. Есть Timber — такая библиотечка-абстракция над логгерами от небезызвестного Жеки Вартанова.

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

object Analytics fun addPlugins(plugs: List<Plugin>) {} fun getPlugins(): List<Plugin> {} } interface Plugin { fun init() fun send(event: Event) fun close() } data class Event( val name: String, val context: Map<String, Any> = emptyMap() )

Берём тот же самый паттерн построения, у нас одна точка входа — это Analytics. Мы можем посылать туда ивенты, добавлять плагины и смотреть, что у нас там уже добавлено.

Plugin — интерфейс плагина, который абстрагирует какое-то конкретное аналитическое API.

Здесь доклад не про то, стоит ли использовать синглтоны, поэтому давайте не будем разводить холивар, а будем смотреть, как это все дело причесать. И, собственно, класс Event, содержащий ключ и наши атрибуты, которые мы отправляем.

Вот пример использования нашей библиотеки в Kotlin: Теперь немножко погрузимся.

private fun useAnalytics() { Analytics.send(Event("only_name_event")) val props = mapOf( USER_ID to 1235, "my_custom_attr" to true ) Analytics.send(Event("custom_event", props)) val hasPlugins = Analytics.hasPlugins Analytics.addPlugin(EMPTY_PLUGIN) // dry-run Analytics.addPlugins(listOf(LoggerPlugin("ALog"), SegmentPlugin))) val plugins = Analytics.getPlugins() // ... }

В принципе, выглядит так, как и ожидается. Одна точка входа, методы вызываются а-ля статики. Event без параметров, event с атрибутами. Проверяем, есть ли у нас плагины, запихиваем туда пустой плагин для того, чтобы просто какой-то «dry run»-прогон сделать. Либо добавляем несколько других плагинов, выводим их, ну и так далее. В общем, стандартные юзкейсы, надеюсь, всё пока понятно.

А теперь посмотрим, что происходит в Java, когда мы делаем то же самое:

private static void useAnalytics() { Analytics.INSTANCE.send(new Event("only_name_event", Collections.emptyMap())); final Map<String, Object> props = new HashMap<>(); props.put(USER_ID, 1235); props.put("my_custom_attr", true); Analytics.INSTANCE.send(new Event("custom_event", props)); boolean hasPlugins = Analytics.INSTANCE.getHasPlugins(); Analytics.INSTANCE.addPlugin(Analytics.INSTANCE.getEMPTY_PLUGIN()); // dry-run final List<EmptyPlugin> pluginsToSet = Arrays.asList(new LoggerPlugin("ALog"), new SegmentPlugin()); // ... }

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

Удаляем из второй строчки параметр Colletions.emptyMap(), и вылезает ошибка компилятора. Начнём с Event. С чем же это связано?

data class Event( val name: String, val context: Map<String, Any> = emptyMap()
)

Наш конструктор имеет дефолтный параметр, в который мы передаём значение. Мы приходим из Java в Kotlin, логично предположить, что наличие дефолтного параметра генерирует два конструктора: один полный с двумя параметрами, и один частичный, у которого можно задать только name. Очевидно, компилятор так не считает. Давайте посмотрим, почему он считает, что мы не правы.

В Android Studio и IntelliJ IDEA он находится в меню Tools — Kotlin — Show Kotlin Bytecode. Наш основной инструмент для анализа всех перипетий того, как Kotlin превращается в JVM-байткод — Kotlin Bytecode Viewer. Можно просто нажать Cmd+Shift+A и вписать в строку поиска Kotlin Bytecode.

Я не ожидаю от вас отличного знания байткода, и, самое главное, разработчики IDE тоже не ожидают. Здесь, как ни удивительно, мы видим байткод того, во что превращается наш Kotlin-класс. Поэтому они сделали кнопочку Decompile.

После её нажатия мы видим такой примерно неплохой Java-код:

public final class Event { @NotNull private final String name; @NotNull private final Map context; @NotNull public final String getName() { return this.name; } @NotNull public final Map getContext() { return this.context; } public Event(@NotNull String name, @NotNull Map context) { Intrinsics.checkParameterIsNotNull(name, "name"); Intrinsics.checkParameterIsNotNull(context, "context"); super(); this.name = name; this.context = context; } // $FF: Synthetic method public Event(String var1, Map var2, int var3, DefaultConstructorMarker var4) { if ((var3 & 2) != 0) { var2 = MapsKt.emptyMap(); } // ... }

Видим наши поля, геттеры, ожидаемый конструктор с двумя параметрами name и context, всё происходит нормально. А ниже мы видим второй конструктор, и вот он с неожиданной сигнатурой: не с одним параметром, а почему-то с четырьмя.

Начав разбираться, мы поймём, что DefaultConstructorMarker — это приватный класс из стандартной библиотеки Kotlin, добавленный сюда, чтобы не было коллизий с нами написанными конструкторами, т. Тут можно смутиться, а можно залезть немного глубже и покопаться. мы не можем задать руками параметр типа DefaultConstructorMarker. к. В данном случае, если битовая маска совпадает с двойкой, мы знаем, что var2 не задан, наши атрибуты не заданы, и мы используем дефолтное значение. А интересное всего int var3 — битовая маска того, какие дефолтные значения мы должны использовать.

Для этого есть чудо-аннотация @JvmOverloads из того пакета, о котором я уже говорил. Как мы можем исправить ситуацию? Мы должны навесить её на конструктор.

data class Event @JvmOverloads constructor( val name: String, val context: Map<String, Any> = emptyMap()
)

И что она сделает? Обратимся к тому же инструменту. Теперь там видим и наш полный конструктор, и конструктор с DefaultConstructorMarker, и, о чудо, конструктор с одним параметром, который доступен теперь из Java:

@JvmOverloads
public Event(@NotNull String name) { this.name, (Map)null, 2, (DefaultConstructorMarker)null);
}

И, как видите, он делегирует всю работу с дефолтными параметрами в тот наш конструктор с битовыми масками. Таким образом, мы не плодим информации о том, что за дефолтное значение нам нужно засунуть туда, мы просто делегируем всё в один конструктор. Славно. Проверяем, что у нас получилось со стороны Java: компилятор рад и не возмущается.

Нам не нравится этот INSTANCE, который в IDEA мозолит фиолетовым цветом. Давайте посмотрим, что нам не нравится дальше. Не люблю фиолетовый цвет 🙂

Снова глянем в байткод. Давайте проверим, за счёт чего так получается.

Выделим, например, функцию init и убедимся, что init действительно сгенерирован не статическим.

Но мы можем зафорсить генерацию всех этих методов как статическую. То есть, как ни крути, нам нужно работать с инстансом этого класса и вызывать у него эти методы. Давайте добавим её к функциям init и send и проверим, что теперь думает компилятор на этот счёт. Для этого есть своя чудесная анннотация @JvmStatic.

Убедимся в этом в Java-коде. Мы видим, что к public final init() добавилось ключевое слово static, и мы избавили себя от работы с INSTANCE.

Это можно поправить: нажать Alt+Enter, выбрать «Cleanup Code», и вуаля, INSTANCE исчезает, всё выглядит примерно так же, как было в Kotlin: Компилятор теперь подсказывает нам, что мы вызываем статический метод из контекста INSTANCE.

Analytics.send(new Event("only_name_event"));

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

Сами поля (например, plugins) генерируются как статические. И комментарий: если методы у нас — очевидно методы instance, то, например, с пропертями не всё так очевидно. Поэтому для пропертей вам тоже нужно добавлять эту аннотацию, чтобы зафорсить сеттеры и геттеры, как статические. А вот геттеры и сеттеры работают как методы инстанса. Например, видим переменную isInited, добавляем ей аннотацию @JvmStatic, и теперь мы видим в Kotlin Bytecode Viewer, что метод isInited() стал статическим, всё отлично.

Теперь пойдем в Java-код, «за-clean-up-им» его, и всё выглядит примерно как в Kotlin, за исключением точек с запятой и слова new — ну, от них вы не избавитесь.

public static void useAnalytics() { Analytics.send(new Event("only_name_event")); final Map<String, Object> props = new HashMap<>(); props.put(USER_ID, 1235); props.put("my_custom_attr", true); Analytics.send(new Event("custom_event", props)); boolean hasPlugins = Analytics.getHasPlugins(); Analytics.addPlugin(Analytics.INSTANCE.getEMPTY_PLUGIN()); // dry-run // ... }

Следующий шаг: мы видим этот стрёмно названный геттер getHasPlugins с двумя префиксами сразу. Я, конечно, не большой знаток английского языка, но мне кажется, здесь что-то другое подразумевалось. Почему так происходит?

Это значит, что геттеры в общем случае будут с префиксами get, сеттеры с префиксами set. Как знают плотно общавшиеся с Kotlin, для пропертей названия геттеров и сеттеров генерируются по правилам JavaBeans. Это можно увидеть на примере вышеупомянутного поля isInited. Но есть одно исключение: если у вас булево поле и в его названии есть префикс is, то и геттер будет с префиксом is.

isPlugins не совсем удовлетворяло бы тому, что мы хотим семантически показать именем. К сожалению, далеко не всегда булевы поля должны называться через is. Как же нам быть?

Аннотация @JvmName позволяет задать любое имя, которое мы хотим (естественно, поддерживаемое Java). А быть нам несложно, для этого есть своя аннотация (как вы уже поняли, я сегодня буду часто это повторять). Добавим её:

@JvmStatic val hasPlugins @JvmName("hasPlugin") get() = plugins.isNotEmpty()

Проверим, что у нас получилось в Java: метода getHasPlugins больше нет, а вот hasPlugins вполне себе есть. Это решило нашу проблему, опять-таки, одной аннотацией. Сейчас всё аннотациями порешаем!

С чем это связано? Как видите, здесь мы навесили аннотацию прямо на геттер. Если перенести аннотацию на сам val hasPlugins, то компилятор не поймёт, к чему её применять. С тем, что под пропертёй лежит много всего, и непонятно, к чему применяется @JvmName.

Можно указать целью геттер, файл целиком, параметр, делегат, поле, проперти, receiver extension-функции, сеттер и параметр сеттера. Однако в Kotlin есть и возможность специфицировать место применения аннотации прямо в ней. И если сделать вот так, будет тот же самый эффект, как когда мы навешивали аннотацию на get: В нашем случае интересен геттер.

@get:JvmName("hasPlugins") @JvmStatic val hasPlugins get() = plugins.isNotEmpty()

Соответственно, если у вас нет кастомного геттера, то вы можете навесить прямо на вашу проперти, и все будет ОK.

INSTANCE.getEMPTY_PLUGIN()». Следующий момент, который немного нас смущает — это «Analytics. Ответ примерно такой же, но сперва небольшое введение. Тут дело уже даже не в английском, а просто: ПОЧЕМУ?

Если константу вы определяете как примитивный тип либо как String, и ещё она внутри объекта, то вы можете использовать ключевое слово const, и тогда не будет сгенерировано геттеров-сеттеров и прочего. Для того, чтобы сделать из поля константу, у вас есть два пути. Это будет обычная константа — private final static — и она будет заинлайнена, то есть абсолютно обычная Java-штука.

Вот у нас есть val EMPTY_PLUGIN = EmptyPlugin(), по нему, очевидно сгенерировался тот страшный геттер. Но если вы хотите сделать константу из объекта, который отличен от строки, то у вас не выйдет использовать для этого слово const. Значит, старые решения не подойдут, ищем новые. Мы можем аннотацией @JvmName переименовать, убрать этот префикс get, но всё равно это останется методом — со скобками.

Поставим её перед val EMPTY_PLUGIN и проверим, что всё действительно так. И тут для этого аннотация @JvmField, которая говорит: «Не хочу здесь геттеров, не хочу сеттеров, сделай мне поле».

Мы сейчас стоим на EMPTY_PLUGIN, и вы видите, что здесь какая-то инициализация написана в конструкторе. Kotlin Bytecode Viewer показывает выделенным тот кусок, на котором вы сейчас стоите в файле. А если нажать decompile, видим, что появилось «public static final EmptyPlugin EMPTY_PLUGIN», это именно то, чего мы и добивались. Дело в том, что геттера-то больше нет и доступ к нему только на запись идёт. Проверяем, что всех всё радует, в частности, компилятор. Славно. Самый главный, кого нужно ублажить — это компилятор.

Generics

Давайте немного оторвёмся от кода и посмотрим на дженерики. Это довольно острая тема. Или скользкая, кому что больше не нравится. В Java есть свои сложности, но Kotlin отличается. В первую очередь нас волнует вариантность. Что это такое?

Вот у нас есть классы Animal и Dog с вполне очевидной связью: Dog — это подтип, Animal — надтип, стрелка идёт от подтипа. Вариантность — это способ переноса информации о иерархии типов с базовых типов на производные, например, на контейнеры или на дженерики.

Давайте рассмотрим несколько кейсов. А какая связь будет у их производных?

Определять, что есть надтип, а что есть подтип, мы будем, руководствуясь правилом подстановки Барбары Лисков. Первый — это Iterator. Сформулировать его можно так:«подтип должен требовать не больше, а предоставлять не меньше».

Если мы где-то принимаем Iterator, мы вполне можем засунуть туда Iterator, и из метода next() получим Animal, потому что собака — это тоже Animal. В нашей ситуации единственное, что делает Iterator — отдаёт нам типизированные объекты, например, Animal. Мы предоставляем не меньше, а больше, потому что собака — это подтип.

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

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

Это значит, что зависимость у нас меняется. Таким образом, здесь мы уже не предоставляем, а требуем, и требовать мы должны не больше. И такие типы называются контравариантными. «Не больше» у нас Animal (Animal меньше, чем собака).

Поэтому в данном случае мы нарушаем одно из правил, на запись требуем больше (собаку, а не животное). Есть еще третий кейс — например, ArrayList, из которого мы и читаем, и пишем. Такие типы никак не связаны отношением, и они называются инвариантными.

5 (где появились дженерики), по умолчанию сделали массивы ковариантными. Так вот, в Java, когда её проектировали ещё до версии 1. Всё свалится у вас. Это значит, что вы можете присвоить массиву объектов массив строк, потом передать его куда-то в метод, где нужен массив объектов, и попробовать туда запихнуть объект, хотя это массив строк.

Научившись на горьком опыте, что так делать нельзя, при проектировании дженериков они решили «коллекции мы сделаем инвариантными, ничего не будем с ними делать».

И в итоге получается, что в такой, казалось бы, очевидной штуке всё должно быть ок, а на самом деле не ок:

// Java
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs;

Но нам же нужно как-то определять, что всё-таки мы можем: если мы только читаем из этого листа, почему бы не сделать возможной передачу сюда листа собак? Поэтому есть возможность с помощью wildcard охарактеризовать то, какая же вариантность будет у этого типа:

List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;

Как вы видите, вариантность эта указывается на месте использования, там, где мы присваиваем собак. Поэтому это называется use-site variance.

Негативная сторона в том, что вы должны везде, где используете ваш API, указывать эти страшные wildcard, и это всё очень плодится в коде. Какие негативные стороны у этого есть? А вот в Kotlin почему-то такая штука работает из коробки, и не нужно ничего указывать:

val dogs: List<Dog> = ArrayList()
val animals: List<Animal> = dogs

С чем это связано? С тем, что листы на самом деле разные. List в Java подразумевает запись, а в Kotlin он immutable, не подразумевает. Поэтому, в принципе, мы можем сразу сказать, что отсюда только читаем, поэтому мы можем быть ковариантными. И это задаётся именно в объявлении типа ключевым словом out, заменяющим wildcard:

interface List<out E> : Collection<E>

Это называется declaration-site variance. Таким образом, мы в одном месте всё указали, и там, где используем, мы больше не затрагиваем эту тему. И это ништяк.

Снова к коду

Поехали обратно в наши пучины. Вот у нас есть метод addPlugins, он принимает List:

@JvmStatic fun addPlugins (plugs: List<Plugin>) { plugs.forEach { addPlugin(it) }
} А мы передаём ему, как видно, List<EmptyPlugin>, ну, просто закастили всё это дело: <source lang="java">
final List<EmptyPlugin> pluginsToSet = Arrays.asList(new LoggerPlugin("Alog"), new SegmentPlugin());

Благодаря тому, что List в Kotlin ковариантный, мы можем без проблем передать сюда лист наследников плагина. Всё сработает, компилятор не возражает. Но из-за того, что у нас есть declaration-site variance, где мы всё указали, мы не можем тогда на этапе использования контролировать связь с Java. А что же будет, если мы реально хотим туда лист Plugin, не хотим туда никаких наследников? Никаких модификаторов для этого нет, но есть что? Правильно, есть аннотация. А аннотация называется @JvmSuppressWildcards, то есть по умолчанию мы считаем, что здесь тип с wildcard, тип ковариантный.

@JvmStatic fun addPlugins(plugs: List<@JvmSuppressWildcards Plugin>) { plugs.forEach { addPlugin(it) }
}

Говоря SuppressWildcards, мы suppress'им все эти вопросики, и наша сигнатура фактически меняется. Даже более того, я покажу, как всё выглядит в байткоде:

Вот наш метод. Удалю пока из кода аннотацию. И у вас в байткоде нет никакой информации о том, что же там за вопросики-то были, ну и вообще дженерики. Вы наверняка знаете, что существует type erasure. Но компилятор за этим следит и подписывает в комментариях к байткоду: а это у нас с вопросиком тип.

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

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

Теперь обратная ситуация. Мы разобрали ковариантные типы.

Очевидно предположить, что когда этот лист возвращается из getPlugins, он тоже будет с вопросом. Мы считаем, что List у нас с вопросом. Это значит, что мы не сможем в него записать, потому что тип ковариантный, а не контравариантный. А что это значит? Давайте взглянем, что происходит в Java.

final List<Plugin> plugins = Analytics.getPlugins();
displayPlugins(plugins); Analytics.getPlugins().add(new EmptyPlugin());

Никто не возмущается, что в последней строчке мы что-то записываем, а это значит, что кто-то здесь не прав. Если заглянем в байткод, то убедимся в верности своих подозрений. Мы никаких аннотаций не навешивали, а тип почему-то без вопроса.

Kotlin постулирует себя, как язык прагматичный, поэтому, когда всё это проектировали, собирали статистику, как вообще используются wildcards в Java. Сюрприз основан вот на чём. Ну полезно везде, где мы хотим List, иметь возможность засунуть туда лист любого наследника от Plugin. Оказалось, что на вход чаще всего вариантность разрешают, то есть делают типы ковариантными. А вот там, где мы возвращаем, наоборот, мы хотим иметь чистые типы: как есть лист Plugin, так он и будет возвращён.

Это кажется немного контринтуитивно, зато порождает в вашем коде меньше чудных аннотаций, потому что это наиболее частый usecase, и если вы не пользуетесь какими-то приколами, всё будет работать из коробки. И здесь мы воочию видим пример этого.

И также мы не хотим, чтобы это можно было сделать из Java. Но в этом случае мы видим, что такая ситуация не для нас, потому что мы не хотим, чтобы туда можно было что-то записать. Поэтому мы собираемся зафорсить, чтобы этот метод возвращал List с wildcard. В Kotlin здесь List — это immutable-тип, и мы туда ничего не можем записать, а пришел клиент нашей библиотеки из Java и напихал туда всё подряд — кому это понравится? Добавив аннотацию @JvmWildcard мы говорим: сгенерируй нам тип с вопросом, всё достаточно просто. И мы можем это сделать понятно, как. Java говорит «что ты делаешь?»: Теперь посмотрим, что происходит в Java в этом месте.

extends Plugin>, но она всё равно говорит «что ты делаешь?» И, в принципе, эта ситуация нас пока что устраивает. Мы можем здесь даже привести к правильному типу List<? И всё сработает, потому что там действительно ArrayList и он знает, что туда можно записывать. Но найдётся script kiddie, который скажет «я же видел исходники, это же опенсорс, я знаю, что там ArrayList, и я вас похачу».

((ArrayList<Plugin>) Analytics.getPlugins()).add(new EmptyPlugin());

Поэтому, конечно, клёво аннотации навешивать, но всё равно нужно использовать defensive-копирование, которое давным-давно известно. Сорян, без него никуда, если вы хотите, чтобы script kiddies вас не похачили.

@JvmStatic fun getPlugins(): List<@JvmWildcard Plugin> = plugin.toImmutableList()

Добавлю только, что аннотацию @JvmSuppressWildcard можно навешивать как на параметр, тогда только он будет об этом знать, так и на функцию, так и на весь класс, тогда расширяется его зона действия.

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

Как хорошие пацаны, мы репортим его исключение: Мы хотим реализовать плагин на стороне Java.

@Override public void send(@NotNull Event event) throws IOException

Здесь же всё видно:

interface Plugin { fun init() /** @throws IOException if sending failed */ fun send(event: Event) // ...
}

В Kotlin же нет checked exception. И мы говорим в документации: сюда можно кидать. Ну мы и кидаем, кидаем, кидаем. А Java не нравится почему-то. Говорит: «а нет Throws почему-то в вашей сигнатуре, месье»:

Ну, вы знаете ответ… А как тут добавить-то, тут же Kotlin?

Она меняет throws-часть в сигнатуре метода. Есть аннотация @Throws, которая именно это и делает. Мы говорим, что можем кинуть сюда IOExсeption:

open class EmptyPlugin : Plugin { @Throws(IOException::class) override fun send(event: Event) {} // ...
}

И ещё добавим это дело заодно в интерфейс:

interface Plugin { fun init() /** @throws IOException if sending failed */ @Throws(IOException::class) fun send(event: Event) // ...
}

И теперь что? Теперь наш плагин, написанный на Java, где у нас есть информация об exception, всем доволен. Всё работает, компилируется. В принципе, с аннотациями на этом более-менее всё, но есть ещё два нюанса того, как использовать @JvmName. Один интересный.

А вот здесь… Мы все эти аннотации добавляли для того, чтобы в Java было красиво.

package util fun List<Int>.printReversedSum() { println(this.foldRight(0) { it, acc -> it + acc })
} @JvmName("printReversedConcatenation")
fun List<String>.printReversedSum() { println(this.foldRight(StringBuilder()) { it, acc -> acc.append(it) })
}

Предположим, на Java нам здесь всё равно, уберём аннотацию. Опачки, теперь IDE показывает ошибку в обеих функциях. Как вы считаете, с чем это связано? Да, без аннотации они генерируются с одинаковыми именами, но здесь же написано, что один на List, другой на List. Верно, type erasure. Мы даже можем проверить это дело:

И вот без этой аннотации мы постараемся сгенерировать printReversedSum от List, а ниже ещё один тоже от List. Вы уже в курсе, как я понял, что все top-level функции генерируются в статическом контекcте. Поэтому это единственный случай, когда аннотации из пакета kotlin.jvm нужны не для того, чтобы в Java было хорошо и удобно, а для того, чтобы ваш Kotlin собрался. Потому что Kotlin-компилятор знает о дженериках, а Java-байткод не знает. Задаём новое имя — раз работаем со строками, то используем concatenation — и всё работает хорошо, теперь всё компилируется.

Он связан вот с чем. И второй юзкейс. У нас есть extension-функция reverse.

inline fun String.reverse() = StringBuilder(this).reverse().toString() inline fun <reified T> reversedClassName() = T::class.java.simpleName.reverse() inline fun <T> Iterable<T>.forEachReversed(action: (T) -> Unit) { for (element in this.reversed()) action(element)
}

Этот reverse компилируется в статический метод класса, который называется ReverserKt.

private static void useUtils() { System.out.println(ReverserKt.reverse("Test")); SumsKt.printReversedSum(asList(1, 2, 3, 4, 5)); SumsKt.printReversedConcatenation(asList("1", "2", "3", "4", "5"));
}

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

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

@file:Suppress("NOTHING_TO_INLINE")
@file:JvmName("ReverserUtils")

Теперь компилятору в Java не нравится ReverserKt, но это ожидаемо, заменяем на ReverserUtils и все довольны. И такой «юзкейс 2.1» — частый случай, когда вы хотите методы нескольких ваших top-level файлов собрать под одним классом, под одним фасадом. Например, про вы не хотите, чтобы методы вышеприведённого sums.kt вызывались из SumsKt, а хотите, чтобы это всё тоже было про reversing и дёргалось из ReverserUtils. Тогда добавляем и туда эту чудную аннотацию @JvmName, пишем «ReverserUtils», в принципе, всё ок, можно даже попробовать это дело скомпилировать, но нет.

Что нужно сделать? Хотя заранее среда не предупреждает, но при попытке компиляции нам скажут, что «вы хотите сгенерировать два класса в одном пакете с один именем, ата-та». Добавить последнюю в этом пакете аннотацию @JvmMultifileClass, говорящую, что содержимое нескольких файлов будет превращаться в один класс, то есть для этого всего будет один фасад.

С аннотациями закончили! Добавляем в обоих случаях "@file:JvmMultifileClass", и можно заменять SumsKt на ReverserUtils, все довольны — поверьте мне.

В принципе, уже из их названий понятно, для чего используется каждая. Мы поговорили с вами об этом пакете, обо всех аннотациях. Есть хитрые кейсы, когда нужно, например, @JvmName использовать даже просто в Kotlin.

Kotlin-специфичное

Но, скорее всего это не всё, что вы хотели бы узнать. Ещё важно отметить, как работать с Kotlin-специфичными вещами.

Они в Kotlin инлайнятся и, казалось бы, а будут ли они вообще доступны из Java в байткоде? Например, inline-функции. Хотя если вы пишете, например, Kotlin-only проект, это не совсем хорошо сказывается на вашем dex count limit. Оказывается, будут, всё будет хорошо, и методы реально доступны для Java. Потому что в Kotlin они не нужны, а реально в байткоде будут.

Такие параметры специфичны для Kotlin, они доступны только для инлайн-функций и позволяют воротить такие хаки, которые в Java с рефлексией недоступны. Дальше надо отметить Reified type parameters. Так как это Kotlin-only штука, она доступна только для Kotlin, и в Java вы не сможете использовать функции с reified, к сожалению.

Class. java.lang. Давайте посмотрим пример. Если мы хотим немного порефлексировать, а библиотека наша и для Java, то и её надо поддержать. Есть у нас такой «свой Retrofit», быстро написанный на коленке (я вообще не понимаю, чего парни так долго писали):

class Retrofit private constructor( val baseUrl: String, val client: Client
) { fun <T : Any> create(service: Class<T>): T {...} fun <T : Any> create(service: KClass<T>): T { return create(service.java) }
}

Есть метод, который работает с классом Java, есть метод, который работает с котлиновским KClass, вам не нужно делать две разные реализации, вы можете использовать extension-проперти, которые из KClass достают Class, из Class достают KClass (он называется Kotlin, в принципе очевидно).

В Kotlin-коде вы не передаёте KClass, вы пишете с использованием Reified-типов, поэтому лучше метод переделать на вот такой: Это всё будет работать, но это немного неидиоматично.

inline fun <reified T : Any> create(): T { return create(T::class.java.java)

Всё шикардос. Теперь пойдём в Kotlin и посмотрим, как эта штука используется. Там val api = retrofit.create(Api::class) превратилось в val api = retrofit.create<Api>(), никаких явных ::class не вылезает. Это типичное использование Reified-функции, и всё будет супер-пупер.

Если ваша функция возвращает Unit, то она прекрасно компилируется в функцию, которая возвращает void в Java, и обратно. Unit. Но всё это заканчивается в том месте, где у вас лямбды начинают возвращать юниты. Вы можете работать с этим взаимозаменяемо. Если кто-то работал со Scala, то в Scala есть вагон и маленькая тележка интерфейсов, которые возвращают какие-то значения, и такой же вагон с тележкой интерфейсов, которые не возвращают ничего, то есть с void.

В Kotlin есть только 22 интерфейса, которые принимают разный набор параметров и что-то возвращают. А в Kotlin этого нет. И это накладывает свои ограничения. Таким образом, лямбда, которая возвращает Unit, будет возвращать не void, а Unit. Вот, посмотрим на неё в этом фрагменте кода. Как выглядит лямбда, которая возвращает Unit? Познакомимся.

inlint fun <T> Iterable<T>.forEachReversed(action: (T) -> Unit) { for (element in this.reversed()) action(element)
}

Использование её из Kotlin: все хорошо, мы используем даже method reference, если можем, и читается отлично, глаза не мозолит.

private fun useMisc() { listOf(1, 2, 3, 4).forEachReversed(::println) println(reversedClassName<String>())
}

Что происходит в Java? В Java происходит вот такая байда:

private static void useMisc() { final List<Integer> list = asList(1, 2, 3, 4); ReverserUtils.forEachReversed(list, integer -> { System.out.println(integer); return Unit.INSTANCE; });

Из-за того, что мы должны возвращать что-то здесь. Это как Void с большой буквы, мы не можем просто взять и забить на него. Мы не можем использовать здесь метод референсы, которые возвращают void, к сожалению. И это, наверное, пока первая штука, которая реально мозолит глаза после всех наших манипуляций с аннотациями. К сожалению, вам придется возвращать инстанс Unit отсюда. Можно null, всё равно он никому не нужен. В смысле, возвращаемое значение никому не нужно.

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

А точнее, internal visibility. А теперь интересное: visibility. Зато есть internal. Вы наверняка знаете, что в Kotlin нет package private, если вы пишете без каких-либо модификаторов, это будет public. В Retrofit у нас есть internal-метод validate. Internal — это такая хитрая штука, что мы сейчас на неё даже посмотрим.

internal fun validate(): Retrofit { println("!!!!!! internal fun validate() was called !!!!!!") return this
}

Он не может быть вызван из Kotlin, и это понятно. Что происходит с Java? Можем ли мы вызвать validate? Возможно, для вас не секрет, что internal превращается в public. Если вы не верите мне, поверьте Kotlin bytecode viewer.

Если у кого-то форматирование на 80 символов сделано, то такой метод может даже не влезть в одну строчку. Это действительно public, но с такой страшной сигнатурой, которая намекает человеку, что это, наверное, не совсем так было задумано, что в публичное API пролезает вот такая портянка.

В Java у нас сейчас так:

final Api api = retrofit .validate$production_sources_for_module_library_main() .create(Api.class); api.sendMessage("Hello from Java");
}

Давайте попробуем скомпилировать это дело. Так, по крайней мере, это не скомпилируется, уже неплохо. На этом можно было бы остановиться, но let me explain this to you. Что, если я сделаю вот так?

final Api api = retrofit .validate$library() .create(Api.class); api.sendMessage("Hello from Java");
}

Тогда компилируется. И возникает вопрос «Почему так?» Что я могу сказать… MAGIC!

И если script kiddie будет вооружён Kotlin Bytecode Viewer, то будет плохо. Поэтому очень важно, если вы засовываете что-то критическое в internal, это плохо, потому что это просочится в ваше публичное API. Не используйте ничего очень важного в методах с internal visibility.

Чтобы было комфортней работать с байткодом и читать его, рекомендую доклад от Жени Вартанова, есть бесплатное видео, несмотря на то, что это с мероприятия SkillsMatter. Если вам хочется еще немного радости, то рекомендую две вещи. Очень круто.

Там всё классно написано, что-то неактуально сейчас, но в целом весьма доходчиво. И довольно старая серия из трёх статей от Кристофа Бейлса про то, во что превращаются разные Kotlin-фичи. Всё тоже с Kotlin bytecode viewer и всё такое.

Спасибо!

Уже известная информация о программе — на сайте, и билеты можно приобрести там же.
Если доклад понравился, обратите внимание: 8-9 декабря в Москве состоится новый Mobius, и там тоже будет много интересного.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Фулстеки — это вечные мидлы. Не идите по этому пути, если не хотите страдать

У меня появилась идея фикс — быть разработчиком, который может всё. Когда я только начал учиться кодить, я поверил старым мудрым засранцам с их мантрой «язык программирования не важен». Но эта затея с треском провалилась. Парнем, который переносит опыт использования ...

«Я просто энтузиаст проекта и пользователь языка Dart» — интервью с Ari Lerner, автором знаменитой ng-book

Что самое важное в обучении, что такое «hallway chat» и вообще, при чём тут Dart и Flutter? Как написать девять книг по совершенно разным технологиям, включая Angular, Vue, React, React Native и другим? Какой будет дальнейшая книга, что автор думает ...