Getting the current Kotlin version is easy, but the actual KotlinVersion
class is much more interesting. This post shows how to get the Kotlin version programmatically, but then looks at the details of the KotlinVersion
class, including how it demonstrates a great way to write an equals
method and more.
Note that this demo is part of my new book, Kotlin Cookbook, from O’Reilly Media.

Also, I was lucky enough to be interviewed by Hadi Harriri on his Talking Kotlin podcast, and this class came up. That episode has not yet been released, but should be out soon depending on his current backlog.
To start, here is the trivial one-liner to find out which version of Kotlin is executing your code:
fun main() { println("The current Kotlin version is ${KotlinVersion.CURRENT}") }
At the time of this writing, the current release version of Kotlin is 1.3.50, and when you run this script on that version that’s what you get.
Where life gets interesting is when you look at how the KotlinVersion
class is implemented. The next series of snippets will examine that in some detail. The KotlinVersion
class in the standard library is in the kotlin
package, and begins:
public class KotlinVersion(val major: Int, val minor: Int, val patch: Int ) : Comparable<KotlinVersion> {
The class is marked public
, which isn’t necessary (public
is the default) but is typical of library classes. Then follows the primary constructor, which takes three integer values, labeled major
, minor
, and patch
. After the colon after the signature shows that the class implements the Comparable
interface for instances of itself, which is already pretty interesting. The Comparable
interface in Kotlin is just like its counterpart in Java. It establishes a “natural ordering” on instances of the class.
The Comparable
interface in the standard library consists of:
public interface Comparable<in T> { public operator fun compareTo(other: T): Int }
Again, the word public
is not necessary either on the class or the function, but doesn’t hurt anything. The compareTo
function is labeled an operator
function. In this case, the function is used whenever you use <, >, ==
, or one of the combination comparison functions, <=, >=
, or !=.
The function returns an integer whose value doesn’t matter, but should be negative, zero, or positive if the current object is less than, equal to, or greater than its argument.
Returning to the KotlinVersion
class, the implementation of compareTo
is:
override fun compareTo(other: KotlinVersion): Int = version - other.version
This assumes that the KotlinVersion
class has a version
property. The primary constructor didn’t show one, so the implementation provides a private property of that name and the function to compute it:
private val version = versionOf(major, minor, patch) private fun versionOf(major: Int, minor: Int, patch: Int): Int { require(major in 0..MAX_COMPONENT_VALUE && minor in 0..MAX_COMPONENT_VALUE && patch in 0..MAX_COMPONENT_VALUE) { "Version components are out of range: $major.$minor.$patch" } return major.shl(16) + minor.shl(8) + patch }
So the version
property is computed from the major
, minor
, and patch
values. The require
statement is a pre-condition that verifies the individual values fall within the required range. For all three, the minimum is zero and the maximum is MAX_COMPONENT_VALUE
. Like most constants, MAX_COMPONENT_VALUE
is found in the companion object:
companion object { public const val MAX_COMPONENT_VALUE = 255 @kotlin.jvm.JvmField public val CURRENT: KotlinVersion = KotlinVersion(1, 3, 50) }
Note the use of both const
and val
together. From a Java perspective, the properties inside the companion object are effectively static, and the val
keyword indicates they’re both final
as well. The const
modifier means the value is a compile-time constant, rather than being specified at runtime. The max value is therefore hard-wired to 255, and on this version of Kotlin the CURRENT
value is an instance of the KotlinVersion
class where major
, minor
, and patch
values are 1, 3, and 50, respectively.
That takes care of the require
block in the versionOf
function. What about the actual value it returns? That’s computed using the shift-left (or left-shift, but the other way reads better) operator function, shl
. That is an infix function that shifts the current value by the specified number of bits. Its signature is given by:
public final infix fun shl( bitCount: Int ): Int
This is an infix function, so the idiomatic way to invoke it would be major shl 16
, minor shl 8
, etc, but the form used here works anyway. Basically, the function shifts by two bytes for major
and one byte for minor
, which gives them enough of an offset that it is very unlikely a different set of major/minor/patch values will result in the same version. For the record, using 1, 3, and 50 gives 65,536 for 1 shl 16
and 768 for 3 shl 8
, and the sum from the versionOf
function is 66,354:
@Test fun `left-shift for major, minor, and patch of 1, 3, and 50`() { assertEquals(65536, 1 shl 16) assertEquals(768, 3 shl 8) assertEquals(66354, (1 shl 16) + (3 shl 8) + 50) }
The major
, minor
, and patch
values are therefore combined into a single integer, which is used for the ordering. The next interesting part is how the KotlinVersion
class implements the standard overrides of toString
, equals
, and hashCode
:
override fun toString(): String = "$major.$minor.$patch" override fun equals(other: Any?): Boolean { if (this === other) return true val otherVersion = (other as? KotlinVersion) ?: return false return this.version == otherVersion.version } override fun hashCode(): Int = version
There’s nothing terribly surprising about the overrides of toString
or hashCode
. The former is just formatting, and the latter reuses that version
calculation just discussed, which is pretty convenient since the left-shifted offset mechanism described above is exactly how Josh Bloch recommended creating a decent hashCode
function in his Effective Java book for the last twenty-some-odd years. 🙂
The real fun is in the override of the equals
function. The KotlinVersion
class, like all Kotlin classes, extends Any
. As a reminder, the Any
class looks like:
package kotlin public open class Any { public open operator fun equals(other: Any?): Boolean public open fun hashCode(): Int public open fun toString(): String }
Returning to the implementation of equals in KotlinVersion
, the first check uses the triple equals operator ===
to see if the current reference and the argument are both pointing to the same instance. If so, the result is equal and no further checking is necessary.
If the two objects are different, the code needs to check the version
property. Because the argument to the equals
function is nullable, you can’t simply access the version
property without checking for null
first. The safe cast operator, as?
, is used to check that. If the argument is not null, this casts it to an instance of KotlinVersion
. If the argument is null, the safe cast operator returns null, so the Elvis operator, ?:
, is used to simply return false
rather than continue.
That’s really interesting, actually. The “return false
” statement aborts the assignment of the otherVersion
property. The results is Nothing
, a class almost guaranteed to confuse Java developers, but beyond the scope of this discussion. Suffice it to say that because Nothing
is a subclass of every other class, the resulting type of otherVersion
is KotlinVersion
, as desired.
Assuming we make it to the last line in the equals
method, now there is a value for otherVersion
and a value for the current version
. The comparison then checks version == otherVersion.version
for equality (since they’re both Int
values) and returns the result.
That’s quite a lot of power for three lines of code, and is a great example of how to implement an equivalence check for a nullable property (which happens to be another recipe in the book).
To complete the story, the class has a secondary constructor that can be used when the patch value is unknown.
public constructor(major: Int, minor: Int) : this(major, minor, 0)
Then there are two overloads of the isAtLeast
function:
public fun isAtLeast(major: Int, minor: Int): Boolean = this.major > major || (this.major == major && this.minor >= minor) public fun isAtLeast(major: Int, minor: Int, patch: Int): Boolean = this.major > major || (this.major == major && (this.minor > minor || this.minor == minor && this.patch >= patch))
A couple of tests show how the basic comparisons work:
@Test fun `comparison of KotlinVersion instances work`() { val v12 = KotlinVersion(major = 1, minor = 2) val v1341 = KotlinVersion(1, 3, 41) assertAll( { assertTrue(v12 < KotlinVersion.CURRENT) }, { assertTrue(v1341 <= KotlinVersion.CURRENT) }, { assertEquals(KotlinVersion(1, 3, 41), KotlinVersion(major = 1, minor = 3, patch = 41)) } ) } @Test fun `versions are Ints less than max`() { val max = KotlinVersion.MAX_COMPONENT_VALUE assertAll( { assertTrue(KotlinVersion.CURRENT.major < max) }, { assertTrue(KotlinVersion.CURRENT.minor < max) }, { assertTrue(KotlinVersion.CURRENT.patch < max) } ) }
Because the KotlinVersion
class implements Comparable
, it can be used in a range and has a contains
method. In other words, you can write:
@Test fun `check current version inside range`() { assertTrue(KotlinVersion.CURRENT in KotlinVersion(1,2)..KotlinVersion(1,4)) }
What you can not do, however, is to iterate over that range, because ranges are not progressions, but that’s a post for another day.
I hope you enjoyed this deep dive into what is arguably a trivial, but highly instructive, class in the Kotlin library. The GitHub repository for the book is located here and contains this example as well as many, many others.
Leave a Reply