1. Introduction

In Kotlin, handling nullability is critical to writing safe and robust code. Nullable types introduce the possibility of null references, which must be handled carefully, especially when handling comparisons.

In this tutorial, we’ll explore different techniques for comparing nullable integers in Kotlin.

2. Understanding the Problem

Greater than and less than comparisons are fundamental operations in any programming language. In Kotlin, these comparisons typically look like this:

val int1 = 1
val int2 = 2

val isInt1GreaterThanInt2 = int1 > int2
val isInt1LessThanInt2 = int1 < int2

These comparisons use the compareTo() operator function under the hood. This function compares two values and returns an integer indicating the order:

  • A positive number if the first value is greater than the second
  • Zero if the values are equal
  • A negative number if the first value is lesser than the second

However, we can’t use these operators if the integers are nullable. If either of the integers is null, attempting to use the comparison operators results in a compile-time error. Therefore, we need alternative methods to perform these comparisons in a null-safe way.

3. Using compareTo()

Kotlin provides a straightforward way to compare nullable integers using the compareTo() method. By using the safe-call operator, we can ensure that the comparison only occurs if the nullable Int isn’t null:

fun compareTwoInts(a: Int?, b: Int?): Int? {
    return a?.compareTo(b ?: return null)
}

This approach is simple and effective for direct comparisons. The nullability is handled by safe-calling compareTo() on a and returning null if b is null with the elvis operator.

Let’s test this to see how this works:

@Test
fun `compare nullable integers using compareTo`() {
    val a: Int? = 5
    val b: Int? = 3
    val c: Int? = null
    assertEquals(0, compareTwoInts(a, a))
    assertEquals(1, compareTwoInts(a, b))
    assertEquals(-1, compareTwoInts(b, a))
    assertEquals(null, compareTwoInts(a, c))
    assertEquals(null, compareTwoInts(c, b))
}

4. Creating an Extension Function

To make the comparison reusable and encapsulate the null-check logic, we can create an extension function on the nullable Int type:

fun Int?.isGreaterThan(other: Int?): Boolean? { 
    return this?.let { 
         it > (other ?: return null)
    } 
}

The extension function isGreaterThan() encapsulates the null-checking logic, making our main codebase cleaner. We also return null if either Int is null.

Now, let’s see how this extension function works in a test:

@Test
fun `compare nullable integers using extension function`() {
    val a: Int? = 5
    val b: Int? = 3
    val c: Int? = null

    assertEquals(true, a.isGreaterThan(b))
    assertEquals(false, b.isGreaterThan(a))
    assertEquals(null, c.isGreaterThan(a))
    assertEquals(null, c.isGreaterThan(c))
}

4.1. Creating a Generic Comparison Function

For more advanced use cases, we can generalize the comparison function to work with any type that implements the Comparable interface:

fun <T : Comparable<T>> T?.isGreaterThan(other: T?): Boolean? {
    return this?.let { it > (other ?: return null) }
}

Let’s also see how this generic comparison function works with an Int, just like our previous tests:

@Test
fun `compare generic Int comparable types using extension function`() {
    val a: Int? = 5
    val b: Int? = 3
    val c: Int? = null

    assertEquals(true, a.isGreaterThan(b))
    assertEquals(false, b.isGreaterThan(a))
    assertEquals(false, b.isGreaterThan(b))
    assertEquals(null, c.isGreaterThan(b))
}

This approach works for any Comparable, so we can use it with other comparable types, such as a String:

@Test
fun `compare generic String comparable types using extension function`() {
    val a: String? = "ABC"
    val b: String? = "ZXY"
    val c: String? = null

    assertEquals(true, b.isGreaterThan(a))
    assertEquals(false, a.isGreaterThan(b))
    assertEquals(false, b.isGreaterThan(b))
    assertEquals(null, c.isGreaterThan(b))
}

5. Leveraging Kotlin’s compareValues() Function

Kotlin provides the compareValues() function, which is specifically designed for comparing two nullable values:

val result = compareValues(a, b)

This function handles nullability internally, making the comparison straightforward and eliminating the need for manual null checks. This function considers null to be less than any other value.

Let’s see how this function works:

@Test
fun `compare nullable integers using compareValues function`() {
    val a: Int? = 5
    val b: Int? = 3
    val c: Int? = null

    assertEquals(1, compareValues(a, b))
    assertEquals(-1, compareValues(b, a))
    assertEquals(1, compareValues(a, c))
    assertEquals(-1, compareValues(c, a))
    assertEquals(0, compareValues(c, c))
}

The compareValues() function is unique because it recognizes two nulls as equal, returning zero when both values are null. This contrasts with our previous implementations, which defaulted to returning null when either value was null.

Finally, the behavior of compareValues() is unique because it always returns a non-null comparable result and treats two nulls as equals.

6. Conclusion

Handling nullable types in Kotlin requires careful consideration, especially when performing comparisons.

The techniques discussed in this article offer robust solutions for comparing nullable integers. Therefore, depending on our specific use case, we should select the approach that best suits our needs.

As always, the code used in this article is available over on GitHub.