Что такое Kotlin и с чем его едят: обучающее руководство и сравнение нового языка Android-разработки с Java

Kotlin

Kotlin — это относительно молодой язык от российской компании JetBrains. Появился он в 2011 году. На конференции Google I/O 2017 команда разработчиков Android сообщила, что Kotlin получил официальную поддержку для разработки Android-приложений.

Как и Java, C и C++, Kotlin — это статически типизированный язык. Он поддерживает как объектно-ориентированное, так и процедурное программирование. По аналогии с вышеупомянутыми языками, основной код Kotlin-программы пишется в функции main, которой передаётся массив аргументов командной строки:

// необязательный заголовочный файл пакета
package hello 

fun main(args: Array < String > ) { 
   val scope = "world"
   println("Hello, $scope!") //точки с запятыми необязательны
}

Вот основные возможности и преимущества Kotlin:

  • компилируется в байткод JVM или в JavaScript;
  • программы могут использовать все существующие Java-фреймворки и библиотеки. Kotlin можно интегрировать с Maven, Gradle и другими системами сборки;
  • язык очень прост для изучения;
  • исходный код открыт;
  • в IntelliJ доступна автоматическая конвертация Java-кода в Kotlin и наоборот;
  • язык null-безопасен — надоедливые NullPointerException остались в Java. Вот пример кода:
    val name: String = null // попытка присвоить null, не скомпилируется.
    fun getName() : String = null // попытка вернуть null, не скомпилируется.
  • легко читаемый синтаксис не составит проблем при code review.

Разберём некоторые из них поподробнее.

Null-безопасность

При попытке присваивания или возвращения null код не скомпилируется. Тем не менее, в языке есть поддержка Nullable-типов. Задать такую переменную или функцию можно, приписав ? к названию типа:

val name: String? = null // присваивается null, код компилируется.
fun getName() : String? = null // возвращается null, код компилируется.
/* неверно */	
val name: String? = null
val len = name.length	

/* верно */	
val name: String? = null	
val len = name?.length

Гибкость и простота синтаксиса

Простые функции и структуры можно объявить одной строкой. Геттеры и сеттеры задаются за кулисами для интероперабельности с Java-кодом. Добавление data-аннотации к классу активирует автоматическую генерацию различных шаблонов.

Рассмотрим следующий пример:

/* Программа на Java */	
public class Address {
   private String street;
   private int streetNumber;
   private String postCode;
   private String city;
   private Country country;
   public Address(String street, int streetNumber, String postCode, String city, Country country) {
       this.street = street;
       this.streetNumber = streetNumber;
       this.postCode = postCode;
       this.city = city;
       this.country = country;
   }
   @Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
       Address address = (Address) o;
       if (streetNumber != address.streetNumber) return false;
       if (!street.equals(address.street)) return false;
       if (!postCode.equals(address.postCode)) return false;
       if (!city.equals(address.city)) return false;
       return country == address.country;
   }
   @Override
   public int hashCode() {
       int result = street.hashCode();
       result = 31 * result + streetNumber;
       result = 31 * result + postCode.hashCode();
       result = 31 * result + city.hashCode();
       result = 31 * result + (country != null ? country.hashCode() : 0);
       return result;
   }
   @Override
   public String toString() {
       return "Address{" +
               "street='" + street + '\'' +
               ",     streetNumber=" + streetNumber +
               ",     postCode='" + postCode + '\'' +
               ",     city='" + city + '\'' +
               ",     country=" + country +
               '}';
   }
   public String getStreet() {
       return street;
   }
   public void setStreet(String street) {
       this.street = street;
   }

   public int getStreetNumber() {
       return streetNumber;
   }
   public void setStreetNumber(int streetNumber) {
       this.streetNumber = streetNumber;
   }
   public String getPostCode() {
       return postCode;
   }
   public void setPostCode(String postCode) {
       this.postCode = postCode;
   }
   public String getCity() {
       return city;
   }
   public void setCity(String city) {
       this.city = city;
   }
   public Country getCountry() {
       return country;
   }
   public void setCountry(Country country) {
       this.country = country;
   }
}
/* Та же программа на Kotlin */
data class Address(var street:String,
                   var streetNumber:Int,
                   var postCode:String,
                   var city:String,
                   var country:Country)

Отличия от Java

Null-безопасность

Как уже говорилось ранее, Kotlin не допускает возникновения NullPointerException, выдавая ошибку компиляции.

Классы данных (Data Classes)

В Kotlin появились специальные классы, предназначенные специально для хранения данных. Они генерируют различные шаблоны: equals(), hashCode(), toString(), геттеры и сеттеры и т.д. Сравните код на Java:

