Navigating Kotlin Nullability: A Comprehensive Guide to Enhance Code Clarity and Reliability

Table of Contents

Null pointer exceptions (NullPointerExceptions) are a common source of errors in programming languages, causing applications to crash unexpectedly. Kotlin, a modern programming language developed by JetBrains, addresses this issue by incorporating null safety as a core feature of its type system. By distinguishing between nullable and non-nullable types, Kotlin enables developers to catch potential null pointer exceptions at compile time, resulting in more robust and reliable code. In this article, we will take an eagle-eye view of Kotlin’s nullability system, along with the tools and techniques provided to handle them effectively. We will also delve into the nuances of working with nullable types when mixing Kotlin and Java code.

Understanding Nullabiliy

Nullability in Kotlin is a crucial feature that prevents NullPointerException errors. These errors often provide vague error messages like “An error has occurred: java.lang.NullPointerException” or “Unfortunately, the application X has stopped,” causing inconvenience for both users and developers.

Modern languages, including Kotlin, aim to transform these runtime errors into compile-time errors. By incorporating nullability into the type system, Kotlin’s compiler can detect potential errors during compilation, significantly reducing the occurrence of runtime exceptions.

In the following sections, we will explore nullable types in Kotlin. We’ll examine how Kotlin identifies values that can be null and delve into the tools provided by Kotlin to handle nullable values. Additionally, we will discuss the specifics of working with nullable types when mixing Kotlin and Java code.

Nullable types

A type in programming determines the possible values and operations that can be performed on those values. For example, in Java, the double type represents a 64-bit floating-point number, allowing standard mathematical operations. On the other hand, the String type in Java can hold instances of the String class or null, with different operations available for each.

However, Java’s type system falls short when it comes to nullability. Variables declared with a specific type, such as String, can still hold null values, leading to potential NullPointerException errors. While annotations like @Nullable and @NotNull can help detect and mitigate these errors, they are not consistently applied, and their use doesn’t entirely solve the problem.

To address this issue, Kotlin provides nullable types and introduces the safe-call operator (We will discuss those in much greater detail later on), ?. The safe-call operator allows combining null checks and method calls into a single operation. For example, the expression s?.toUpperCase() is equivalent to if (s != null) s.toUpperCase() else null. The result type of such an invocation is nullable, denoted by appending a question mark to the type (e.g., String?).

So, Nullable types in Kotlin are used to indicate variables or properties that are allowed to have null values. When a variable is nullable, calling a method on it can be unsafe, as it may result in a NullPointerException.

In Kotlin, you can indicate that variables of a certain type can store null references by adding a question mark after the type declaration. For example, String?, Int?, and MyCustomType? are all nullable types.

Here above Figure show, a variable of a nullable type can store a null reference.

By default, regular types in Kotlin are non-null, which means they cannot store null references unless explicitly marked as nullable. However, when dealing with nullable types, the set of operations that can be performed on them becomes restricted. Let’s explore a few examples to understand the problems that can arise and their solutions.

Assigning null to a non-null variable:

Kotlin
val x: String? = null
var y: String = x // Error: Type mismatch - inferred type is String? but String was expected

In the above example, we try to assign a nullable type (x) to a variable of a non-null type (y), resulting in a type mismatch error during compilation.

Passing nullable type as a non-null parameter:

Kotlin
fun strLen(str: String) {
    // Function logic
}

val x: String? = null
strLen(x) // Error: Type mismatch - inferred type is String? but String was expected

In the above example, we attempt to pass a nullable type (x) as an argument to a function (strLen()) that expects a non-null parameter. This results in a type mismatch error during compilation.

To handle nullable types, you can perform null checks to compare them with null. The Kotlin compiler remembers these null checks and treats the value as non-null within the scope of the check.

Performing a null check:

Kotlin
val x: String? = null

if (x != null) {
    // Code block within the null check
    // Compiler treats 'x' as non-null here
}

In the above example, by adding a null check, the compiler recognizes that the value of x is handled for nullability within the code block. The compiler will treat x as non-null within that scope, allowing further operations without type mismatch errors.

By distinguishing nullable and non-null types, Kotlin’s type system offers clearer insights into allowed operations and potential exceptions at runtime. Notably, nullable types in Kotlin do not introduce runtime overhead as all checks are performed during compilation.

By utilizing null checks, you can effectively work with nullable types and ensure your code compiles correctly.

