1. Overview

In software development, input validations and general checks about the status of the data are critical pieces needed for building reliable systems. In this tutorial, we’ll learn how to perform validations and assertions using built-in Preconditions functions available within the Kotlin standard library.

2. Preconditions

Kotlin provides a set of utility functions to simplify input validation in the Preconditions utility class. This collection of functions helps to minimize the need for excessive code by streamlining the input validation process.

2.1. The require() Function

The require() function takes two parameters. The first parameter, value, is a Boolean representing the condition to be fulfilled. The second one is the lazyMessage lambda, which would return an error message. Basically, if the first parameter evaluates to false, the require() function will throw an IllegalArgumentException set with the message defined in the lazyMessage lambda:

fun printPositiveNumber(num: Int) {
    require(num > 0) { "Number must be positive" }
    println(num)
}

The reason why the second parameter is a function is that if we include the interpolation operation in a function, it will only be executed if the precondition check fails, not during the actual function call.

2.2. The check() Function

Likewise, the check() function has the same parameters as require() to validate the condition we want to apply. However, the key difference is that if the first parameter evaluates to false, instead of throwing an IllegalArgumentException, check() throws an IllegalStateException.

Although this is a technical difference, it also implies that there’s a semantic difference in the usage. While require() is for validating arguments, check() is for validating state.

Now, to illustrate this concept, let’s consider the next example, in which check() verifies the state of a list of numbers to calculate the average:

class AverageCalculator() {
    private val numbers: MutableList<Int> = mutableListOf()
    
    fun add(number: Int) {
        numbers.add(number)
    }
    
    fun clear() {
        numbers.clear()
    }
    
    fun average(): Double {
        check(numbers.size > 0) { "Must have numbers to be able to perform an average." }
        println("Computing averages of numbers: $numbers")
        return numbers.average()
    }
}

2.3. The requireNotNull() Function

The requireNotNull() function verifies whether a nullable reference is null or not. If the reference is null, it will throw an IllegalArgumentException, otherwise, it’ll return the non-null value. Similarly to other Preconditions functions, requireNotNull() can also take the lazyMessage lambda to build the exception message:

class Person(name: String?, age: Int?) {
    val name: String = requireNotNull(name) { "Name must not be null" }
    val age: Int = requireNotNull(age) { "Age must not be null" }
}

2.4. The checkNotNull() Function

The checkNotNull() function throws an IllegalStateException if the value evaluated is null, otherwise, it returns the non-null value. This is, again, for semantic purposes to distinguish between argument and state checks:

fun processStrings(list: List<String?>): String {
    val nonNullStrings = mutableListOf<String>()
    for (string in list) {
        checkNotNull(string) { "The list must not contain null elements" }
        nonNullStrings.add(string)
    }
    return "Processed strings: $nonNullStrings"
}

2.5. The error() Function

The error() function directly throws an IllegalStateException with a message, without any checks. The definition of the function is specified as:

fun error(message: Any): Nothing

The Nothing return type indicates that the function never returns a value and always throws an exception.

The error() function is useful when we want to throw an exception intentionally in our code. For example, we can use it to throw an exception when an unexpected condition occurs in our program, such as when an invalid argument is passed to a function or when a resource is not found.

Let’s see an example of how to use the error() function:

fun divide(a: Int, b: Int): Int {
    if (b == 0) {
        error("Cannot divide by zero")
    }
    return a / b
}

Another example of using the error() function could be when we want to validate the object type of a JSON node in order to apply specific logic for each type. If the node type is not supported, we can throw the exception with error():

fun processJsonArrayOrObject(json: String): JsonNode {
    val genericJson: JsonNode = jacksonObjectMapper().readTree(json)

    when (genericJson) {
        is ArrayNode -> { /* we have a json list */ }
        is ObjectNode -> { /* we have an object */ }
        else -> error("This function only handles list and object wrappers in Json")
    }

    return genericJson
}

3. Conclusion

Kotlin’s standard library is packed with useful tools that can simplify our code and make it more idiomatic. In this article, we’ve explored some useful functions in the Preconditions class that help to perform input validations and check the state of the objects within our system.

As always, the complete code for this article is available over on GitHub.

Comments are closed on this article!