Это продолжение публикации. Первую часть можно посмотреть тут
Содержание:
Циклы
When
Делегаты
Object и companion object
lateinit свойства
coroutines
Выводы
Циклы:
В языке Kotlin отсутствует классический for с тремя частями, как в Java. Кому-то это может показаться проблемой, но если подробнее посмотреть все случаи использования такого цикла, то можно увидеть, что по большей части он применяется как раз для перебора значений. На смену ему в Kotlin есть упрощенная конструкция.
//Kotlin
fun rangeLoop() {
for (i in 1..10) {
println(i)
}
}
1..10 тут это диапазон по которому происходит итерация. Компилятор Kotlin достаточно умный, он понимает что мы собираемся в данном случае делать и поэтому убирает весь лишний оверхед. Код компилируется в обычный цикл while с переменной счетчика цикла. Никаких итераторов, никакого оверхеда, все достаточно компактно.
//Java
public static final void rangeLoop() {
int i = 1;
byte var1 = 10;
if(i <= var1) {
while(true) {
System.out.println(i);
if(i == var1) {
break;
}
++i;
}
}
}
Похожий цикл по массиву (который в Kotlin записывается в виде Array<*>), компилируется аналогичным образом в цикл for.
//Kotlin
fun arrayLoop(x: Array<String>) {
for (s in x) {
println(s)
}
}
//Java
public static final void arrayLoop(@NotNull String[] x) {
Intrinsics.checkParameterIsNotNull(x, "x");
for(int var2 = 0; var2 < x.length; ++var2) {
String s = x[var2];
System.out.println(s);
}
}
Немного другая ситуация возникает, когда происходит перебор элементов из списка:
//Kotlin
fun listLoop(x: List<String>) {
for (s in x) {
println(s)
}
}
В этом случае приходится использовать итератор:
//Java
public static final void listLoop(@NotNull List x) {
Intrinsics.checkParameterIsNotNull(x, "x");
Iterator var2 = x.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
System.out.println(s);
}
}
Таким образом, в зависимости от того по каким элементам происходит перебор, компилятор Kotlin сам выбирает самый эффективный способ преобразовать цикл в байткод.
Ниже приведено сравнение производительности для циклов с аналогичными решениями в Java:
Циклы
Как видно разница между Kotlin и Java минимальна. Байткод получается очень близким к тому что генерирует javac. По словам разработчиков они еще планируют улучшить это в следующих версиях Kotlin, чтобы результирующий байткод был максимально близок к тем паттернам, которые генерирует javac.
When
When — это аналог switch из Java, только с большей функциональностью. Рассмотрим ниже несколько примеров и то, во что они компилируются:
/Kotlin
fun tableWhen(x: Int): String = when(x) {
0 -> "zero"
1 -> "one"
else -> "many"
}
Для такого простого случая результирующий код компилируется в обычный switch, тут никакой магии не происходит:
//Java
public static final String tableWhen(int x) {
String var10000;
switch(x) {
case 0:
var10000 = "zero";
break;
case 1:
var10000 = "one";
break;
default:
var10000 = "many";
}
return var10000;
}
Если же немного изменить пример выше, и добавить константы:
//Kotlin
val ZERO = 1
val ONE = 1
fun constWhen(x: Int): String = when(x) {
ZERO -> "zero"
ONE -> "one"
else -> "many"
}
То код в этом случае уже компилируется в следующий вид:
//Java
public static final String constWhen(int x) {
return x == ZERO?"zero":(x == ONE?"one":"many");
}
Это происходит потому, что на данный момент компилятор Kotlin не понимает, что значения являются константами, и вместо преобразования к switch, код преобразуется к набору сравнений. Поэтому вместо константного времени происходит переход к линейному (в зависимости от количества сравнений). По словам разработчиков языка, в будущем это может быть легко исправлено, но в текущей версии это пока так.
Существует также возможность использовать модификатор const для констант, известных на момент компиляции.
//Kotlin (файл When2.kt)
const val ZERO = 1
const val ONE = 1
fun constWhen(x: Int): String = when(x) {
ZERO -> "zero"
ONE -> "one"
else -> "many"
}
Тогда в этом случае компилятор уже правильно оптимизирует when:
public final class When2Kt {
public static final int ZERO = 1;
public static final int ONE = 2;
@NotNull
public static final String constWhen(int x) {
String var10000;
switch(x) {
case 1:
var10000 = "zero";
break;
case 2:
var10000 = "one";
break;
default:
var10000 = "many";
}
return var10000;
}
}
Если же заменить константы на Enum:
//Kotlin (файл When3.kt)
enum class NumberValue {
ZERO, ONE, MANY
}
fun enumWhen(x: NumberValue): String = when(x) {
NumberValue.ZERO -> "zero"
NumberValue.ONE -> "one"
NumberValue.MANY -> "many"
}
То код, также как в первом случае, будет компилироваться в switch (практический такой же как в случае перебора enum в Java).
//Java
public final class When3Kt$WhenMappings {
// $FF: synthetic field
public static final int[] $EnumSwitchMapping$0 = new int[NumberValue.values().length];
static {
$EnumSwitchMapping$0[NumberValue.ZERO.ordinal()] = 1;
$EnumSwitchMapping$0[NumberValue.ONE.ordinal()] = 2;
$EnumSwitchMapping$0[NumberValue.MANY.ordinal()] = 3;
}
}
public static final String enumWhen(@NotNull NumberValue x) {
Intrinsics.checkParameterIsNotNull(x, "x");
String var10000;
switch(When3Kt$WhenMappings.$EnumSwitchMapping$0[x.ordinal()]) {
case 1:
var10000 = "zero";
break;
case 2:
var10000 = "one";
break;
case 3:
var10000 = "many";
break;
default:
throw new NoWhenBranchMatchedException();
}
return var10000;
}
По ordinal номеру элемента определяется номер ветки в switch, по которому далее и происходит выбор нужной ветви.
Посмотрим на сравнение производительности решений на Kotlin и Java:
When
Как видно простой switch работает точно также. В случае, когда компилятор Kotlin не смог определить что переменные константы и перешел к сравнениям, Java работает чуть быстрее. И в ситуации, когда перебираем значения enum, также есть небольшая потеря на возню с определением ветви по значению ordinal. Но все эти недостатки будут исправлены в будущих версиях, и к тому же потеря в производительности не очень большая, а в критичных местах можно переписать код на другой вариант. Вполне разумная цена за удобство использования.
Делегаты
Делегирование — это хорошая альтернатива наследованию, и Kotlin поддерживает его прямо из коробки. Рассмотрим простой пример с делегированием класса:
//Kotlin
package examples
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
class Derived(b: Base) : Base by b {
fun anotherMethod(): Unit {}
}
Класс Derived в конструкторе получает экземпляр класса, реализующий интерфейс Base, и в свою очередь делегирует реализацию всех методов интерфейса Base к передаваемому экземпляру. Декомпилированный код класса Derived будет выглядеть следующим образом:
public final class Derived implements Base {
private final Base $$delegate_0;
public Derived(@NotNull Base b) {
Intrinsics.checkParameterIsNotNull(b, "b");
super();
this.$$delegate_0 = b;
}
public void print() {
this.$$delegate_0.print();
}
public final void anotherMethod() {
}
}
В конструктор класса передается экземпляр класса, который запоминается в неизменяемом внутреннем поле. Также переопределяется метод print интерфейса Base, в котором просто происходит вызов метода из делегата. Все достаточно просто.
Существует также возможность делегировать не только реализацию всего класса, но и отдельных его свойств (а с версии 1.1 еще возможно делегировать инициализацию в локальных переменных).
Код на Kotlin:
//Kotlin
class DeleteExample {
val name: String by Delegate()
}
Компилируется в код:
public final class DeleteExample {
@NotNull
private final Delegate name$delegate = new Delegate();
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(DeleteExample.class), "name", "getName()Ljava/lang/String;"))};
@NotNull
public final String getName() {
return this.name$delegate.getValue(this, $$delegatedProperties[0]);
}
}
При инициализации класса DeleteExample создается экземпляр класса Delegate, сохраняемый в поле name$delegate. И далее вызов функции getName переадресовывается к вызову функции getValue из name$delegate.
В Kotlin есть уже несколько стандартных делегатов:
— lazy, для ленивых вычислений значения поля.
— observable, который позволяет получать уведомления обо всех изменения значения поля
— map, используемый для инициализации значений поля из значений Map.
Object и companion object
В Kotlin нет модификатора static для методов и полей. Вместо них, по большей части, рекомендуется использовать функции на уровне файла. Если же нужно объявить функции, которые можно вызывать без экземпляра класса, то для этого есть object и companion object. Рассмотрим на примерах как они выглядят в байткоде:
Простое объявление object с одним методом выглядит следующим образом:
//Kotlin
object ObjectExample {
fun objectFun(): Int {
return 1
}
}
В коде дальше можно обращаться к методу objectFun без создания экземпляра ObjectExample. Код компилируется в практически каноничный синглтон:
public final class ObjectExample {
public static final ObjectExample INSTANCE;
public final int objectFun() {
return 1;
}
private ObjectExample() {
INSTANCE = (ObjectExample)this;
}
static {
new ObjectExample();
}
}
И место вызова:
//Kotlin
val value = ObjectExample.objectFun()
Компилируется к вызову INSTANCE:
//Java
int value = ObjectExample.INSTANCE.objectFun();
companion object используется для создания аналогичных методов только уже в классе, для которого предполагается создание экземпляров.
//Kotlin
class ClassWithCompanion {
val name: String = "Kurt"
companion object {
fun companionFun(): Int = 5
}
}
//method call
ClassWithCompanion.companionFun()
Обращение к методу companionFun также не требует создания экземпляра класса, и в Kotlin будет выглядеть как простое обращение к статическому методу. Но на самом деле происходит обращение к компаньону класса. Посмотрим декомпилированный код:
//Java
public final class ClassWithCompanion {
@NotNull
private final String name = "Kurt";
public static final ClassWithCompanion.Companion Companion = new ClassWithCompanion.Companion((DefaultConstructorMarker)null);
@NotNull
public final String getName() {
return this.name;
}
public static final class Companion {
public final int companionFun() {
return 5;
}
private Companion() {
}
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
//вызов функции
ClassWithCompanion.Companion.companionFun();
Компилятор Kotlin упрощает вызовы, но из Java, правда, выглядит уже не так красиво. К счастью, есть возможность объявить методы по настоящему статическими. Для этого существует аннотация @JvmStatic. Ее можно добавить как к методам object, так и к методам companion object. Рассмотрим на примере object:
//Kotlin
object ObjectWithStatic {
@JvmStatic
fun staticFun(): Int {
return 5
}
}
В этом случае метод staticFun будет действительно объявлен статическим:
public final class ObjectWithStatic {
public static final ObjectWithStatic INSTANCE;
@JvmStatic
public static final int staticFun() {
return 5;
}
private ObjectWithStatic() {
INSTANCE = (ObjectWithStatic)this;
}
static {
new ObjectWithStatic();
}
}
Для методов из companion object тоже можно добавить аннотацию @JvmStatic:
class ClassWithCompanionStatic {
val name: String = "Kurt"
companion object {
@JvmStatic
fun companionFun(): Int = 5
}
}
Для такого кода будет также создан статичный метод companionFun. Но сам метод все равно будет вызывать метод из компаньона:
public final class ClassWithCompanionStatic {
@NotNull
private final String name = "Kurt";
public static final ClassWithCompanionStatic.Companion Companion = new ClassWithCompanionStatic.Companion((DefaultConstructorMarker)null);
@NotNull
public final String getName() {
return this.name;
}
@JvmStatic
public static final int companionFun() {
return Companion.companionFun();
}
public static final class Companion {
@JvmStatic
public final int companionFun() {
return 5;
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
Как показано выше, Kotlin предоставляет различные возможности для объявления как статических методов так и методов компаньонов. Вызов статических методов чуть быстрее, поэтому в местах, где важна производительность, все же лучше ставить аннотации @JvmStatic на методы (но все равно не стоит рассчитывать на большой выигрыш в быстродействии)
lateinit свойства
Иногда возникает ситуация, когда нужно объявить notnull свойство в классе, значение для которого мы не можем сразу указать. Но при инициализации notnull поля мы обязаны присвоить ему значение по умолчанию, либо сделать свойство Nullable и записать в него null. Чтобы не переходить к nullable, в Kotlin существует специальный модификатор lateinit, который говорит компилятору Kotlin о том, что мы обязуемся сами позднее инициализировать свойство.
//Kotlin
class LateinitExample {
lateinit var lateinitValue: String
}
Если же мы попробуем обратиться к свойству без инициализации, то будет брошено исключение UninitializedPropertyAccessException. Подобная функциональность работает достаточно просто:
//Java
public final class LateinitExample {
@NotNull
public String lateinitValue;
@NotNull
public final String getLateinitValue() {
String var10000 = this.lateinitValue;
if(this.lateinitValue == null) {
Intrinsics.throwUninitializedPropertyAccessException("lateinitValue");
}
return var10000;
}
public final void setLateinitValue(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.lateinitValue = var1;
}
}
В getter вставляется дополнительная проверка значения свойства, и если в нем хранится null, то кидается исключение. Кстати именно из-за этого в Kotlin нельзя сделать lateinit свойство с типом Int, Long и других типов, которые соответствуют примитивным типам Java.
coroutines
В версии Kotlin 1.1 появилась новая функциональность, называемая корутины (coroutines). С ее помощью можно легко писать асинхронный код в синхронном виде. Помимо основной библиотеки (kotlinx-coroutines-core) для поддержки прерываний, есть еще и большой набор библиотек с различными расширениями:
kotlinx-coroutines-jdk8 — дополнительная библиотека для JDK8
kotlinx-coroutines-nio — расширения для асинхронного IO из JDK7+.
kotlinx-coroutines-reactive — утилиты для реактивных стримов
kotlinx-coroutines-reactor — утилиты для Reactor
kotlinx-coroutines-rx1 — утилиты для RxJava 1.x
kotlinx-coroutines-rx2 — утилиты для RxJava 2.x
kotlinx-coroutines-android — UI контекст для Android.
kotlinx-coroutines-javafx — JavaFx контекст для JavaFX UI приложений.
kotlinx-coroutines-swing — Swing контекст для Swing UI приложений.
Примечание: Функциональность пока находится в экспериментальной стадии, поэтому все сказанное ниже еще может измениться.
Для того, чтобы обозначить, что функция может быть прервана и использована в контексте прерывания, используется модификатор suspend
//Kotlin
suspend fun asyncFun(x: Int): Int {
return x * 3
}
Декомпилированный код выглядит следующим образом:
//Java
public static final Object asyncFun(int x, @NotNull Continuation $continuation) {
Intrinsics.checkParameterIsNotNull($continuation, "$continuation");
return Integer.valueOf(x * 3);
}
Получается практически исходная функция, за исключением того, что еще передается один дополнительный параметр, реализующий интерфейс Continuation.
interface Continuation<in T> {
val context: CoroutineContext
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}
В нем хранится контекст выполнения, определена функция возвращения результата и функция возвращения исключения, в случае ошибки.
Корутины компилируются в конечный автомат (state machine). Рассмотрим на примере:
val a = a()
val y = foo(a).await() // точка прерывания #1
b()
val z = bar(a, y).await() // точка прерывания #2
c(z)
Функции foo и bar возвращают CompletableFuture, на которых вызывается suspend функция await. Декомпилировать в Java такой код не получится (по большей части из-за goto), поэтому рассмотрим его в псевдокоде:
class <anonymous_for_state_machine> extends CoroutineImpl<...> implements Continuation<Object> {
// текущее состояние машины состояний
int label = 0
// локальные переменные корутин
A a = null
Y y = null
void resume(Object data) {
if (label == 0) goto L0
if (label == 1) goto L1
if (label == 2) goto L2
else throw IllegalStateException()
L0:
a = a()
label = 1
data = foo(a).await(this) // 'this' передается как continuation
if (data == COROUTINE_SUSPENDED) return // возвращение, если await прервал выполнение
L1:
// внешний код возвращает выполнение корутины, передавая результат как data
y = (Y) data
b()
label = 2
data = bar(a, y).await(this) // 'this' передается как continuation
if (data == COROUTINE_SUSPENDED) return // возвращение, если await прервал выполнение
L2:
// внешний код возвращает выполнение корутины передавая результат как data
Z z = (Z) data
c(z)
label = -1 // Не допускается больше никаких шагов
return
}
}
Как видно, получаются 3 состояния: L0, L1, L2. Выполнение начинается в состоянии L0, далее из которого происходит переключение в состояние L1 и после в L2. В конце происходит переключение состояния в -1 как индикация того, что больше никаких шагов не допускается.
Сами корутины могут выполняться в различных потоках, есть удобный механизм для управления этим при помощи указания пула в контексте запуска корутины. Можно посмотреть подробный гайд с большим количеством примеров и описанием их использования.
Все исходные коды на Kotlin доступны в github. Можно открыть их у себя и поэкспериментировать с кодом, параллельно просматривая, в какой итоговый байткод компилируются исходники.
Выводы
Производительность приложений на Kotlin будет не сильно хуже, чем на Java, а с использованием модификатора inline может даже оказаться лучше. Компилятор во всех местах старается генерировать наиболее оптимизированный байткод. Поэтому не стоит бояться, что при переходе на Kotlin вы получите большое ухудшение производительности. А в особо критичных местах, зная во что компилируется Kotlin, всегда можно переписать код на более подходящий вариант. Небольшая плата за то, что язык позволяет реализовывать сложные конструкции в достаточно лаконичном и простом виде.
Спасибо за внимание! Надеюсь вам понравилась статья. Прошу всех тех, кто заметил какие-либо ошибки или неточности написать мне об этом в личном сообщении.
Автор: nerumb