Null Safety Tools in Kotlin

Now let’s see how to work with nullable types in Kotlin and why dealing with them is by no means annoying. We’ll start with the special operator for safely accessing a nullable value.

Safe call operator: “?.”

The safe-call operator, ?., is a powerful tool in Kotlin that combines null checks and method calls into a single operation. It allows you to call methods on non-null values while gracefully handling null values.

For example, consider the expression s?.toUpperCase(). This is equivalent to the more verbose code if (s != null) s.toUpperCase() else null. The safe-call operator ensures that if the value s is not null, the toUpperCase() method will be executed normally. However, if s is null, the method call is skipped, and the result will be null.

Here’s an example usage of the safe-call operator:

Kotlin
fun printAllCaps(s: String?) {
    val allCaps: String? = s?.toUpperCase()
    println(allCaps)
}

In the printAllCaps() function, the s?.toUpperCase() expression safely calls the toUpperCase() method on s if it\’s not null. The result, allCaps, will be of type String?, indicating that it can hold either a non-null uppercase string or a null value.

The safe-call operator is not limited to method calls; it can also be used for accessing properties. Additionally, you can chain multiple safe-call operators together for more complex scenarios. Let’s see the below example.

Kotlin
class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun Person.countryName(): String {
    val country = this.company?.address?.country
    return if (country != null) country else "Unknown"
}

We have three classes: Address, Company, and Person. The Address class represents a physical address with properties such as streetAddress, zipCode, city, and country. The Company class represents a company with properties name and address, where address is an instance of the Address class. The Person class represents a person with properties name and company, where company is an instance of the Company class.

The Person class also has an extension function called countryName(), which returns the country name associated with the person’s company. The function uses the safe-call operator ?. to access the country property of the address property of the company. If any of these properties are null, the result will be null.

Kotlin
val person = Person("amol", null)
println(person.countryName())

Here, we created an instance of the Person class named person with the name of “amol” and a null company. When we call the countryName() function on person, it tries to access the company property, which is null. Consequently, the address and country properties will also be null.

Therefore, the output of println(person.countryName()) will be “Unknown” because the safe-call operator ensures that if any part of the chain (company, address, or country) is null, the overall result will be null. In this case, since the company is null, the country is considered unknown.

The countryName() function demonstrates how to safely navigate through a chain of nullable properties using the safe-call operator, providing a default value (“Unknown” in this case) when any of the properties in the chain are null. This approach helps avoid null pointer exceptions and handle null values gracefully.

Elvis operator: “?:”

In Kotlin, you can eliminate unnecessary repetition when dealing with null checks using the Elvis operator ?:. This operator provides a default value instead of null.

Here’s how it works:

Kotlin
fun foo(s: String?) {
    val t: String = s ?: ""
}

In the above example, If “s” is null, the result is an empty string.

The Elvis operator takes two values, and its result is the first value if it isn’t null, or the second value if the first one is null.

When used in conjunction with the safe-call operator (?.), the Elvis operator (?:) in Kotlin serves the purpose of providing an alternative value instead of null when the object on which a method is called is null.

Kotlin
val name: String? = null
val length = name?.length ?: 0

In the above code, the variable name is nullable, and we want to obtain its length. However, if name is null, calling length directly would result in a NullPointerException. To handle this situation, we use the safe-call operator (?.) to safely access the length property of name.

Additionally, we employ the Elvis operator (?:) to provide a fallback value of 0 in case name is null. So, if name is not null, its length will be assigned to the variable length. Otherwise, length will be assigned the value 0.

This way, we avoid potential NullPointerException errors and ensure that length always has a valid value, even if name is null.

Let’s see one more example, the countryName() function from the previous code listing can be simplified to a single line using the Elvis operator:

Kotlin
fun Person.countryName() = company?.address?.country ?: "Unknown"

In this case, if any part of the chain (company, address, or country) is null, the default value “Unknown” will be used.

The Elvis operator is particularly handy in Kotlin because operations like return and throw can be used on its right side. If the value on the left side is null, the function will immediately return a value or throw an exception, allowing for convenient precondition checks.

Here’s an example of using the Elvis operator in the printShippingLabel() function:

Kotlin
fun printShippingLabel(person: Person) {
    val address = person.company?.address ?: throw IllegalArgumentException("No address")
    with (address) {
        println(streetAddress)
        println("$zipCode $city, $country")
    }
}

