Я впервые встретился с Kotlin 17 июня этого года, и с тех пор я не использовал другой язык. (То есть почти!)

Автор Юрай Мичко (19), разработчик программного обеспечения

Все началось за несколько недель до этого на HackPrague, хакатоне в Праге, который я открыл благодаря StarLift. Я мало что знал о мероприятии, но мы с друзьями составили красивое сопроводительное письмо в формате PDF, и, к счастью, мы вошли.

Это должен был быть наш первый хакатон.

Но как это связано с Котлином? Что ж, был спонсорский приз «Давайте программу в Котлине!» на хакатоне, побуждая участников использовать язык новорожденных.

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

Конечно, у нас не было времени выучить новый язык во время хакатона, но мы заняли 2-е место в категории «Самый инновационный UX для обнаружения мобильных событий» и, мотивированные победой, посетили еще один хакатон в Праге. буквально через неделю. На второй день написания кода мы поняли, что наша концепция не является выигрышной, и захотели сделать наш проект более инновационным. В то время мой Github включал несколько классов, написанных на Kotlin, и я получил первое представление о том, что может быть Kotlin - и что, возможно, это может быть не так уж и плохо.

(Сторона нет: это не помогло нам победить.)

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

Не скучный язык

Программистам на Java очень выгодно, чтобы все было объявлено с типом, известным во время компиляции. Недостатком является написание скучных частей кода, то есть то, что может написать обезьяна, получая только строку кода, даже не зная контекста.

String s = "Hello World!";
BufferedStreamReader bsr = new BufferedStreamReader(stream);

В Kotlin то же самое можно сделать без скучного кода. Например, разработчики Kotlin считают объявления типов, ключевые слова new и точки с запятой скучными, поэтому эквивалент в Kotlin:

val s = "Hello World!"
val bsr = BufferedStreamReader(stream)

Конечно, вы можете переопределить предполагаемый тип или предоставить его, если он не может быть выведен.

val pet: Animal = Dog()
val pet2 = Dog() as Animal // downcasting or upcasting

Всякий раз, когда вы указываете val, переменная является окончательной. Для изменяемых переменных используйте var.

Нулевая безопасность

В Kotlin каждый тип может содержать нулевые значения (String?) или не может (String). Присвойте возможно нулевое значение переменной, не принимающей нулевые значения, и компилятор пожалуется. Также не допускается вызов метода для типа, допускающего значение NULL. Так как же нам разобраться? Все следующие примеры дают одинаковый результат.

// This is basically what one would write in Java style
fun whoIs(user: User?): String? {
    if (user != null) {
        return user.name
    } else {
        return null
    }
}
// But the `if` block is also an expression in Kotlin
fun whoIs(user: User?): String? {
    return if (user != null) user.name else null
}
// The ?. operator will invoke the method only if the object is not null,
// otherwise the whole expression evaluates to null
fun whoIs(user: User?): String? {
    return user?.name
}
// Finally, using a short syntax of methods (the inferred return type is still String?)
fun whoIs(user: User?) = user?.name
// We can supply default values with the Elvis operator ?: that will evaluate
// to the right side if and only if the expression on the left is null
// Note that in this particular case, the compiler infers the return type to by non-null `String`
fun whoIs(user: User?) = user?.name ?: "no one"

Функции расширения

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

Во-первых, важно понять, как работают функции расширения Kotlin и как их можно скомпилировать для Java. Рассмотрим следующее определение:

fun Boolean.thenRun(block: () -> Unit): Boolean {
    if (this) {
        block.invoke()
    }
    return this
}

У метода есть один аргумент - лямбда без параметров, возвращающая Unit (эквивалент void в Java). Если получатель функции (то есть логическое значение) истинно, вызывается лямбда. После этого возвращается исходное логическое значение.

Давайте сохраним код в MyFile.kt, скомпилируем его и декомпилируем полученный MyFileKt.class обратно в Java. Результат примерно такой (я удалил неважные части):

public final class MyFileKt {
    public static final boolean thenRun(boolean $receiver, @NotNull Function0 block) {
        Intrinsics.checkParameterIsNotNull(block, "block");
        if($receiver) {
            block.invoke();
        }
        return $receiver;
    }
}

Как видите, Kotlin решает проблему, определяя общедоступный статический метод вместо того, чтобы принимать логическое значение в качестве первого аргумента. Удивительный! У нас может быть столько библиотек, сколько мы хотим, и столкновения членов не произойдет, поскольку вам всегда нужно импортировать правильную.

Несколько заметок здесь:

  • Тип Kotlin Boolean изменился на primitive boolean, поскольку компилятор знает, что он не может содержать нулевые значения.
  • Мы определили метод непосредственно в файле, но не как член класса. Kotlin также поддерживает функции расширения в качестве членов класса.
  • Как вы могли заметить, в Котлине обычно используются public и final, если не указано иное.

