Kotlin Training Program

DOWNLOAD APP

FEEDBACK

Null

null is a keyword that represents absence of value.

The variables we declared till now are non-null i.e. they must hold a value. Non-null variables must be assigned a value. For example, following program doesn’t compile :

 fun main() {
		var x: Int // ERROR! x must be assigned a value.
		println(x)
}
 

It is not necessary to assign the value only while declaring the variable. We can assign anywhere before the first usage. Following program is valid even though value is not assigned while declaration:

 fun main() {
		var x: Int
		println("hello")
		println("null")
		
		x = 10  // Works! Value is assigned before usage
		println(x)
}
 

There are certain usecases when we can’t assign a value to variable. In other words, value can be absent in variable. We can use Nullable variables in such scenarios.

Nullable variable is a variable that can hold null value i.e. value can be absent.

Need

Lets understand the need of nullable variables with the help of some examples :

Safe String index access

Consider the following program that inputs an alphabet from user and prints its opposite alphabet. Ex. - A → Z, B → Y, C → X etc.

 fun main() {
    // Prompt
    print("Enter an alphabet : ")

    // Input alphabet
    val char = readln()  // Read input string
        .first()         // Get the first character of it
        .uppercaseChar() // Convert it to uppercase

    // Find the opposite of alphabet
    val opposite = Char('Z'.code - (char.code - 'A'.code))

    // Print
    println("$char -> $opposite")
}

/* Output :

Enter an alphabet : D
D -> W
 */
 

Here we made an assumption that the string entered by the user will be non-empty. Turns out that user can enter a empty string which leads to a runtime error :

 Enter an alphabet : 
Exception in thread "main" java.util.NoSuchElementException: Char sequence is empty.
	at kotlin.text.StringsKt___StringsKt.first(_Strings.kt:71)
 

Trying to read the first character of empty input string leads to this exception. Following will also throw the same exception :

 val firstChar = "".first()
 

We can fix this issue by adding a check for empty string :

 fun main() {
    // Input alphabet
    val char = inputAlphabet("Enter an alphabet : ")

    // Find the opposite of alphabet
    val opposite = Char('Z'.code - (char.code - 'A'.code))

    // Print
    println("$char -> $opposite")
}

fun inputAlphabet(
    prompt: String
): Char {
    print(prompt)
    val input = readln()  // Read input string

    // Empty check
    if (input.isEmpty()) {
        println("Empty input!")
        return inputAlphabet(prompt)
    }

    // Alphabet check
    val char = input.first()
    if (!char.isLetter()) {
        println("Invalid input!")
        return inputAlphabet(prompt)
    }

    // Valid input! Convert it to uppercase & return
    return char.uppercaseChar()
}

/* Output :

Enter an alphabet : 
Empty input!
Enter an alphabet : 1
Invalid input!
Enter an alphabet : S
S -> H
 */
 

A new function inputAlphabet() is defined which prompts the user to input and an alphabet. It returns only when a valid alphabet is entered by the user.

We can define the inputAlphabet() function in a concise way using nullable variables :

 fun inputAlphabet(
    prompt: String
): Char {
    print(prompt)

    // Input char and empty check
    val char = readln().firstOrNull()
        ?: run {
            println("Empty input!")
            return inputAlphabet(prompt)
        }

    if (!char.isLetter()) {
        println("Invalid input!")
        return inputAlphabet(prompt)
    }

    // Valid input! Convert it to uppercase & return
    return char.uppercaseChar()
}
 

Note :

  • We have used firstOrNull() instead of first() function. If first character of string is not found (in case of empty string), null will be returned. This makes the char variable nullable. It may or may not contain a character.
  • We have defined the null case for char using Elvis operator ?: and run block (We will learn more about them in the following sections). If char is null, run block will be executed which returns. So implicitly, the type of char is Char and not Char? because we are returning in case of null. However in intermediate step, use of nullable variable is involved.

Similarly, last() and get() functions of String throw exception when that character does not exist. Instead, we can use lastOrNull() and getOrNull() functions to avoid exceptions.

 // Non-null variables
val x: Char = "ABC"[3] // Throws IndexOutOfBoundsException
val p: Char = "".last() // Throws NoSuchElementException

// Nullable variables
val y: Char? = "ABC".getOrNull(3) // Safe access
val q: Char? = "".lastOrNull() // Safe access
 

Safe List index access

Consider the following program that inputs month number from user and prints the corresponding month name using List :

 fun main() {
    // Months list to lookup from
    val months = listOf(
        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
    )

    // Prompt and input monthNo
    print("Enter month number: ")
    val monthNo = readln().toInt()
    
    // Lookup
    val monthName = months[monthNo - 1]
    
    // Print
    println("Month #$monthNo is $monthName")
}

/* Output :

Enter month number: 12
Month #12 is Dec
 */
 

But the above program throws exceptions on certain inputs :

  • Empty input :

     Enter month number: 
    Exception in thread "main" java.lang.NumberFormatException: For input string: ""
    	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
     
  • Non-numeric input :

     Enter month number: A
    Exception in thread "main" java.lang.NumberFormatException: For input string: "A"
    	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
     
  • Month number out of 1…12 range :

     Enter month number: 15
    Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 14 out of bounds for length 12
    	at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4351)
     

