1. Introduction

In this tutorial, we’ll examine the key characteristics and features of Scala and Kotlin, two of the popular programming languages for JVM besides Java.

In the process, we’ll also understand how they compare against each other and in what circumstances they may be a better fit as a language than Java.

2. Key Characteristics

Let’s begin by going through the key characteristics of Scala and Kotlin. This will help us establish the course of discussion for the rest of the tutorial.

2.1. Scala

The design for Scala began at EPFL by Martin Odersky, way back in 2001. The name itself is a portmanteau of the words “scalable” and “language”. Back then, while Java made rapid advancements, it was very constrained due to the requirements to maintain backward compatibility. This gave rise to the academic project, Funnel, based on an object-oriented version of join calculus called Functional Nets.

However, Funnel was not very practical to use and ultimately led to the development of Scala. Scala was designed to create a better Java and is still connected to the Java infrastructure, the JVM, and its libraries. In summary, Scala is a general-purpose programming language that supports both object-oriented and functional programming.

Scala source code compiles to Java bytecode, so it can execute on any JVM. It also provides complete interoperability with Java, and hence, we can reference Java from Scala and vice versa with ease. Scala adds several features that are absent in Java. These are inspired by several other languages like Scheme, Standard ML, and Haskell.

2.2. Kotlin

Kotlin is a relatively newer programming language, started by JetBrains in 2011. JetBrains is the same company that has developed the popular IDE, IntelliJ IDEA. While having very similar design goals as Scala, Kotlin targeted to improve the shortcomings of Scala — most notably, the slow compilation time of Scala. It’s named after Kotlin Island, near St. Petersberg in Russia.

At heart, Kotlin is a multi-paradigm, object-oriented programming language that aims to be a better language than Java. However, it maintains complete interoperability with Java, allowing for gradual adoption to build amongst the Java developer community. To help this cause, in 2019, Goggle announced Kotlin to be the preferred language for the Android platform!

Kotlin also supports multi-platform programming, which may prove to be one of its key benefits. While it started with JVM as the primary target platform, we can now quite easily compile the Kotlin source for other platforms, like JS and Native platforms. Kotlin itself is a very lightweight language, and it provides most of the advanced features like serialization and concurrency through extension libraries.

3. Type System

The type system is one of the defining features of any programming language. Basically, it’s a set of rules to assign the type to various language constructs like variables, functions, or expressions. Among other objectives, it serves to avoid type errors in programs. Broadly speaking, a type system can be static or dynamic, depending upon if the type checks happen at compile time or run time, respectively.

Moreover, one of the important aspects of the type system in a programming language is its support for creating parameterized types. Basically, this allows us to define generic constructs, like classes and functions, that accept a type as a parameter. Apart from supporting a stronger type check at compile-time, it helps us write generic algorithms.

Also, it’s important to understand how subtyping between more complex types relates to subtyping between their components. We know this correlation as the variance. The subtyping relation between components may be ignored, preserved, or even reversed in the respective complex types. This gives rise to invariance, covariance, and contravariance relations, respectively.

3.1. Scala Type System

Scala is a statically typed language like Java, but it has one of the most sophisticated and comprehensive type systems. While it improves upon the shortcoming of Java, it also combines functional programming elements together with object-oriented programming in its type system.

Scala has a unified type hierarchy, where Any is a supertype of all types. It has two direct subclasses, AnyVal, representing value types, and AnyRef, representing reference types:

Additionally, there is Null, a subtype of all reference types, and Nothing, a subtype of all types.

We can also use type parametrization to write generic classes in Scala. Let’s take an example:

class Garage[+A]

Since the Garage class here takes a type as a parameter, we can create its instances with any type:

class Vehicle
class Car extends Vehicle
class Bike extends Vehicle
var myCarGarage = new Garage[Car] 
var myBikeGarage = new Garage[Bike]

But, subtypes of a generic type in Scala are invariant by default. In simpler words, the following statement is illegal, even though it’s intuitive:

val myGarage: Garage[Vehicle] = new Garage[Car] // Illegal statement
val myGarage: Garage[Car] = new Garage[Vehicle] // Illegal statement