In this function, if there’s no address, it throws an IllegalArgumentException with a meaningful error message instead of a NullPointerException. If an address is present, it prints the street address, ZIP code, city, and country.

Using the with function helps avoid repeating address four times in a row.

Example usage:

Kotlin
val address = Address("NDA Road", 411023, "Pune", "India")
val softAai = Company("softAai", address)
val person = Person("amol", softAai)

printShippingLabel(person)
// Output:
// NDA ROAD
// 411023 Pune, India

printShippingLabel(Person("xyz", null))
// Output:
// java.lang.IllegalArgumentException: No address

Overall, the Elvis operator in Kotlin allows you to provide default values instead of null, simplifying null checks and eliminating unnecessary repetition. It can be combined with the safe-call operator and used in conjunction with return and throw to handle null values and perform meaningful error reporting.

Safe casts: “as?”

In Kotlin, the safe-cast operator (as?) serves as a safe version of the instanceof check in Java. It allows you to safely check and cast an object to a specific type without throwing a ClassCastException if the object does not have the expected type.

The regular Kotlin operator for type casts is the as operator, which throws a ClassCastException if the value doesn\’t have the specified type.

On the other hand, the as? operator attempts to cast a value to the specified type and returns null if the value doesn’t have the proper type.

The safe-cast operator is commonly used in conjunction with safe calls (?.) and Elvis operators (?:). This combination helps handle situations where you want to perform type checks on nullable objects and provide a default value or behavior when the object is not of the expected type.

A most common pattern is to combine the safe cast (as?) with the Elvis operator, which is useful for implementing the equals method.

Here’s an example implementation of the equals method using the safe cast and Elvis operator:

Kotlin
class Person(val firstName: String, val lastName: String) {
    override fun equals(other: Any?): Boolean {
        val otherPerson = other as? Person ?: return false
        return otherPerson.firstName == firstName && otherPerson.lastName == lastName
    }

    override fun hashCode(): Int = firstName.hashCode() * 37 + lastName.hashCode()
}

In this example, the equals method checks if the parameter other has the proper type (Person) using the safe cast operator (as?). If the type isn’t correct, it immediately returns false using the Elvis operator (?:).

Example usage:

Kotlin
val p1 = Person("amol", "pawar")
val p2 = Person("amol", "pawar")

println(p1 == p2) // Output: true
println(p1.equals(42)) // Output: false

With this pattern, you can easily check the type of the parameter, perform the cast, and return false if the type is incorrect, all in the same expression.

Kotlin provides a safe-cast operator (as?) that allows you to perform type checks and casts without throwing ClassCastException. It can be combined with the Elvis operator to handle cases where the type is not correct, providing a concise and safe way to perform type checks in Kotlin.

Not-null assertions: “!!”

In Kotlin, you have several tools to handle null values, such as the safe-call operator (?.), safe-cast operator (as?), and Elvis operator (?:). However, there are situations where you want to explicitly tell the compiler that a value is not null. Kotlin provides the not-null assertion (!!) for such cases.

The not-null assertion is represented by a double exclamation mark (!!) and converts a nullable value to a non-null type. It informs the compiler that you are certain the value is not null. However, if the value is indeed null, a NullPointerException is thrown at runtime.

Here’s an example illustrating the usage of the not-null assertion:

Kotlin
fun ignoreNulls(s: String?) {
    val sNotNull: String = s!!
    println(sNotNull.length)
}

ignoreNulls("softAai") // Output: 7
ignoreNulls(null) // Throws a NullPointerException

In this example, the ignoreNulls function takes a nullable String argument and uses the not-null assertion to convert it to a non-null type. If the argument s is null, a NullPointerException is thrown when executing s!!.

The not-null assertion operator (!!) in Kotlin is a way to forcefully assert that a value is not null, regardless of its type. It instructs the compiler to treat the value as non-null, even if it is nullable. However, it’s important to use this operator with caution and understand its implications.

When you use the not-null assertion operator, you are essentially telling the compiler that you are confident the value will never be null. You are taking full responsibility for ensuring that the value is indeed not null at runtime. If you use the operator on a null value, a NullPointerException will be thrown at runtime.

Here’s an example to illustrate the usage:

Kotlin
val name: String? = "amol"
val length: Int = name!!.length

