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 има две специфични функции, които помагат за работата с типове монади и класове на случаите — съвпадение на шаблон и за разбиране.

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

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 superset, може да е досадно, но всъщност поддръжката на IDE го прави незначителен. По-сериозен проблем е, че библиотеките на Java не използват типове монади на Scala и могат да използват променливи обекти. Следователно можем да получим нулев обект, когато не го очакваме, сблъсквайки се с NullPointerException. Това вероятно е една от причините колекциите на Scala да са написани от нулата и дори да не имплементират интерфейсите java.util..

Kotlin – „по-добра Java“

Kotlin е най-новият от описаните езици. Поради това имаше възможността да вземе най-доброто от други езици и да поправи онези аспекти, които не бяха добре.

Nullable типове и Java интеграция

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

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

Всички стойности, идващи от Java код, са nullable по подразбиране, което прави този механизъм да работи дори когато интегрираме нашия Kotlin код с някаква Java библиотека.

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

  • Класовете от данни могат да имат променливи полета, докато класовете за регистър на Scala не могат.
  • В стандартната библиотека няма типове монади. Можем да използваме библиотеки, като arrow-kt, но трябва сами да опаковаме стойностите.
  • Без съвпадение на шаблони, без разбиране, по-малко сложна (но по този начин — по-малко изразителна) система от типове.

Тези функционалности правят Kotlin идеален кандидат за „по-добра Java“. Днес можем да видим резултати, когато Android и Spring вече имат интеграция на Kotlin.

Езици, специфични за домейна

Подобно на Groovy, Kotlin има затваряния, което ни позволява да изграждаме DSL и в Kotlin. Предимството е, че в Kotlin такива DSL са типизирани. Gradle има Kotlin DSL и моята IDE най-накрая може да провери моя код за грешки и да ми даде някои съвети относно наличните свойства.

Корутини

Затварянията се използват и в друга отличителна функция на Kotlin — Coroutines. По същество те са просто леки нишки, които могат да обработват асинхронни операции, като същевременно запазват четливостта.

Примерът по-долу идва от документацията на Kotlin. Ако се опитаме да стартираме 100k нишка наведнъж, ще предизвикаме 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, това е функционален език със стръмна крива на обучение. За разлика от него обаче, това е динамичен език и е реализация на Lisp. Това го прави много мощенезик, тъй като кодът също е данни на програмата и може да бъде модифициран. Въпреки това, по мое субективно мнение, това също прави програмите на Clojure много нечетими. Въпреки това, много хора, които ми повлияха по някакъв начин, са потребители на Clojure, така че може би греша, затова не изключвам да го използвам в бъдеще 😉

Има JRuby и Jython, които са основно реализации на Ruby и Python на JVM. Докато използването на Java библиотеки на тези езици все още е възможно, те обикновено се използват само като по-производителен интерпретатор на Ruby или Python.

И накрая, има … Java 😀 Не мога да пренебрегна напредъка, който Java постигна във версии 9 до 15 и нататък. Нови функции като „извод на тип с променлива“, „съвпадение на шаблони“ или „записи“ определено звучат като глътка свеж въздух и стъпка в правилната посока. За съжаление и аз нямам много опит с това.

Резюме

В момента използвам Scala на работа и Kotlin за моите хоби проекти.

Кой език бих препоръчал да използвате?

Бих избрал Scala за ориентирани към данни, асинхронни тежки приложения. Тази част е доста добре разработена в Scala. Има много библиотеки за това и парадигмата на функционалното програмиране улеснява писането на такъв код.

Бих избрал Kotlin за прости приложения или такива, които трябва да бъдат силно интегрирани с Java библиотеки. Това става все по-удобно, защото много Java библиотеки вече започнаха да се интегрират и с Kotlin.

Предаденият Groovy на Apache според мен е знак за намаляваща популярност и предназначение на този език. Въпреки това, ако използвате Java за производствения си код, мисля, че Spock сам по себе си е достатъчно добра причина да проверите Groovy.

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