Java is one of the classic object-oriented languages, so classes and objects are of special interest to Java developers. Object-oriented programming gives you considerable power, particularly when it comes to structuring and organizing programs as they grow larger. That’s why most modern languages give you objects in one form or another.
On the other hand, defining and maintaining classes and their hierarchies can be complex, and it’s easy to get bogged down when programming with them. What’s needed is a compromise. Kotlin is one of the newer languages that works behind the scenes to simplify programming while still letting you access complexity when you need it.
How Kotlin simplifies classes
In my previous article, I showed an example of a class written in Kotlin. Here’s another:
data class StarWarsMovie(val title: String, val episode_id: Int, val release_date: String)
Believe it or not, this one line gives you a full-blown class. It is a data class, which provides common methods like equals
and toString
automatically. Otherwise, it acts just like any other class. This class models a Star Wars movie with the movie title, ID, and release date.
The thing to notice is that Kotlin reduces class creation down to the bare essence. In this example, all we provided was the class name and constructor arguments. Kotlin deduced everything else it needed based on the types and names of the arguments. A Kotlin class behaves just like a Java class, but without requiring that we write out the members explicitly.
Public members and methods
In Kotlin, members and methods are public by default, so we can create a class instance and directly access its properties:
val newHope = StarWarsMovie("A New Hope", 4, "1977-05-25")
println(newHope.id) // outputs “4”
It’s an interesting design difference between requiring a visibility declaration, as Java does, versus assuming public, as Kotlin does. This kind of streamlining makes for a simpler and faster language surface. On the flipside, if you want the safety of private members, Kotlin requires that you explicitly set that modifier.
To make one of the fields private, we could do this:
class StarWarsMovie(val title: String, private val episode_id: Int, val release_date: String)
println("test: " + newHope.episode_id) // Error
Function extensions
Another example of Kotlin’s dynamism is function extensions, or simply extensions. These let you add a function to an existing class or interface. This feature is well-known in the JavaScript world, but it really stands out in the context of Java-like classes. Here, we are adding a new method to our existing StarWarsMovie
class:
fun StarWarsMovie.releaseYear(): Int {
val year = release_date.substring(0, 4)
return year.toInt()
}
val newHope = StarWarsMovie("A New Hope", 4, "1977-05-25")
val releaseYear = newHope.releaseYear()
println("The release year of A New Hope is $releaseYear")
In the above example, we’ve defined a new method on the class, called releaseYear()
. Note that we defined it directly on the existing class: StarWarsMovie.releaseYear()
. We can do this with our own classes, but also with classes imported from third-party libraries. The Kotlin documentation shows an example of adding a method to the standard library. (I’m a bit wary of this kind of monkey patching but it certainly shows Kotlin’s flexibility.)
Now, imagine we wanted StarWarsMovie
to be a subclass of a Movie
superclass. In Kotlin, we could do something like this:
open class Movie(val title: String, val releaseDate: String) {
open fun releaseYear(): Int {
val year = releaseDate.substring(0, 4) return year.toInt()
}
}
class StarWarsMovie(title: String, episodeId: Int, releaseDate: String) : Movie(title, releaseDate) {
val episodeId: Int = episodeId
}
The open
keyword indicates that a class or function is available for subclassing or overriding. In Java terms, Kotlin classes are final by default. Default public members and default final classes could be interpreted as subtle encouragement to prefer composition over inheritance. In the preceding example, we used constructor-based declaration for both StarWarsMovie
and Movie
. The colon in : movie
indicates extension, working similarly to Java’s extends
keyword.
Making class declarations explicit
You also have the option to declare things more explicitly in Kotlin, more along the lines of Java:
class Movie {
var title: String = ""
var releaseDate: String = ""
constructor(title: String, releaseDate: String) {
this.title = title
this.releaseDate = releaseDate
}
fun releaseYear(): Int {
val year = releaseDate.substring(0, 4)
return year.toInt()
}
}
class StarWarsMovie(title: String, releaseDate: String, val episodeId: Int) : Movie(title, releaseDate) {
// ...
}
Here we’ve opened up the class, which to a Java developer is self-explanatory. The constructor is defined with the constructor
keyword instead of the class name. This can be combined with the default constructor style I showed earlier if alternate constructors are needed:
class Movie(title: String, releaseDate: String) {
var title: String = title
var releaseDate: String = releaseDate
constructor(title: String) : this(title, "") {
println("Secondary constructor called")
}
}
In general, it’s pretty easy to mix and match Kotlin’s object-oriented syntaxes, opting for the simpler syntax when you can and expanding into the more detailed syntax when you must.
Associated with the notion of constructors, Kotlin provides an init
keyword that lets you run code during object creation. Outside of init
, only property declarations are allowed. Kotlin differentiating properties from logic during construction is different from Java:
class StarWarsMovie(title: String, episodeId: Int, releaseDate: String) : Movie(title, releaseDate) {
val episodeId: Int = episodeId
init { println("Made a new star wars movie: $episodeId") } // un-Java-like init block
}
Ad hoc class declarations
One of the most freeing of Kotlin’s improvements is the ability to declare types, classes, and functions ad hoc, wherever you are in the code. This makes it simpler to think, “I’m going to need a class here,” and immediately write one. You can then return to your overall task in the same spot and use the new class. Later, you can go back and refine the class and possibly extract it to its own file, just like you would in Java.
Singleton-style objects
Kotlin lets you define a singleton-style object with the object
keyword:
object MovieDatabase {
private val movies = mutableListOf()
fun addMovie(movie: Movie) {
movies.add(movie)
}
fun getMovies(): List {
return movies.toList()
}
}
Objects like this can only be declared top-level, not nested inside other blocks.
A singleton means there’s only one instance globally for a given run of the program. This is a common pattern in Java and Java-based frameworks like Spring. You have references that you pass around, but they all refer to the same MovieDatabase
:
val db = MovieDatabase;
db.addMovie(movie)
db.addMovie(starWarsMovie)
// some other remote part of your app:
println("Got the same instance: ${db.getMovies()}")
The point here is that everyone gets a handle to the same instance. If we run this code immediately, we’ll get ugly output:
Got the same instance: Movie@3b764bce StarWarsMovie@759ebb3d
Your first thought might be to make Movie
a data class, to get the toString()
method for free, but data classes are final. A better option is to add a simple toString
ourselves:
open class Movie(val title: String, val releaseDate: String) {
override fun toString(): String {
return "Movie(title='$title', releaseYear=${releaseYear()})"
}
}
Now we’ll get nicer output:
Got the same instance: Movie(title='The Shawshank Redemption', releaseYear=1994) Movie(title='A New Hope', releaseYear=1977)
Concurrency and coroutines in Kotlin
Concurrency is another area where Kotlin brings interesting options to the table. Java has been very active lately in improving its concurrency model, and some of the improvements are inspired by Kotlin’s coroutines. Coroutines give you a different kind of access to threading. One of the mechanisms used is structured concurrency, which is now being added to Java.
Coroutines and structured concurrency keep your thread management in a synchronistic, declarative style. Here’s a simple example from Kotlin’s documentation:
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}
// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}
The suspend
keyword indicates that the doWorld
function can be suspended by the concurrency engine, meaning that method is non-blocking. You’ll notice the main function is set to the runBlocking
function, which establishes a blocking CoroutineScope
. All concurrent code must be declared inside a CoroutineScope
.
The actual mechanism for launching a thread is the launch
keyword, which is used by the main method to call doWorld
.
Obviously, we are just scratching the surface of concurrency and coroutines in Kotlin, but these examples give you a sense of how coroutines work in relation to Java. As I mentioned, Java is actually moving closer to Kotlin in this area. Another point to note about Kotlin’s concurrency is the JVM’s introduction of virtual threads. This feature implies the possibility of using Kotlin’s coroutines with the JVM’s virtual threads.
Conclusion
Kotlin modifies some of Java’s rules to introduce more flexibility and improve areas where coding can become bogged down. Whereas Java code tends toward the rigorous and explicit, Kotlin leans towards flowing and conventional. The great thing is that Kotlin gives you this wonderfully expressive modern language that you can use right along with Java, in the JVM. I think most Java developers will appreciate what they can do with Kotlin.