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:
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:
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:
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:
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.
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.
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:
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.
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:
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:
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:
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:
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:
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 throwingClassCastException
. 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:
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:
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:
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:
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:
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.
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:
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 regularif
expression to check all the values together.
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:
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:
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.
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.
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
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.
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.
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.
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:
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:
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:
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:
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:
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:
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 aNullPointerException
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.