Lets understand the need of nullable variables with the help of some examples :
Safe String index access
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 offirst()
function. If first character of string is not found (in case of empty string),null
will be returned. This makes thechar
variable nullable. It may or may not contain a character. - We have defined the
null
case forchar
using Elvis operator?:
andrun
block (We will learn more about them in the following sections). Ifchar
isnull
,run
block will be executed which returns. So implicitly, the type ofchar
isChar
and notChar?
because we are returning in case ofnull
. 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
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 takesprompt
andacceptableRange
as input. It returns the input int only when it falls inacceptableRange
and asks to retry otherwise. - We have used
try-catch
block to catchNumberFormatException
which is thrown in case of empty and non-numeric inputs. in
operator is used to check whether input int is inacceptableRange
.
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 benull
in case of empty and non-numeric inputs. - We have defined the
null
case formonthNo
using Elvis operator?:
andrun
block (We will learn more about them in the following sections). IfmonthNo
isnull
,run
block will be executed which returns. So implicitly, the type ofmonthNo
isInt
and notInt?
because we are returning in case ofnull
. However in intermediate step, use of nullable variable is involved. - We have used
List#getOrNull()
function to lookup, instead of unsafeList#get()
function. HeremonthName
is a nullable variable which will benull
in case of out of bounds indices. Here also,null
case is defined to allow retry.
Safe Map key access
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
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.