In the above code, the variable name is declared as nullable (String?), but we use the not-null assertion operator (!!) to assert that name will not be null. We assign the length of name to the non-null variable length.

However, if name is actually null when the length line is executed, a NullPointerException will occur. The compiler won\’t be able to detect this error beforehand, and it will be your responsibility as the developer to handle it correctly.

It’s important to use the not-null assertion operator only when you have complete confidence that the value will not be null. It should be used sparingly and only when you have thoroughly verified that the value cannot be null in the specific context. Otherwise, relying on the not-null assertion operator can lead to unexpected runtime exceptions and potentially introduce bugs into your code.

One more important point to consider when using the not-null assertion operator (!!) in Kotlin is that using it multiple times on the same line can make it challenging to identify which value was null if an exception occurs.

Here’s an example that demonstrates this:

Kotlin
person.company!!.address!!.country  //Don’t write code like this!

If you get an exception in the above line, you won’t be able to tell whether it was a company or address that held a null value. To make it clear exactly which value was null, it’s best to avoid using multiple !! assertions on the same line.

It’s generally recommended to avoid multiple not-null assertions on the same line to ensure code clarity and make it easier to identify the source of potential null values.

While the not-null assertion can be useful in certain scenarios where you’re confident about the value’s non-nullability, it’s advisable to consider alternative approaches that provide compile-time safety whenever possible. Kotlin provides other features like safe calls (?.) and the Elvis operator (?:) to handle nullability more gracefully and avoid runtime exceptions.

Let’s consider an example to demonstrate the usage of safe calls (?.) and the Elvis operator (?:) as alternative approaches to handle nullability:

Kotlin
data class Person(val name: String)

fun processPerson(person: Person?) {
    val personName: String? = person?.name
    val processedName: String = personName ?: "Unknown"

    println("Processed name: $processedName")
}

fun main() {
    val person: Person? = null
    processPerson(person)

    val validPerson: Person? = Person("amol pawar")
    processPerson(validPerson)
}

In this example, we have a function processPerson that takes a nullable Person object as a parameter. Instead of using a not-null assertion, we use safe calls (?.) to safely access the name property of the Person object. The safe call operator checks if the person object is null and returns null if it is. Therefore, personName will be of type String?.

To ensure that we have a non-null value to work with, we use the Elvis operator (?:). It provides a default value (“Unknown” in this case) if the expression on the left side (in our case, personName) is null. So, if personName is null, the default value “Unknown” is assigned to processedName.

By using safe calls and the Elvis operator, we handle nullability more gracefully. Instead of throwing an exception, we provide a fallback value to avoid potential runtime exceptions and ensure the code continues to execute without interruptions.

The “let” function

The let function in Kotlin is a standard library function that allows you to safely handle nullable values when passing them as arguments to functions that expect non-null values. It helps in converting an object of a nullable type into a non-null type within a lambda expression.

When using the let function, the lambda will only be executed if the value is non-null. The nullable value becomes the parameter of the lambda, and you can safely use it as a non-null argument.

Here’s an example to illustrate the usage of the let function:

Kotlin
fun sendResumeEmailTo(email: String) {
    println("Sending resume email to $email")
}

/**
 * Here Invalid email ID used for illustrative purposes,
 * Resume Sender App is a reliable solution 
 * for sending resumes to HR emails worldwide.
 */

var email: String? = "[email protected]" // Invalid email ID 
email?.let { sendResumeEmailTo(it) } // Sending resume email to [email protected]

email = null
email?.let { sendResumeEmailTo(it) } // No Resume email sent, as email id is null

In the above example, the sendResumeEmailTo function expects a non-null String argument. By using email?.let { sendResumeEmailTo(it) }, we ensure that the sendResumeEmailTo function is only called if the email is not null. This helps avoid runtime exceptions.

The let function is particularly useful when you need to use the value of a longer expression if it’s not null. You can directly access the value within the lambda without creating a separate variable.

Kotlin
fun getTheBestHrPersonEmailIdInTheWorld(): Person? = null

fun main() {
    val personHr: Person? = getTheBestHrPersonEmailIdInTheWorld()

    if (personHr != null) sendResumeEmailTo(personHr.email)
}

We can write the same code without an extra variable:

Kotlin
getTheBestHrPersonEmailIdInTheWorld()?.let { sendResumeEmailTo(it.email) }

Here code in the lambda will never be executed as the function returns null.