Теперь подумайте, какое применение может иметь эта функция. Взгляните на следующую логику, написанную на Java:

public class MyClass {
    ...
    
    public boolean doSomething() {
        boolean isSuccess = doTheTask(); // returns true on success
        if (isSuccess) {
            System.out.println("OK");
        }
        return isSuccess;
    }
}

Довольно просто. Запускаем задачу, сохраняем ее результат. В случае положительного результата мы запускаем другой блок кода (в данном случае вывод на консоль). Затем возвращаем исходное значение. А теперь посмотрим, что предлагает Котлин:

fun doSomething(): Boolean {
    return doTheTask().thenRun {
        println("OK")
    }
}
// or even better
fun doSomething() = doTheTask().thenRun { println("OK") }

Мало того, что код намного короче, мы полностью избавились от переменной!

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

Нет шаблонного кода

Код на Котлине:

data class Human(var name: String?, private val birth: Date) : Mammal(birth)

Код, декомпилированный в Java, следует ниже. Я снова удалил неинтересные части.

public final class Human extends Mammal {
    @Nullable
    private String name;
    private final Date birth;

    public Human(@Nullable String name, @NotNull Date birth) {
        Intrinsics.checkParameterIsNotNull(birth, "birth");
        super(birth);
        this.name = name;
        this.birth = birth;
    }

    @Nullable
    public final String getName() {
        return this.name;
    }

    public final void setName(@Nullable String var1) {
        this.name = var1;
    }

    @Nullable
    public final String component1() {
        return this.name;
    }

    private final Date component2() {
        return this.birth;
    }

    @NotNull
    public final Human copy(@Nullable String name, @NotNull Date birth) {
        Intrinsics.checkParameterIsNotNull(birth, "birth");
        return new Human(name, birth);
    }

    public String toString() {
        return "Human(name=" + this.name + ", birth=" + this.birth + ")";
    }

    public int hashCode() {
        return (this.name != null?this.name.hashCode():0) * 31 + (this.birth != null?this.birth.hashCode():0);
    }

    public boolean equals(Object var1) {
        if(this != var1) {
            if(var1 instanceof Human) {
                Human var2 = (Human)var1;
                if(Intrinsics.areEqual(this.name, var2.name) && Intrinsics.areEqual(this.birth, var2.birth)) {
                    return true;
                }
            }
            return false;
        } else {
            return true;
        }
    }
}

Думаю, вы поняли, что Kotlin лаконичен. Намного более лаконично.

Свойства и делегированные свойства

Когда есть методы getSomething() и setSomething(value), Kotlin считает их свойством, и вы можете получить к нему доступ, просто написав obj.something и obj.something = value. Когда вы создаете класс со свойством, например, в следующем примере, он компилируется в класс с частным свойством с соответствующими геттерами и сеттерами. Именно это и произошло в предыдущем примере. Конечно, вы можете все изменить.

open class HeyWorld(val abc: String) { // not final class
    // `abc` is a constructor argument and an immutable property with a setter

    var foo = "bar" // public getter and setter
    
    private val now = Date() // private getter and no setter (because of `val`)
    
    protected var amount: Int? = null // protected property initialized to null
        // here we define custom getter; field is a special keyword in this scope
        get() = field ?: 0 // returns the value if not null, otherwise returns 0
        private set // setter is private 
    
    open var abracadabra: Boolean? = true // not final field (unlike the rest)
        set(value) { // value is of type Boolean?
            field = if (value != null)
                // but we negate it as it has been smart-casted to Boolean
                !value
            else null
            // the same could be achieved by writing
            //  field = value?.not()
        }
}

Делегированное свойство означает, что вы делегируете ответственность за принятие и предоставление ценности кому-то (или чему-то) еще.

class Example {
    var p: String by Delegate()
}

Для объекта Delegate должны быть определены определенные методы getValue и setValue, но давайте снова сосредоточимся на удобстве использования.

// lazy evaluation - the value is evaluated only when it's first accessed
val lazyValue by lazy {
    print("The property is being evaluated for the first time!")
    "Hello World!"
}

// managing more properties by one object
class Person(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int     by map
}

Вывод

Котлин обладает множеством волшебных свойств. Вначале меня поразило то, что все компилируется для Java и, следовательно, должно соответствовать ее ограничениям. И действительно, каждой магии здесь есть объяснение.

В статье многое осталось необъяснимым, и вам может быть интересно, действительно ли язык работает так, как ожидалось, и есть ли какие-либо недостатки. Что ж, вы можете сами прочитать документы или поверить мне, когда я скажу, что это потрясающе. Удачи и удачного кодирования!

Вы молодой разработчик из Чехии или Словакии? Хотели бы вы начать свою карьеру в стартапе в США или ЕС? Свяжитесь с нами прямо сейчас через наш сайт, Facebook или Twitter!