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

Честно говоря, за всю свою карьеру я использовал больше Groovy, Scala или Kotlin, чем чистую Java!

Так как же выглядят эти языки и как они соотносятся друг с другом?

Groovy — мощный язык и современный синтаксис

Groovy был первым языком JVM, который я выучил. Я использовал его в основном, когда использовал фреймворк Grails на своей первой работе.

Синтаксис

Что выделяло Groovy, так это его синтаксис. С одной стороны, это облегчило некоторые проблемы языка Java с помощью необязательных точек с запятой, позволив более одного класса на файл или вывод типа с помощью ключевого слова def.

С другой стороны, в нем появились современные языковые функции, такие как трейты, замыкания, интерполяция строк, необязательная цепочка (оператор ?.) и многие другие функции задолго до того, как вышла Java 8.

// Optional chaining:

def gear = car?.getGearBox()?.getGear()

// Instead of

Gear gear = null;
if (car != null) {
  if (car.getGearBox() != null) {
    gear = car.getGearBox().getGear();
  }
}

Более того, Groovy — это надмножество Java. Это означает, что класс Java также является вполне допустимым классом Groovy! Это упрощает его внедрение, потому что вы можете просто изменить расширение файла на .groovy и шаг за шагом перенести его в более идиоматический код, когда вам нужно.

DSL

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

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

show = { println it }
square_root = { Math.sqrt(it) }

def please(action) {
  [the: { what ->
    [of: { n -> action(what(n)) }]
  }]
}

// equivalent to: please(show).the(square_root).of(100)
please show the square_root of 100
// ==> 10.0

Эти DSL используются, например, в Gradle, Jenkins Pipelines или Spock, поэтому есть вероятность, что вы использовали Groovy, даже не подозревая об этом.

Резюме

Groovy — это динамический язык, а это означает, что объекты можно изменять во время выполнения с помощью добавления методов или перехвата вызовов. Это также является причиной того, что Groovy больше не является моим основным языком. Он как бы страдает от недостатков обоих подходов, которые он пытается объединить.

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

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

Scala — комбинация FP и статической типизации

В Scala также есть некоторые улучшения синтаксиса, но гораздо важнее поддержка парадигмы функционального программирования. Неизменяемые объекты и отсутствие побочных эффектов не только упрощают рефакторинг и тестирование, но и делают Scala подходящим для асинхронного программирования. Поэтому экосистема богата библиотеками для этой цели, такими как Akka, Monix, Cats или ZIO.

Типы

Scala статически типизирована, но это гораздо более продвинутый механизм, чем в Java. Типы — это не просто классы. Иерархия больше, включая такие типы, как Any, AnyVal или Nothing. Мы также можем создавать типы как функции с определенными аргументами и возвращаемыми значениями. Можно определить псевдонимы типов, чтобы сделать наши определения короче и яснее.

Case Class — еще одна помощь в использовании типов в Scala. По сути, это класс с неизменяемыми полями и включенными батареями. Поскольку поля неизменяемы, нам не нужно определять геттеры, а такие методы, как equals или hashCode, уже есть. Обычно эти классы состоят всего из одной строки кода и очень удобны для определения простых типов структур.

case class Circle(x: Double, y: Double, radius: Double)

В Scala мы также используем типы monad. Что такое монады — это тема для отдельной статьи в блоге, но важно то, что они помогают сделать наш код более осмысленным. Мы можем использовать Option, если мы хотим представить значение, которое может быть или не быть определено, или мы можем использовать Try или Either, если мы хотим представить успешный результат какой-либо операции или ошибку, если она не удалась.

val studentOpt: Option[Student] = students.findById(123)

studentOpt.foreach(student => println(student.name))

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

Сопоставление с образцом и для понимания

В Scala есть две особенности, помогающие работать с монадными типами и case-классами — сопоставление с образцом и для понимания.

Сопоставление с образцом кажется простым оператором switch, но на самом деле это нечто большее. Мы можем определить более сложные условия, особенно типы проверки, и использовать отфильтрованные значения без небезопасного приведения.

val result: Either[ErrorCode, SuccessValue] = ???

result match {
    case Right(successValue) =>
      handleSuccess(successValue) // no cast needed, successValue is already of SuccessValue type 

    case Left(errorCode) if errorCode.value > 300 => // additional conditions are possible too
      handleLowPriorityError(errorCode)

    case Left(errorCode) =>
      handleHighPriortyCode(errorCode)  
}

Для понимания — это синтаксический сахар, который помогает нам избежать вложенных цепочек вызовов .flatMap или .foreach при работе с монадными типами. Проще всего объяснить это на примере:

val employeeNames = for {
  company <- companies
  employee <- company.employees
  if employee.currentlyEmployed
} yield {
  employee.name
}

// is an equivalent of this:

val employeeNames = companies
  .flatMap(company =>
    company.employees
      .withFilter(employee => employee.currentlyEmployed)
      .map(employee => employee.name)
  )

Недостатки

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

Другая проблема заключается в том, что он похож на C в одном конкретном отношении — оба эти языка являются мощными инструментами, но из-за этого очень легко увлечься и написать сложный, нечитаемый код. Это одна из причин, по которой кривая обучения Scala такая крутая.

Последнее — более сложная интеграция с библиотеками Java. Не быть расширенным набором Java может быть раздражающим, но на самом деле поддержка IDE делает его незначительным. Более серьезная проблема заключается в том, что библиотеки Java не используют монадные типы Scala и могут использовать изменяемые объекты. Поэтому мы можем получить нулевой объект, когда мы этого не ожидаем, что приведет к исключению NullPointerException. Вероятно, это одна из причин, по которой Scala-коллекции писались с нуля и в них даже не реализованы интерфейсы java.util..