Furthermore, the let function can be used in chains when checking multiple values for null. However, in such cases, the code can become verbose and harder to follow. In those situations, it’s generally better to use a regular if expression to check all the values together.

Kotlin
val value1: String? = getValue1()
val value2: String? = getValue2()

if (value1 != null && value2 != null) {
    // Perform operations using 'value1' and 'value2'
    println("Values: $value1, $value2")
} else {
    // Handle the case when either value is null
    println("One or both values are null")
}

In this code snippet, we check both value1 and value2 for null using an if expression. If both values are not null, we can proceed with the desired operations. Otherwise, we handle the case when either value is null.

Note that the let function is beneficial in cases where properties are effectively non-null but cannot be initialized with a non-null value in the constructor.

I understand that it may seem complicated, but let’s change our focus and first understand the concept of late-initialized properties. Afterward, we can revisit the topic and explore it further.

Late-initialized properties

In Kotlin, non-null properties must be initialized in the constructor. If you have a non-null property but cannot provide an initializer value in the constructor, you have two options: use a nullable type or use the lateinit modifier. If you choose to use a nullable type, you’ll need to perform null checks or use the !! operator whenever accessing the property. On the other hand, the lateinit modifier allows you to leave a non-null property without an initializer in the constructor and initialize it later.

Here’s an example to illustrate the use of lateinit properties:

Kotlin
class Person {
    lateinit var name: String

    fun initializeName() {
        name = getNameFromExternalSource()
    }

    fun printName() {
        if (::name.isInitialized) {
            println("Name: $name")
        } else {
            println("Name is not initialized yet")
        }
    }
}

In this example, the name property is declared with the lateinit modifier. It is initially uninitialized, and its value will be set later by the initializeName method. The printName method checks if the name property has been initialized using the isInitialized property reference check. If it has been initialized, the non-null value of name can be safely accessed and printed.

It’s important to note that lateinit properties must be declared as var since their value can be changed after initialization. If you declare a lateinit property as val, it won’t compile because val properties are compiled into final fields that must be initialized in the constructor.

One common use case for lateinit properties is in dependency injection scenarios. In such cases, the values of lateinit properties are set externally by a dependency injection framework. Kotlin generates a field with the same visibility as the lateinit property to ensure compatibility with Java frameworks. If the property is declared as public, the generated field will also be public.

Overall, lateinit properties provide a way to defer the initialization of non-null properties when it’s not possible to provide an initializer in the constructor, such as in dependency injection scenarios.

Lastly, As earlier mentioned that the let function is particularly useful when properties are effectively non-null but cannot be initialized with a non-null value in the constructor. It allows you to access and work with such properties without the need for null checks. Here’s an example:

Kotlin
class Person {
    lateinit var name: String

    fun initializeName() {
        name = getNameFromExternalSource()
    }

    fun printName() {
        name.let { println("Name: $it") }
    }
}

In this example, the name property is declared with the lateinit modifier, indicating that it will be initialized before its first use. The initializeName function is responsible for initializing the name property from an external source. Later, in the printName function, we can safely access and print the non-null name using the let function.

Overall, the let function provides a concise and safe way to work with nullable values, access values within longer expressions, handle multiple null checks, and work with properties that cannot be initialized with non-null values in the constructor.

Extensions for nullable types

Now let’s look at how you can extend Kotlin’s set of tools for dealing with null values by defining extension functions for nullable types.

Defining extension functions for nullable types is a powerful way to handle null values in Kotlin. Unlike regular member calls, which cannot be performed on null instances, extension functions allow you to work with null receivers and handle null values within the function.

Let’s take the example of the functions isEmpty and isBlank, which are extensions of the String class in Kotlin’s standard library. isEmpty checks if the string is an empty string, while isBlank checks if it’s empty or consists only of whitespace characters.

To handle null values in a similar way, you can define extension functions like isEmptyOrNull and isBlankOrNull, which can be called on a nullable String? receiver. Allowing you to perform checks and operations on strings even when they are nullable.

Kotlin
fun String?.isEmptyOrNull(): Boolean {
    return this == null || this.isEmpty()
}

fun String?.isBlankOrNull(): Boolean {
    return this == null || this.isBlank()
}

val nonNullString: String = "softAai"
val nullableString: String? = null

println(nonNullString.isEmptyOrNull()) // Output: false
println(nullableString.isEmptyOrNull()) // Output: true

