Not-Null Assertion Is Pointless

Hi, I’m a Java Developer who is new to Kotlin, and while I find it to be more advanced in many ways, especially syntactically, I am having trouble seeing a fair reason to use the not-null (!!) assertion operator.

The way I understand it is you need (!!) only when you are absolutely certain that a variable will never be null. And you must use this in functions with a nullable receiver. But if the variable is not null, perhaps the function shouldn’t receive a nullable type?

And in a function that returns a nonnullable type, using not-null would just be redundant.

And in the case of an extension function where the parent function is nullable, as long as you specify this in the beginning, you won’t need not-null at all:

fun String?.process(): String {
val safeString: String = this ?: “Fallback Value” // Always non-null
return safeString.uppercase() // No need for !!
}

To me, if you even find yourself reaching for a not-null operator, it simply means you haven’t handled null properly.

In principle yes, but if you work e.g. with Java libraries, you don’t have the nullability behavior under control and need a way to make it work. E.g. I work with code where JPA entities need to have a “nullable” ID field, but when you read them from the DB, the ID is definitely no longer null.

Using this operator should definitely raise an eyebrow in code review.

1 Like

I’ve also encountered a use case for !! while using jOOQ, in very similar circumstances to what Landei describes above.

Another case, involving a purely Kotlin codebase, is when you use a third-party library that was written without much care about nullability. Consider the following:

data class FooDetails(
    val id: String,
    val description: String,
    val createdBy: User,
    val createdOn: DateTime,
    val modified: Boolean,
    val modifiedBy: User?,
    val modifiedOn: DateTime?
)

fun foo(bar: FooDetails) {
    if (bar.modified) logModificationDetails(bar.modifiedBy!!, bar.modifiedOn!!)
}

I agree that usage of !! usually means something is not written quite right in your codebase, but when you have a third-party library that you can’t change, sometimes the use of !! is not as bad as alternative ways of dealing with the issue.

3 Likes

It makes sense for validation.

class MyRequestObject(
    @field:NotNull
    val myProperty1: String?,
    @field:NotNull
    val myProperty2: Int?
) {
    fun toDomainObject() = MyDomainObject(myProperty1!!, myProperty2!!)
}

Also if you have a property that only requires a value in some cases.

fun doJob(jobType: JobType, optionalJobProperty: String? = null) {
    if (jobType == JobType.RequiresExtraProperty && optionalJobProperty == null) {
        throw IllegalArgumentException("Must provide optional job property for extra property job type.")
    }

    runJob(jobType, optionalJobProperty)
}

private fun runJob(jobType: JobType, optionalJobProperty: String? = null) {
    if (jobType == JobType.RequiresExtraProperty) {
        doExtraThing(optionalJobProperty!!)
    }
}
1 Like

I think you miss the main point. !! is essentially a downcasting and we use it in similar cases as other types of downcasting - when we don’t know the exact type at the compile time, but we know it at the runtime. Simple example is a result object:

interface Result<T : Any> {
    val isSuccess: Boolean
    // non-null if successful
    val value: T?
    // non-null if not successful
    val error: Throwable?
}

if (result.isSuccess) {
    return result.value!!
} else {
    throw Exception(result.error!!)
}

In practice, it is better to avoid using this operator. Usually, there are better alternatives or we can redesign our code to make it more type safe. Above example could be easily solved by using sealed types. Even if there would be no sealed types, we could provide functions like:

inline fun ifSuccess(ifSuccess: (T) -> Unit) { ... }
inline fun ifError(ifError: (Throwable) -> Unit) { ... }

This allows to avoid !! in most of the code where we may make a mistake, but still, we have to use it in implementations of these functions.

3 Likes

Here, we consider two scenarios:

1. When using Kotlin code alone

Not-Null Assertions are used to please the compiler in a safe situation (if you are also unsure if a value is null, then using null assertions is definitely unsafe)
In the following case, we can make sure that map[1] is not null, so the Not-Null Assertion here is only to please the compiler. In this case, you cannot say that you are using Not-Null Assertions because you did not handle null values properly, it is just that the compiler does not understand this situation.

val map = buildMap {
    put(1, "1")
}
if (map.containsKey(1)) {
    val value = map[1]!!
    println(value.length)
}

2. When calling Java from Kotlin

We usually use Not-Null Assertions to avoid platform types (platform types is what Kotlin compiler see Java types, see Calling Java from Kotlin | Kotlin Documentation) everywhere.

val str = JavaClass.javaMethod()!!
// or
val str1: NotNullType = JavaClass.javaMethod()

Actually, you can see this usage everywhere in Kotlin stdlib:

2 Likes

I can’t agree more about downcasting