Рубрики
Без рубрики

Мое путешествие по языкам JVM

Java – это не просто язык. Настоящая причина, по которой он все еще так популярен сегодня, заключается в зрелости и производительности… Помечено программированием, java, kotlin, scala.

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

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

Итак, как выглядят эти языки и как они соотносятся друг с другом?

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

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

Синтаксис

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

С другой стороны, в нем были представлены современные языковые функции, такие как черты, замыкания, интерполяция строк, необязательная цепочка ( ?. оператор) и многие другие функции задолго до того, как вышла 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 или Spock, поэтому есть вероятность, что вы использовали Groovy, даже не осознавая этого.

Резюме

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

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

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

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

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

Типы

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

Класс Case – это еще одна помощь в использовании типов в Scala. По сути, это класс с неизменяемыми полями и всеми включенными батарейками. Поскольку поля неизменяемы, нам не нужно определять геттеры, и такие методы, как равно или Хэш-код , уже существуют для нас. Обычно эти классы содержат всего одну строку кода, и они очень удобны для определения простых типов структур.

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

В Scala мы также используем типы монад . Что такое монады – тема для совершенно отдельного поста в блоге, но важно то, что они помогают сделать наш код более значимым. Мы можем использовать Опцию , если мы хотим представить значение, которое может быть определено или не определено, или мы можем использовать Попробуйте или Либо , если мы хотим представить успешный результат какой-либо операции, либо ошибку, если она завершилась неудачей.

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

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

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

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

Scala обладает двумя особыми функциями, которые помогают работать с типами монад и классами регистров – сопоставление шаблонов и для понимания .

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

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. интерфейсы.

Котлин – “лучшая Java”

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

Обнуляемые типы и интеграция с Java

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

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

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

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

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

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

Языки, Специфичные для Конкретной области

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

Сопрограммы

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

Приведенный ниже пример взят из Документации Kotlin . Если бы мы попытались запустить 100 кб потоков одновременно, мы бы вызвали исключение 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 Gradle DSL, где, с одной стороны, здорово, что наконец-то набран DSL, но, с другой стороны, все равно было проще скопировать и вставить какой-нибудь классный код из Интернета, чем выяснить, как его перевести. Однако я уверен, что со временем эта ситуация будет становиться все лучше и лучше.

Другие языки

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

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

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

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

Резюме

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

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

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

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

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

Сообщение Мое путешествие по языкам JVM впервые появилось в Техническом блоге Konkit .

Оригинал: “https://dev.to/konkit/my-journey-through-jvm-languages-52bp”