We can improve this program using guard code (checks) :

 fun main() {
    // Months list to lookup from
    val months = listOf(
        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
    )

    val monthNo = inputInt("Enter month number: ", 1..12)

    // Lookup
    val monthName = months[monthNo - 1]

    // Print
    println("Month #$monthNo is $monthName")
}

fun inputInt(prompt: String, acceptableRange: IntRange): Int {
    return try {

        // Prompt and input int
        print(prompt)
        val int = readln().toInt()

        // Validate
        if (int !in acceptableRange) {

            // Notify
            println("Invalid input! Acceptable range = $acceptableRange")

            // Retry
            inputInt(prompt, acceptableRange)
        } else {
            // Return
            int
        }
    } catch (e: NumberFormatException) {

        // Notify
        println("Invalid input!")

        // Retry
        inputInt(prompt, acceptableRange)
    }
}

/* Output :

Enter month number:
Invalid input!
Enter month number: A
Invalid input!
Enter month number: 43
Invalid input! Acceptable range = 1..12
Enter month number: 8
Month #8 is Aug
 */
 

Note :

  • A new function inputInt() is defined which takes prompt and acceptableRange as input. It returns the input int only when it falls in acceptableRange and asks to retry otherwise.
  • We have used try-catch block to catch NumberFormatException which is thrown in case of empty and non-numeric inputs.
  • in operator is used to check whether input int is in acceptableRange.

The above program works as expected but code length is increased drastically just to perform additional checks. We can perform the same validation using nullable variables.

 fun main() {
    monthNumberToName()
}

fun monthNumberToName() {
    // Months list to lookup from
    val months = listOf(
        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
    )

    // Prompt and input month number
    print("Enter month number: ")
    val monthNo = readln().toIntOrNull()
        ?: run {
            // Notify
            println("Invalid input!")

            // Retry
            return monthNumberToName()
        }

    // Lookup
    val monthName = months.getOrNull(monthNo - 1)
        ?: run {
            // Notify
            println("Invalid month number!")

            // Retry
            return monthNumberToName()
        }

    // Print
    println("Month #$monthNo is $monthName")
}
 

Note :

  • Here monthNo is a nullable variable which will be null in case of empty and non-numeric inputs.
  • We have defined the null case for monthNo using Elvis operator ?: and run block (We will learn more about them in the following sections). If monthNo is null, run block will be executed which returns. So implicitly, the type of monthNo is Int and not Int? because we are returning in case of null. However in intermediate step, use of nullable variable is involved.
  • We have used List#getOrNull() function to lookup, instead of unsafe List#get() function. Here monthName is a nullable variable which will be null in case of out of bounds indices. Here also, null case is defined to allow retry.

Safe Map key access

Consider the following program that inputs name of mountain and prints its height using Map :

 fun main() {
    // Data
    val mountainNameToHeightMap = mapOf(
        "Mount Everest" to 8848,
        "Kangchenjunga" to 8586,
        "K2" to 8611
    )

    // Prompt & input Mountain name
    print("Enter mountain name : ")
    val mountain = readln()

    // Lookup
    val height = mountainNameToHeightMap[mountain]

    //Print
    println("Height of $mountain is $height meters")
}

/* Output :

Enter mountain name : K2
Height of K2 is 8611 meters
 */
 

The above program prints weird output in case of empty and invalid mountain names :

 Enter mountain name : 
Height of  is null meters

Enter mountain name : ABC
Height of ABC is null meters
 

This is because map lookups always return nullable variables. null is returned in case key is not found in map. Hence we see null meters in output. Here we have Map<String, Int> so lookups return Int? :

 val height: Int? = mountainNameToHeightMap[mountain]
 

We can define the null case to avoid the weird outputs :

 fun main() {
    mountainLookup()
}

fun mountainLookup() {
    // Data
    val mountainNameToHeightMap = mapOf(
        "Mount Everest" to 8848,
        "Kangchenjunga" to 8586,
        "K2" to 8611
    )

    // Prompt & input Mountain name
    print("Enter mountain name : ")
    val mountain = readln()

    // Lookup
    val height = mountainNameToHeightMap[mountain]
        ?: run {
            println("Mountain not found!")
            return mountainLookup()
        }

    //Print
    println("Height of $mountain is $height meters")
}

/* Output :

Enter mountain name : 
Mountain not found!
Enter mountain name : ABC
Mountain not found!
Enter mountain name : K2
Height of K2 is 8611 meters
 */
 

To make the program more concise we can use the Map#getOrElse() function for map lookup :

 // Lookup
val height = mountainNameToHeightMap.getOrElse(mountain) {
    // Notify not found
		println("Mountain not found!")

		// Retry
    return mountainLookup()
}
 

Note :

  • getOrElse() function takes two inputs :

     Map<K, V>.getOrElse(
    		key: K, 
    		defaultValue: () -> V
    ): V
     

    defaultValue is a lambda function which is executed in case the key is not found in the map. We use this lambda to notify the user and allow a retry.