Kotlin — «лучшая Java»

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

Типы, допускающие значение NULL, и интеграция с Java

Подход Kotlin к решению проблемы NullPointerException состоит в том, чтобы ввести типы, допускающие значение null. Когда тип объявлен с ? знак в конце (например, String?), это означает, что он может быть нулевым. Самое приятное то, что именно компилятор проверяет, пытаемся ли мы использовать такой объект, не проверяя, не является ли он нулевым, и возвращает ошибку, если мы это сделали.

val nullableString: String? = null // OK
val notNullableString: String = null // compilation error!

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

Как и в Scala, мы можем работать с неизменяемыми значениями и коллекциями, но не слишком привязываясь к парадигме функционального программирования:

  • Классы данных могут иметь изменяемые поля, в отличие от классов case Scala.
  • В стандартной библиотеке нет монадных типов. Мы можем использовать библиотеки, такие как arrow-kt, но мы должны сами оборачивать значения.
  • Нет сопоставления с образцом, нет для понимания, менее сложная (а значит — менее выразительная) система типов.

Эти функции делают Kotlin идеальным кандидатом на роль «лучшей Java». Сегодня мы видим результаты, когда Android и Spring уже имеют интеграцию с Kotlin.

Языки домена

Как и в Groovy, в Kotlin есть замыкания, что позволяет нам создавать DSL и в Kotlin. Преимущество в том, что в Kotlin такие DSL являются типизированными. В Gradle есть Kotlin DSL, и моя IDE, наконец, может проверить мой код на наличие ошибок и дать мне несколько советов о доступных свойствах.

Корутины

Замыкания также используются в другой отличительной функции Kotlin — Coroutines. По сути, это просто легковесные потоки, которые могут обрабатывать асинхронные операции, сохраняя удобочитаемость.

Приведенный ниже пример взят из Документации Kotlin. Если бы мы попытались запустить 100 000 потоков одновременно, мы бы вызвали ошибку OutOfMemoryException.

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}

В контексте сопрограммы код может быть организован с помощью функций приостановки, которые представляют собой функции, которые запускаются внутри сопрограммы, и их выполнение может быть приостановлено и возобновлено позже, например. когда приходят данные из HTTP-запроса.

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking

fun doSomeWork() = runBlocking {
    val result = makeHttpCall()
    println(result)
}

// Simulate making the call and return the result
suspend fun makeHttpCall(): String {
    // The delay here is also a suspend function, which does block the thread.
    // The execution of the makeHttpCall function is paused until
    // the time of delay passes and then it's resumed
    delay(1000)
    return "Some result"
}

Корутины довольно активно используются в веб-фреймворке под названием Ktor.

Недостатки

Что касается недостатков, я думаю, что самым большим из них является молодой возраст Kotlin. Я мог особенно испытать это с Kotlin Gradle DSL, где, с одной стороны, здорово, что наконец-то DSL набирается, но, с другой стороны, было все же проще скопировать и вставить какой-нибудь код Groovy из интернета, чем придумывать, как перевести Это. Однако я уверен, что со временем эта ситуация будет улучшаться и улучшаться.

Другие языки

Есть, конечно, и другие языки JVM, но я не буду их так подробно описывать, потому что особо ими не пользовался.

Самый популярный язык этой группы — Clojure. Как и Scala, это функциональный язык с крутой кривой обучения. Однако, в отличие от него, это динамический язык и реализация Лиспа. Это делает его очень мощным языком, поскольку код также является данными программы и может быть изменен. Однако, по моему субъективному мнению, это также делает программы Clojure очень нечитаемыми. Тем не менее, многие люди, которые так или иначе повлияли на меня, являются пользователями Clojure, поэтому, возможно, я ошибаюсь, поэтому я не исключаю его использования в будущем 😉

Существуют JRuby и Jython, которые в основном представляют собой реализации Ruby и Python на JVM. Хотя использование библиотек Java на этих языках по-прежнему возможно, они обычно используются только как более производительный интерпретатор Ruby или Python.

Наконец, есть… Java 😀 Я не могу игнорировать прогресс, достигнутый Java в версиях с 9 по 15 и далее. Новые функции, такие как вывод типов с помощью var, сопоставление с образцом или записи, определенно звучат как глоток свежего воздуха и шаг в правильном направлении. К сожалению, у меня тоже нет большого опыта в этом.

Резюме

В настоящее время я использую Scala на работе и Kotlin для своих хобби-проектов.

Какой язык я бы рекомендовал использовать?

Я бы выбрал Scala для ориентированных на данные асинхронных приложений. Эта часть довольно хорошо проработана в Scala. Для этого существует множество библиотек, а парадигма функционального программирования упрощает написание такого кода.

Я бы выбрал Kotlin для простых приложений или тех, которые должны быть тесно интегрированы с библиотеками Java. Это становится все более и более удобным, потому что многие библиотеки Java уже начали интегрироваться и с Kotlin.

Groovy, переданный Apache, на мой взгляд, является признаком снижения популярности и предназначения этого языка. Однако, если вы используете Java для своего производственного кода, я думаю, что Spock сам по себе является достаточной причиной, чтобы проверить Groovy.

Первоначально опубликовано на https://konkit.tech 25 октября 2020 г.