Scala supports variance annotations of the generic classes’ type parameters to allow them to be covariant or contravariant.

Let’s see how we can use the variance annotation to make our generic class covariant:

class Garage[+A]
var myGarage: Garage[Vehicle] = new Garage[Car] // This is legal now
var myGarage: Garage[Car] = new Garage[Vehicle] // This is still illegal

Similarly, we can use another variance annotation to make this generic class contravariant:

class Garage[-A]
var myGarage: Garage[Vehicle] = new Garage[Car] // This is still illegal
var myGarage: Garage[Car] = new Garage[Vehicle] // This is legal now

We know this style of variance as declaration-site variance because we add the variance annotation while defining a generic class.

Further, we can constrain the type parameters in Scala by using type bounds. The bounds limit the concrete values of the type variable:

class Garage[T <: Vehicle] { }
class Garage[T >: Car] { }

As we can see here, the type bounds can be either an upper type bound or a lower type bound. The upper type bound restricts the type variable T to be a subtype of Vehicle. Conversely, the lower type bound restricts the type variable T to be a supertype of Car.

3.2. Kotlin Type System

Kotlin is also a statically typed language but has a much simpler type system compared to Scala. Similar to Scala, Kotlin also has a unified type system. However, Kotlin clearly distinguishes between nullable and non-nullable types. We can define any type as either nullable or non-nullable. Kotlin has Any representing the supertype of all non-nullable types and Any? representing the supertype of all nullable types:

Please note that nullable types are the supertype of corresponding non-nullable types. Further, Kotlin has as a special type, Unit, representing the lack of a type, equivalent to void in Java. Similar to Scala, Kotlin has Nothing, which represents non-termination.

Like Scala and Java, we can define generic classes in Kotlin that take a type as a parameter:

class Garage<T>

Now, we can instantiate the Garage class with different type parameters:

open class Vehicle
class Car: Vehicle()
class Bike: Vehicle()
val myCarGarage = Garage<Car>()
val myBikeGarage = Garage<Bike>()

Similar to Scala, generic classes are invariant by default in Kotlin. But, Kotlin also provides declaration-site variance annotations to define covariant and contravariant generic types:

//Covariant Garage
class Garage<out T>
var myGarage: Garage<Vehicle> = Garage<Car>() //This is legal
var myGarage: Garage<Car> = Garage<Vehicle>() //This is illegal

//Contravariant Garage
class Garage<in T>
var myGarage: Garage<Vehicle> = Garage<Car>() //This is illegal
var myGarage: Garage<Car> = Garage<Vehicle>() //This is legal

However, Kotlin also supports use-site variance through type projection. It’s not always possible to apply a declaration-site annotation to type parameter:

class Garage<in T> {
    fun park(t: T): T {return t}
}

Here, the type parameter T appears both as producer and consumer, therefore, it’s not possible to make it either covariant or contravariant. But, we can still get type safety through use-site variance:

fun transfer(from: Garage<out Vehicle>, to: Garage<Vehicle>) { }
fun main() {
    transfer(Garage<Car>(), Garage<Vehicle>()) // This is legal due to type projection
}

The type projection basically means that the function parameter from is a restricted or projected type.

Kotlin also provides star projection for the cases where we have no idea about the type argument but still want to use them in a type-safe manner:

fun transfer(from: Garage<*>, to: Garage<Vehicle>) {  }
fun main() {
    transfer(Garage<Car>(), Garage<Vehicle>()) // This is legal  due to star projection
}

Finally, Kotlin also supports defining upper bounds to the type parameters for restricting the values that it can take:

open class Vehicle
interface Electric
// Single Upper bound
class Garage<T: Vehicle> { } 
// Multiple Upper bounds
class Garage<T> where T: Vehicle, T: Electric { }

Here, as we can see, we can define one or more upper bounds on the type parameter.

4. Support for Object-oriented Programming

Object-oriented programming (OOP) is a programming paradigm that organizes software design around objects. An object is basically a type that contains data in the form of properties and behavior in methods. The use of objects is further bound by certain principles like encapsulation, abstraction, inheritance, and polymorphism.