Conclusion

In the above examples, we have seen that how nullable values provide a way to deal with variables which may or may not contain a value.

  • When taking inputs from user, nullable variables can be used such that they contain null in case of invalid input.
  • For safe list lookup using index i.e. without throwing exception, nullable variables can be used such that they contain null in case of out of bounds indices.
  • Map lookups by default use nullable variables to avoid exceptions and return null in case key is not found in the map.

Basics

Declaration

To declare a nullable variable, we use a ? postfixed to the type of variable :

 var x: Int?
var y: String?
 

However, they must be assigned before use. If we want a nullable variable to not hold any value, we must assign null to it.

 fun main() {
		// ERROR! Value not assigned
		var x: Int?
		
		// Assign null if no value
		var y: String? = null
}
 

Note that null can never be assigned to non-null variables :

 fun main() {
		val x: Int = null // ERROR! null can't be assigned to non-null variable
		
		var y = null   // Accepted but useless (type = Nothing?)
		y = 3 // Not allowed
}
 

Elvis operator ?:

Elvis operator is used with nullable variables to define a default value to be used when the value is null.

 /* nullable variable */ ?: /* default value */
 

Example :

 
fun main() {
		val a: Int? = null
		var b: String? = "ABC"
		
		println(a ?: 5)      // Prints "5" i.e. default value because a = null
		println(b ?: "DEF")  // Prints "ABC" i.e. assigned value because b != null
		
		b = null
		println(b ?: "DEF")  // Prints "DEF" i.e. default value because b = null
}
 

We can use the elvis operator to create a non-null variable from nullable variable :

 fun main() {
		// Nullable variable
		val input: Int? = readln().toIntOrNull()
		    
		// Non-null variable
		val size = input ?: 5
}
 

For null case, if we want to execute a block of code instead of directly providing a default value, we can use a run block :

 val size = input ?: run {
    println("using default size...")
    
    // Default value
    5
}
 

run is a function that takes a lambda function as input, invokes it and returns its output :

 run(
		block: () -> R
): R
 

Working with nullables

Consider the following program to convert a decimal number to binary number :

 fun main() {
    // Input a number
		print("Enter a number: ")
    val num: Int? = readln().toIntOrNull()

    // Convert to binary
    val binary = num.toString(2)

    // Print
    println("Binary($num) = $binary")
}
 

This program doesn’t compile due to the following compile time error :

 val binary = num.toString(2)

// ERROR! Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Int?
 

Note that num is a nullable variable i.e. it may or may not contain a value. If it does not contain a value, we can’t invoke its function.

If it contains a value, function will be invoked but the compilation fails because we need to explicitly specify what to do if it does not contain a value. We have two options to resolve this error :

Safe call operator ?.

We can use the safe call operator ?. to invoke the function of nullable object only if it contains a value and return null otherwise.

 val binary = num?.toString(2)

/* Output :

Enter a number: 
Binary(null) = null

Enter a number: 10
Binary(10) = 1010
 */
 

Note :

  • binary is also a nullable variable
  • num will be null in case of empty input. Because it was null, the toString(2) function was not invoked and binary is assigned null. Hence, we see Binary(null) = null
  • In case of valid inputs, num will be non-null so toString(2) will be invoked and binary will also be non-null. Hence, we see Binary(10) = 1010.

Another example :

 fun main() {
		var name: String? = null
		var uppercaseName = name?.uppercase() // name is null so function not invoked
		println(uppercaseName) // Prints null
		
		name = "himalayas"
		uppercaseName = name?.uppercase() // name is not-null so function is invoked
		println(uppercaseName) // Prints "HIMALAYAS"
}
 

Note :

  • uppercaseName is also a nullable variable

  • In the second case, we can remove the safe call because the compiler is smart enough to infer that name is not null at that point :

     name = "himalayas"
    uppercaseName = name.uppercase() // ?. not needed!
     

Not-null assertion operator !!

If we are very sure that a nullable variable will have a non-null value i.e. if we assert that a nullable variable is not-null, it is safe to use Not-null assertion operator !!. But if the value turns out to be null, a NullPointerException (NPE) is thrown.

 fun main() {
		var name: String? = null
		var uppercaseName = name!!.uppercase() // Throws NPE
		println(uppercaseName)
		
		// Exception in thread "main" java.lang.NullPointerException
}
 

Even though name is a nullable variable with null value, we are asserting it as non-null and invoking the uppercase() function. When we forcefully invoke a function on null value, we get a NullPointerException.

If the nullable variable actually holds a value, program runs as expected :

 fun main() {
		val name: String? = "himalayas"
		val uppercaseName = name!!.uppercase() // Works!
		println(uppercaseName) // Prints "HIMALAYAS"
}
 

⚠️ Avoid using Not-null assertion operator !! because it has to be used with immense care. If used without understanding the usecase, it may cause the program to crash due to NPE.

Conclusion

When accessing member of a nullable object, ?. operator tells the compiler to return null while !! operator tells the compiler to throw NPE if the object is null.