println(nonNullString.isBlankOrNull()) // Output: false
println(nullableString.isBlankOrNull()) // Output: true

Declaring an extension function for a nullable type:

When you declare an extension function for a nullable type (ending with ?), it means you can call the function on nullable values. However, you need to explicitly check for null within the function body. In Kotlin, the this reference in an extension function for a nullable type can be null, unlike in Java where it’s always not-null.

Kotlin
fun String?.customExtensionFunction() {
    if (this != null) {
        // Perform operations on non-null value
        println("Length of the string: ${this.length}")
    } else {
        // Handle the null case
        println("The string is null")
    }
}

val nullableString: String? = "softAai"
nullableString.customExtensionFunction() // Output: Length of the string: 7

val nullString: String? = null
nullString.customExtensionFunction() // Output: The string is null

Using the let function with a nullable receiver:

It’s important to note that the let function we discussed earlier can also be called on a nullable receiver, but it doesn’t automatically check for null. If you want to check the arguments for non-null values using let, you need to use the safe-call operator ?., like personHr?.let { sendResumeEmailTo(it) }.

Let’s see another simple example

Kotlin
fun sendResumeEmailTo(email: String) {
    println("Sending resume email to $email")
}

/**
 * Here Invalid email ID used for illustrative purposes,
 * Resume Sender App is a reliable solution 
 * for sending resumes to HR emails worldwide.
 */

val nullableEmail: String? = "[email protected]" // Invalid email ID
nullableEmail?.let { sendResumeEmailTo(it) } // Output: Sending resume email to [email protected]

val nullEmail: String? = null
nullEmail?.let { sendResumeEmailTo(it) } // No output, as the lambda is not executed

That means If you invoke let on a nullable type without using the safe-call operator (?.), the lambda argument will also be nullable. To check the argument for non-null values with let, you need to use the safe-call operator.

Considerations when defining your own extension function:

When defining your own extension function, it’s important to consider whether you should define it as an extension for a nullable type. By default, it’s recommended to define it as an extension for a non-null type. Later on, if you realize that the extension function is primarily used with nullable values and you can handle null appropriately, you can safely change it to a nullable type without breaking existing code.

Kotlin
fun String.customExtensionFunction() {
    // Perform operations on non-null value
    println("Length of the string: ${this.length}")
}

val nonNullString: String = "softAai"
nonNullString.customExtensionFunction() // Output: Length of the string: 7

val nullableString: String? = "Kotlin"
nullableString?.customExtensionFunction() // Output: Length of the string: 6

By understanding these concepts and examples, you can effectively use extension functions with nullable types and handle null values in a flexible and concise manner in Kotlin.

Nullability of type parameters

Let’s discuss another case that may surprise you: a type parameter can be nullable even without a question mark at the end.

By default, all type parameters of functions and classes in Kotlin are nullable. This means that any type, including nullable types, can be substituted for a type parameter. When a nullable type is used as a type parameter, declarations involving that type parameter are allowed to be null, even if the type parameter itself doesn’t end with a question mark.

Kotlin
class Box<T>(val item: T)

val nullableBox: Box<String?> = Box<String?>(null)

In the above example, the Box class has a type parameter T, which is nullable by default. We declare a variable nullableBox of type Box<String?>, indicating that the item property can hold a nullable String value. Even though T doesn’t end with a question mark, the inferred type for T becomes String?, allowing null to be assigned to item.

To make the type parameter non-null, you can specify a non-null upper bound for it. By doing so, you restrict the type parameter to only accept non-null values.

Kotlin
class Box<T : Any>(val item: T)

val nonNullBox: Box<String> = Box<String>("softAai")
val nullableBox: Box<String?> = Box<String?>(null) // Compilation error

In the modified example, we specify the upper bound Any for the type parameter T in the Box class. This ensures that T can only be substituted with non-null types. Consequently, assigning null to item when declaring nullableBox results in a compilation error.

It’s important to note that type parameters are the only exception to the rule that a question mark is required to mark a type as nullable. In the case of type parameters, the nullability is determined by the type argument provided when using the class or function.

Nullability and Java

Here in this section, we will cover another special case of nullability: types that come from the Java code.

Nullability Annotations