Java has been primarily a class-based object-oriented programming language since its inception. It has fundamental support for all the basic principles of object-oriented programming. It’s only natural to assume that Scala and Kotlin, coming from the Java background, would have even better support for it.

Scala, being a more mature language, has support for many additional features. Some of them are even confusing, like implicit classes and parameters based on implicit conversion. Implicit conversion allows the Scala compiler to convert one type to another based on some conditions automatically. However, we’ll skip these advanced topics.

4.1. OOP Support in Scala

Similar to Java, Scala is a class-based object-oriented programming language, where a class serves as the blueprint for creating objects. Scala classes can contain members like methods, values, variables, types, objects, traits, and classes:

class Car(var make: String, var model: String) {
  val description = s"($make, $model)"
}

The primary constructor cannot have any initialization code. But, the initialization code can appear anywhere in the class body. Apart from the primary constructor, we can also define any number of auxiliary constructors in Scala, but they must call the primary constructor:

class Car(var make: String, var model: String) { 
  val description = s"($make, $model)"
  def this(make: String) = {
    this(make, "Unknown")
  } 
}

Members in a class are public in Scala by default. But, we can use access modifiers to restrict their visibility from outside the class:

class Car(var make: String, var model: String) {
  private val _description = s"($make, $model)"
  def description = _description
}

Sometimes, we need a single instance of a class, and in those cases, it’s not necessary to define a class for it. Scala provides singleton object for this:

object Logger {
  def info(message: String): Unit = println(s"INFO: $message")
}

An object with the same name as a class is called a companion object of the class. This is analogous to defining static members in a Java class. A companion object can access the private members of its companion class and vice versa:

class Car(var make: String, var model: String) {
  private val _description = s"($make, $model)"
  import Car._
  log(_description)
}

object Car {
  private def log(message: String) = Logger.info(message)
}

We can create abstractions in Scala with abstract classes or traits. Traits are analogous to interfaces in Java. Both can contain abstract and non-abstract methods:

abstract class Vehicle {
  def accelerate: Unit
  def decelerate: Unit
}

trait Electric {
  def recharge: Unit
}

trait Insured {
  def renew: Unit
}

But, unlike abstract classes, we can achieve multiple inheritances with traits, using mixins in class composition:

class Car extends Vehicle with Electric with Insured {
  def accelerate = log("Accelerating")
  def decelerate = log("decelerating")
  def recharge = log("Recharging")
  def renew = log("Renewing")
}

An abstract class or a trait in scala can extend other abstract classes or traits.

Here, Car doesn’t need to mix both the traits, Electric and Insured. But, there’s a way in which we can declare that a trait must be mixed with another trait:

trait Electric {
  this: Insured =>
  def recharge: Unit
}

This is called a self-type in Scala and forces us to mix the trait Electric with the trait Insured.

4.2. OOP Support in Kotlin

Kotlin also supports class-based object-oriented programming. We can create a class with an optional primary constructor but can only add initialization code to initializer blocks:

class Car(var make: String, var model: String) {
    val description = "($make, $model)"
    init {
        print(description)
    }
}

We can also define secondary constructors in Kotlin, but they must delegate to the primary constructor:

class Car(val make: String, val model: String) {
    val description = "($make, $model)"
    constructor(make:String): this(make, "Unknown") {
        print(description)
    }
}

The default visibility modifier in Kotlin is public, but we can change it to private, protected, or internal as well. Getters and setters of properties are automatically generated in Kotlin, and getters have the same visibility as their corresponding properties:

class Car(val make: String, val model: String) {
    private val description = "($make, $model)"
}

A class is final in Kotlin by default, which means we cannot inherit from it. However, we can annotate a class to be open for inheritance:

open class Vehicle
class Bike: Vehicle()
class Car: Vehicle()

Similar to Scala, we have an option to declare singleton objects in Kotlin. A singleton object can also have supertypes:

object Logger { 
    fun info(message: String) {println("INFO: $message")}
}

We can mark an object declared inside a class as a companion. This makes it possible to call the members of a companion object by using the class name as the qualifier:

class Bike: Vehicle(){
    override fun accelerate() {log("Accelerating")}
    override fun decelerate() {log("Decelerating")}
    companion object {
        private fun log(message: String) {Logger.info(message)}
    }
}

Kotlin also provides an option to extend a class with new functionality without having to inherit from it. This is possible through extension functions:

fun Bike.start() {
    Logger.info("Starting")   
}

val bike:Bike = Bike()
bike.start()

Similar to Java and Scala, we can create abstractions in Kotlin using abstract classes and interfaces. An interface is a little different from an abstract class as it can’t store state:

abstract class Vehicle {
    abstract fun accelerate()
    abstract fun decelerate()
}

interface Electric {
    fun recharge()
}

interface Insured {
    fun renew()
}

As before with Scala, a class can inherit from only a single class or abstract class, but it can inherit from multiple interfaces:

class Bike: Vehicle(), Electric, Insured {
    override fun accelerate() {println("Accelerating")}
    override fun decelerate() {println("Decelerating")}
    override fun recharge() {println("Recharge")}
    override fun renew() {println("Renew")}
}

We can mark an interface with a single abstract method is a functional interface in Kotlin. As a benefit, we can use lambda expressions for instantiating it:

fun interface Insured {
    fun renew()
}

val insured = Insured({println("Renewing")})

Finally, Kotlin has native support for delegation as an alternate to inheritance:

class BaseInsured: Insured {
    override fun renew(){Logger.info("Renewing")}
}

class Bus(insured: Insured) : Vehicle(), Insured by insured {
    override fun accelerate() {Logger.info("Accelerating")}
    override fun decelerate() {Logger.info("Decelerating")}
}

val bus:Bus = Bus(BaseInsured())

Here, the class Bus is delegating all public members Insured to an object of BaseInsured instead of implementing them itself.

5. Support for Functional Programming

Functional programming is basically a declarative programming paradigm where we construct programs by applying and composing functions. By functions, we mean trees of expressions that map values to other values. In functional programming, we treat functions as first-class citizens. Thus, we should be able to treat them just like any other data type.

As we’ve seen earlier, Java originated as an object-oriented language and never had native support for functional programming. Although it was always possible to do functional programming in Java, it was certainly not easy. However, starting with Java 8, we’ve seen a lot of features that make it easier. Naturally, support for functional programming should be much better in Scala and Kotlin.

Functional programming includes many techniques like pure functions, higher-order functions, currying, iteration, and monads, to name a few. Typically, it’s possible to implement many of these in any general-purpose programming language, even if they’re not natively supported. However, we’ll restrict our discussion to the techniques with some level of native support in Scala or Kotlin.

5.1. Functional Programming Support in Scala

To begin with, functions are first-class citizens in Scala, and that makes many functional programming idioms easier to handle. Let’s take an example:

val myFunction = (x:Int) => x*2
myfunction(2) // 4

Here, we can create a function like any other data type.

A higher-order function is a function that takes other functions as parameters or returns a function as a result. This is a handy technique in functional programming:

def handle(number: Int, function: Int => Int): Int = function(number)
handle(4, myFunction) // 8

Here, we’ve defined a higher-order function that takes another function as a parameter.

Scala also allows us to nest a function inside another function. This has some useful applications in functional programming. Let’s see how can we get the nth Fibonacci number using nested functions:

def fibonacci(x: Int): Int = {
  def next(x: Int, y: Int, current: Int, target: Int): Int = {
    if (current == target) y
    else next(y, x + y, current + 1, target)
  }
  if(x==1) 0 else if(x==2) 1 else next(0, 1, 2, x)
}
<code class="language-kotlin">println(fibonacci(10)) // 34

Scala also allows methods to take multiple parameter lists, a technique that’s also known as currying in functional programming. One of the use cases for this feature is the partial application:

def weight(gravity: Double) = (mass: Double) => mass*gravity;
val weightOnEarth = weight(9.81)
println(weight(9.81)(80.0))
println(weightOnEarth(80.0))