class Book {
   private String title;
   private Author author;
   public String getTitle() {
       return title;
   }
   public void setTitle(String title) {
       this.title = title;
   }
   public Author getAuthor() {
       return author;
   }
   public void setAuthor(Author author) {
       this.author = author;
   }
}

И на Kotlin:

/* Kotlin */
data class Book(var title:String,var author:Author)

Легко создавать копии классов данных при помощи метода copy():

val book = Book("Kotlin", "JetBrains")
val copy = book.copy()

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

Kotlin позволяет расширять функциональность существующих классов, не прибегая к наследованию. Это делается при помощи функций-расширений. Для объявления такой функции к её имени нужно приписать префикс в виде расширяемого типа. Вот так можно добавить функцию swap в MutableList:

fun MutableList<Int>.swap(index1:Int,index2:Int){
   val tmp=this[index1]
   this[index1]=this[index2]
   this[index2]=tmp
}

Ключевое слово this внутри функции-расширения относится к объекту-получателю, который передаётся перед точкой. Теперь мы можем применить функцию swap к любому изменяемому списку:

val abc = mutableListOf(1, 2, 3)
abc.swap(0, 2)

Умные приведения типов

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

fun demo(x:Any) {
   if(x is String) {
      print(x.length)  // x автоматически приводится к типу String
   }
}

Вывод типов

В Kotlin необязательно явно указывать тип переменной:

/* неявное определение */
fun main(args: Array<String>) {
   val text = 10
   println(text)
}

/* явное определение */
fun main(args: Array<String>) {
   val text: Int = 10
   println(text)
}

Функциональное программирование

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

fun main(args: Array<String>) {
   val numbers = arrayListOf(15, -5, 11, -39)
   val nonNegativeNumbers = numbers.filter { it >= 0 }
   println(nonNegativeNumbers)
}
// Вывод: 15, 11

Функции высшего порядка — это функции, которые принимают другие функции в качестве аргументов и возвращают функции. Рассмотрим следующий пример:

fun alphaNum(func: () -> Unit) {}

В нём func — это имя аргумента, а ( ) -> Unit — это тип функции. Мы говорим, что func будет функцией, не принимающей аргументов и ничего не возвращающей.

Лямбда-выражения, или анонимные функции — это функции, которые не объявляются, а передаются в виде выражений. Вот пример:

val sum: (Int, Int) -> Int = { x, y -> x + y }

Мы объявляем переменную sum, которая берёт два числа, складывает их и принимает значение суммы, приведённое к целому. Для вызова достаточно простого sum(2,2).

Сравнение скорости Java и Kotlin

Первая сборка Kotlin-кода занимает примерно на 15–20% больше времени, чем аналогичный процесс на Java. Однако инкрементная сборка Kotlin даже немного быстрее, чем у Java. Таким образом, языки примерно равны по скорости компиляции.

Будущее Kotlin

Kotlin — это следующий этап развития Java, с которой он полностью совместим. Это делает его отличным инструментом для мобильных и энтерпрайз-приложений. А поскольку Kotlin теперь является официальным языком Android, можно не бояться, что, изучив его, вы останетесь без работы.

В изучении этого языка вам поможет серия статей, описывающая процесс создания простого мобильного приложения Keddit — клиента для популярного ресурса Reddit. Все ее части для вашего удобства мы перечислили в списке ниже.

Вам потребуются следующие библиотеки:

  • Retrofit 2.0;
  • RxJava;
  • Picasso;
  • RecyclerView;
  • Расширения Kotlin для Android;
  • Dagger 2.

Все исходники доступны на GitHub. Серия состоит из следующих частей:

  1. Настройка Android Studio.
  2. MainActivity.kt: синтаксис, null-безопасность и другое.
  3. NewsFragment.kt: функции-расширения, Android-расширения и т.д.
  4. RecyclerView : классы данных и адаптеры делегатов.
  5. Kotlin, RxJava и RxAndroid.
  6. API :  Retrofit и Kotlin.
  7. Бесконечная прокрутка: функции высшего порядка и лямбды.
  8. Изменение ориентации экрана.
  9. Юнит-тестирование: Mockito, RxJava и Spek.
  10. Внедрение зависимостей: Kotlin и Dagger 2.
  11. Непрерывная интеграция: BuddyBuild.
  12. Заключение.

Если вам удобнее видеоформат, обратите внимание на русскоязычный видеокурс по Kotlin.

Адаптированный перевод статьи «Overview of Kotlin & Comparison With Java»