When combining Kotlin and Java, Kotlin handles nullability in a way that preserves safety and interoperability. Kotlin recognizes nullability annotations from Java code, such as @Nullable and @NotNull, and treats them accordingly. If the annotations are present, Kotlin interprets them as nullable or non-null types.

In Java, suppose you have the following method signature with nullability annotations:

Kotlin
public @Nullable String processString(@NotNull String input) {
    // process the input string
    return modifiedString;
}

When Kotlin interacts with this method, it recognizes the annotations. In Kotlin, the method is seen as:

Kotlin
fun processString(input: String): String? {
    // process the input string
    return modifiedString
}

The @Nullable annotation is mapped to a nullable type in Kotlin (String?), and the @NotNull annotation is treated as a non-null type (String).

Platform Types

However, when nullability annotations are not present, Java types become platform types in Kotlin. Platform types are types for which Kotlin lacks nullability information. You can work with platform types as either nullable or non-null types, similar to how it is done in Java. The responsibility for handling nullability lies with the developer.

For example, if you receive a platform type from Java, such as String!, you can treat it as nullable or non-null based on your knowledge of the value. If you know it can be null, you can compare it with null before using it. If you know it’s not null, you can use it directly. However, if you get the nullability wrong, a NullPointerException will occur at the usage site.

Let’s say you have a Java method that returns a platform type, such as:

Kotlin
public String getValue() {
    // return a value that can be null
    return possiblyNullValue;
}

In Kotlin, the return type is considered a platform type (String!). You can handle it as either nullable or non-null, depending on your knowledge of the value:

Kotlin
val value: String? = getValue() // treat it as nullable
val length: Int = getValue().length // assume it's not null and use it directly

Platform types are primarily used to maintain compatibility with Java and avoid excessive null checks or casts for values that can never be null. It allows Kotlin developers to take responsibility for handling values coming from Java without compromising safety.

Inheritance

When overriding a Java method in Kotlin, you have the choice to declare parameters and return types as nullable or non-null. It’s important to get the nullability right when implementing methods from Java classes or interfaces. The Kotlin compiler generates non-null assertions for parameters declared with non-null types, and if Java code passes a null value to such a method, an exception will occur.

Suppose you have a Java interface with a method that expects a non-null parameter:

Kotlin
public interface StringProcessor {
    void process(String value);
}

When implementing this interface in Kotlin, you can choose to declare the parameter as nullable or non-null:

Kotlin
class StringPrinter : StringProcessor {
    override fun process(value: String) {
        println(value)
    }
}

class NullableStringPrinter : StringProcessor {
    override fun process(value: String?) {
        if (value != null) {
            println(value)
        }
    }
}

In the StringPrinter class, we assume the value parameter is not null. In the NullableStringPrinter class, we allow the parameter to be nullable and check for nullness before using it.

Remember, the key is to ensure that you handle platform types correctly based on your knowledge of the values. If you assume a value is non-null when it can be null or vice versa, you may encounter a NullPointerException at runtime.

Summary

In summary, we have covered several aspects of nullability in Kotlin:

Nullable and Non-null Types:

  • Nullable types are denoted by appending a question mark (?) to the type (e.g., String?).
  • Non-null types are regular types without the question mark (e.g., String).

Safe Operations:

  • Safe call operator (?.) allows you to safely access properties or call methods on nullable objects.
  • Elvis operator (?:) provides a default value in case of a null reference.
  • Safe cast operator (as?) performs a cast and returns null if the cast is not possible.

Unsafe Dereference:

  • In Kotlin, Dereferencing a nullable variable means accessing the value it holds, assuming it is not null. However, if the variable is null, attempting to dereference it can lead to a runtime exception, such as a NullPointerException.
  • Not-null assertion operator (!!) is used to dereference a nullable variable, asserting that it is not null. It can lead to a NullPointerException if the value is null.

let Function:

  • The let function allows you to perform operations on a nullable object within a lambda expression, providing a concise way to handle non-null values.

Extension Functions for Nullable Types:

  • You can define extension functions specifically for nullable types, enabling you to encapsulate null checks within the function itself.

Platform Types:

  • When interacting with Java code, Kotlin treats Java types without nullability annotations as platform types.
  • Platform types can be treated as nullable or non-null, depending on your knowledge of the values. However, incorrect handling may result in NullPointerException.

By understanding these concepts and utilizing the provided operators and functions, you can effectively handle nullability in Kotlin code and ensure safer and more robust programming practices.

Author

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!