Here, we’ve defined a function that takes two parameters to calculate the weight on a planet. But, we can pass a single parameter that results in another function with partial application.

Another important principle of functional programming is immutability. Simply put, we can’t change the state of an immutable type after creating it. Scala has native support for modeling immutable data in the form of the case class:

case class Planet(gravity: Double)
val earth = Planet(9.81)
earth.gravity = 10 //Illegal, can you imagine what might happen if gravity changes!

The parameters of a case class are values by default, which means they can’t be reassigned.

Another useful technique in functional programming is recursion, where a function calls itself from within its own code. Depending upon where the recursive call happens, it can be either head recursion or tail recursion.

Now, tail recursion can greatly benefit from the memory optimization technique called tail call optimization (TCO):

import scala.annotation.tailrec
def factorial(x: Int): Int = {
  @tailrec
  def next(x: Int, result: Int): Int = {
    if(x == 1) result
    else next(x-1, result*x)
  }
  next(x, 1)
}

Scala performs compile-time tail-call optimization and practically replaces recursive calls with a single call.

5.2. Functional Programming Support in Kotlin

As with Scala, Kotlin also treats functions as its first-class citizens — we can create and use them like any other data type:

val myFunction = {x: Int -> x*2}
myFunction(2) // 4

Kotlin achieves this through a set of function types to represent functions and specialized language constructs like lambda expressions to use them.

Let’s see how we can use this function to create a higher-order function:

fun handle(number: Int, function: (Int) -> Int): Int = function(number)
handle(4, myFunction) // 8

Typically, as each function is an object and may further capture a closure, the runtime overhead in terms of memory allocation is higher. However, we can eliminate this overhead by inlining the lambda expressions:

inline fun handle(number: Int, function: (Int) -> Int): Int = function(number)

The inline modifier instructs the Kotlin compiler to inline the function and the lambdas passed to it at the call site.

Similar to Scala, Kotlin also allows us to nest a function inside another function:

fun fibonacci(x: Int): Int {
  fun next(x: Int, y: Int, current: Int, target: Int): Int {
    return if (current == target) y
      else next(y, x + y, current + 1, target)
  }
  return if (x == 1) 0 else if (x == 2) 1 else next(0, 1, 2, x)
}
println(fibonacci(10)) // 34

Defining curried functions in Kotlin also looks very similar to what we did in Scala:

fun weight(gravity: Double) = {mass: Double -> mass*gravity}
val weightOnEarth = weight(9.81)
println(weight(9.81)(80.0))
println(weightOnEarth(80.0))

For classes that only hold data, Kotlin provides an option of creating data classes. The benefit of using this is far less boilerplate code. Moreover, we can declare the properties of a data class as values to make it immutable:

data class Planet(val gravity: Double)
val earth = Planet(9.81)
earth.gravity = 10 // Illegal assignment

Finally, Kotlin also supports tail-call optimization for tail recursion. We have to mark the function with a special modifier:

fun factorial(x: Int): Int {
  tailrec fun next(x: Int, result: Int): Int {
    return if (x == 1) result
      else next(x - 1, result * x)
  }
  return next(x, 1)
}

Similar to Scala, Kotlin performs compile-time optimization and replaces recursion with fast and efficient loop-based code.

6. Other Language Features

Apart from the features we’ve already covered, there are a few additional features worth discussing. This is especially important as they set both Scala and Kotlin distinctly apart from Java.

6.1. Null Safety

In Java, we can assign any reference type to a special value called null. While this indicates that the type has no value, it has been a source of numerous criticisms in Java. In general, the invention of the null reference in programming by Tony Hoare has been described as a “billion-dollar mistake”.

Scala tries to improve upon this by forcing us to initialize all variables:

val x: String = "Hello World!" // Legal statement
val x: String // Illegal statement, we must initialize
val x: String = _ // Legal statement, initializes with default
val x: String = null // Legal statement

Although we may still initialize our variables with null, at least that would be deliberate.

Scala also offers an Option class that we can use as a carrier of single or no element of a given type. When a method can possibly return a null value, it’s better to use an Option:

def findCar(model: String): Option[Car] = {
  val cars = Map("Ford" -> new Car("Ford"))
  cars.get(model)
}

When we invoke this method, we’ll either receive Some(Car) or None, but never null. This also allows us to use powerful pattern matching to get the optional values apart.

Kotlin handles null values much more defensively, leaving practically very little room for errors. By default, it does not allow variables to take the value null:

val x: String = "Hello World!" // Legal statement 
val x: String // Illegal statement, we must initialize 
val x: String = null // Illegal statement

However, we can still declare a variable to be null, but as we’ve seen before, Kotlin separates them at the type system level:

val x: String? = null // This is legal statement

But, this places some restrictions on how we can actually use these nullable receivers:

println(x.length) // Illegal use of a nullable receiver

We have to use it either with safe or non-null asserted calls:

println(x?.length) // Safe call, will not result into null pointer exception
println(x!!.length) // Non-null asserted call, will result into null pointer exception

This still leaves room for error but makes it very explicit and clear.

6.2. Pattern Matching

Pattern matching is the process of checking a given sequence of tokens for the presence of the constituents of some pattern. It has several uses in any programming language, like deconstructing a value into its parts. We often accomplish it with the help of regular expressions.

Scala has very mature support for pattern matching. We frequently use it in the match expression, which is a powerful variant of the switch expression in Java:

def dayOfWeek(x: Int): String = x match {
  case 1 => "Monday"
  case 2 => "Tuesday"
  case _ => "other"
}

Although, we can realize the real power of pattern matching when we use it together with case classes:

abstract class Order
case class MailOrder(name: String, address: String) extends Order
case class WebOrder(name: String, email: String, phone: String) extends Order

def generateResponse(order: Order): String = {
  order match {
    case MailOrder(name, address) => s"Thank you $name, you will receive confirmation through post at: $address"
    case WebOrder(name, email, _) => s"Thank you $name, you will receive confirmation through email at: $email"
  }
}

Here, we can use pattern matching to identify the type and perform destructing of the values.

The support for pattern matching is not that sophisticated in Kotlin compared to Scala. We can use the when-block in Kotlin to achieve something similar:

fun dayOfWeek(x: Int): String {
    return when (x) {
        1 -> "Monday"
        2 -> "Tuesday"
        else -> "other"
    }
}

Of course, we can use the when-block in many different ways, but it can’t substitute for complete pattern matching.

6.3. Exception Handling

Exception handling, in general, refers to the process of responding gracefully to anomalous conditions in programming. Java provides the capability to define either checked or unchecked exceptions. While the former is checked at compile-time, the latter is not. However, the presence of checked exceptions has drawn criticism in Java for quite some time.

Scala provides a mechanism to declare and use unchecked exceptions. But unlike Java, Scala does not have checked exceptions. Let’s see how we can use checked exceptions in Scala:

def divide(x: Double, y: Double): Double = {
  if (y == 0) throw IllegalArgumentException
  x / y
}

def performDivision(x: Double, y: Double) = {
  try {
    divide(x, y)
  } catch {
    case ex: IllegalArgumentException =>
      println(ex.getMessage)
  } finally {
    println("Performing cleanup")
  }
}

This is pretty standard, except for the pattern matching we can use in the catch block to handle any exception in a single block.

Similarly, Kotlin also allows us to use unchecked exceptions but doesn’t have checked exceptions. The features it offers are pretty much the same as Scala, except for the pattern matching:

fun divide(x: Double, y: Double): Double {
    if (y == 0.0) throw IllegalArgumentException("Illegal argument")
    return x / y
}

fun performDivision(x: Double, y: Double) {
    try {
        divide(x, y)
    } catch (ex: IllegalArgumentException) {
        println(ex.message)
    } finally {
        println("Performing cleanup")
    }
}

Additionally, we can also use throws or try-catch blocks as expressions in Kotlin.

7. Conclusion

To sum up, in this tutorial, we went through the feature comparison of the Scala and Kotlin programming languages. This covered the comparison of their type systems, their features supporting object-oriented programming, and those supporting functional programming.

Comments are closed on this article!