My journey through JVM languages
Łukasz Tenerowicz
Posted on October 25, 2020
Java is not just a language. The real reason why it’s still so popular today is the mature and performant platform. And because the programs are compiled to the bytecode, the language's deficiencies could be made up for by ... creating other JVM languages.
To be honest, throughout my career, I’ve used more Groovy, Scala, or Kotlin than pure Java itself!
So how do those languages look like and how do they compare with each other?
Groovy – powerful language and modern syntax
Groovy was the first JVM language I’ve learned. I was using it mostly when I was using the Grails framework at my first job.
Syntax
What made Groovy stand out was its syntax. On the one side, it eased some of Java language pains via optional semicolons, allowing more than one class per file or type inference with the def
keyword.
On the other hand, it featured modern language features, such as traits, closures, string interpolation, optional chaining (?.
operator) and many other features long before even Java 8 came out.
// Optional chaining:
def gear = car?.getGearBox()?.getGear()
// Instead of
Gear gear = null;
if (car != null) {
if (car.getGearBox() != null) {
gear = car.getGearBox().getGear();
}
}
Moreover, Groovy is a superset of Java. It means, that a Java class is a perfectly valid Groovy class too! This makes its adoption way easier because you can just change the file extension to .groovy
and transfer it to a more idiomatic code step by step as you need.
DSLs
Closures, maps and optional dots and parentheses allows us to create Domain Specific Languages, which can look like almost like written in plain English.
The excerpt below is from the official Groovy documentation and in my opinion it explains it quite well.
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
Those DSLs are used for example in Gradle, Jenkins Pipelines, or Spock, so there is a chance, that you’ve been using Groovy without even realizing it.
Summary
Groovy is a dynamic language, which means, that objects can be modified at runtime via appending methods or intercepting the calls. This is also the reason, why Groovy isn’t my g0-to language anymore. It kind of suffers from the disadvantages of both approaches it tries to merge.
On one side, it still has to be compiled, because it is run on the JVM platform. Because of that, it can’t benefit from fast feedback loops like in other dynamic languages such as Ruby, which are interpreted.
On the other one, because it’s a dynamic language, the compilation step won’t give us that much guarantees about program correctness. Even though we can specify types of our variables and method arguments, we still can run into a runtime error at some point.
Scala – FP and static typing combo
Scala also features some syntax improvements, but way more important is its support for the functional programming paradigm. Immutable objects and lack of side-effects not only make refactoring and testing easier, but it makes Scala well suited for asynchronous programming. Therefore the ecosystem is rich in libraries for that purpose, such as Akka, Monix, Cats, or ZIO.
Types
Scala is statically typed, but it is a way more advanced mechanism than in Java. Types are not just classes. The hierarchy is larger, including types such as Any
, AnyVal
or Nothing
. We can also construct types as functions, with defined arguments and returned values. It’s possible to define type aliases to make our definitions shorter and clearer.
Case Class is another help in using types in Scala. It’s basically a class with immutable fields and all batteries included. Because the fields are immutable, we don’t have to define getters and methods such as equals
or hashCode
are already there for us. Usually, those classes have just one line of code and they are very convenient to define simple structure types.
case class Circle(x: Double, y: Double, radius: Double)
In Scala, we also make use of monad types. What monads are is a topic for a completely separate blog post, but what’s important is that they help to make our code more meaningful. We can use Option
if we want to represent a value that may or may not be defined or we can use Try
or Either
if we want to represent a successful result of some operation or an error if it failed.
val studentOpt: Option[Student] = students.findById(123)
studentOpt.foreach(student => println(student.name))
The gamechanger here is the fact, that the Scala Standard Library uses those types throughout the library, so we don’t have to worry about null checks or runtime exceptions within the Scala code unless we use them explicitly or integrate with Java libraries using them.
Pattern matching and for comprehension
Scala has two particular features, that help to work with monad types and case classes – pattern matching and for comprehension.
Pattern matching seems like a simple switch statement, but it is much more than that. We can define more advanced conditions, especially check types, and use the filtered values without unsafe casting.
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)
}
For comprehension is the syntactic sugar, which helps us to avoid nested chains of .flatMap
or .foreach
calls when we work with monad types. It’s the easiest to explain it with an example:
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)
)
Downsides
The obvious Scala’s downside is a steep learning curve. Due to its complexity, it takes some time to be productive in it and write a proper idiomatic code. In my case, I’ve made the biggest progress during my first code review in a team, which was already using Scala and I think it’s the best way to learn it – from other teammates already proficient in it ;)
Another issue is that it is similar to C in one specific manner – both of those languages are powerful tools, but because of that, it’s very easy to get carried away and write complicated, unreadable code. It’s part of the reason why Scala’s learning curve is so steep.
The last one is harder integration with Java libraries. Not being a Java superset could be annoying, but in fact, IDE support makes it negligible. A more serious problem is that Java libraries do not use Scala monad types and they may use mutable objects. Therefore we can receive a null object when we do not expect it, running into a NullPointerException. This is probably one of the reasons why Scala collections were written from scratch and they even don’t implement the java.util.
interfaces.
Kotlin – a “better Java”
Kotlin is the newest of the described languages. Because of that, it had the opportunity to take what’s best from other languages and fix those aspects which were less ok.
Nullable types and Java integration
Kotlin’s approach to tackle the NullPointerException problem is to introduce nullable types. When a type is declared with a ? sign at the end (e.g. String?), it means that it can be null. The best part is that it’s the compiler that checks if we try to use such an object without checking if it wasn’t null and returns the error if we did.
val nullableString: String? = null // OK
val notNullableString: String = null // compilation error!
All values coming from Java code are nullable by default, which makes this mechanism work even when we integrate our Kotlin code with some Java library.
Just like in Scala, we can work with immutable values and collections, but without committing that much to the functional programming paradigm:
- Data classes can have mutable fields, where Scala’s case classes can not.
- There are no monad types in the standard library. We can use libraries, like arrow-kt, but we have to wrap the values ourselves.
- No pattern matching, no for comprehension, less complicated (but thus – less expressive) type system.
Those functionalities make Kotlin a perfect candidate for being a “better Java”. We can see results today, where Android and Spring already have Kotlin integration.
Domain Specific Languages
Similarly to Groovy, Kotlin has closures, which lets us build DSL in Kotlin too. The advantage is that in Kotlin such DSLs are typed. Gradle has Kotlin DSL and my IDE can finally check my code for errors and give me some hints about available properties.
Coroutines
Closures are also used in another distinct Kotlin feature – Coroutines. In essence, they are simply lightweight threads, which can handle asynchronous operations while preserving the readability.
The below example comes from Kotlin documentation. If we tried to run 100k of thread at once, we would cause an OutOfMemoryException
.
import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(100_000) { // launch a lot of coroutines
launch {
delay(5000L)
print(".")
}
}
}
Within the coroutine context, the code can be organized with suspend functions , which are functions that are run within the coroutine and its execution can be paused and resumed later, e.g. when the data from the HTTP request arrives.
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"
}
Coroutines are pretty heavily used in the web framework called Ktor.
Downsides
Regarding downsides, I think that the biggest one is Kotlin’s young age. I could especially experience it with Kotlin Gradle DSL, where on one hand it’s great that finally the DSL is typed, but on the other hand, it was still easier to copy and paste some Groovy code from the internet than to figure out how to translate it. However, I’m sure, that this situation will be better and better over time.
Other languages
There are of course other JVM languages, but I won’t describe them in such detail, because I haven’t really used them that much.
The most popular language of this group is Clojure. Just like Scala, it’s a functional language with a steep learning curve. Unlike it, however, it’s a dynamic language and it’s a Lisp implementation. This makes it a very powerful language because the code is also the program’s data and it can be modified. However, in my subjective opinion, this also makes Clojure programs very unreadable. However, a lot of people that influenced me in some way are Clojure users, so maybe I am wrong, therefore I’m not ruling out using it in the future :D
There are JRuby and Jython, which are basically Ruby and Python implementations on JVM. While using Java libraries in those languages is still possible, they are usually used just as a more performant Ruby or Python interpreter.
Finally, there is … Java :D I can’t neglect the progress, that Java has made throughout versions 9 to 15 and onwards. New features like type inference with var, pattern matching, or records definitely sound like a breath of fresh air and a step in the right direction. Unfortunately, I don’t have much experience with that either.
Summary
Currently, I’m using Scala at work and Kotlin for my hobby projects.
Which language would I recommend using?
I’d choose Scala for data-oriented, asynchronous-heavy applications. That part is pretty well worked out in Scala. There are many libraries for that and functional programming paradigm makes writing such code easier.
I’d choose Kotlin, for simple applications or the ones which have to be heavily integrated with Java libraries. This is becoming more and more convenient because many Java libraries already started to integrate with Kotlin too.
Groovy handed to Apache is in my opinion a sign of declining popularity and purpose of this language. However, if you use Java for your production code, I think that Spock alone is a good enough reason to check Groovy out.
The post My journey through JVM languages appeared first on Konkit's Tech Blog.
Posted on October 25, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.