SoFunction
Updated on 2025-03-06

Talk about the use of Kotlin's empty processing

There have been quite a lot of articles about Kotlin recently. Google's official support has made more and more developers pay attention to Kotlin. The project I joined not long ago uses a mixed development model of Kotlin and Java. It is so shallow that I can finally practice a new language. This article will briefly talk about the empty processing in Kotlin.

1. It's really easy to get started

Let’s start by learning Kotlin.

I heard from people that it is easy to get started, but if I really want to switch to another language, I will inevitably hesitate whether there is this need. Now, because of work, I just started Kotlin and felt it was really delicious (it was really easy to get started).

First of all, at the code reading level, for programmers with Java foundation, reading Kotlin code is basically accessible. Apart from some operators and some order changes, you can read directly as a whole.

Secondly, at the code writing level, only some coding habits need to be changed. Mainly: don't write semicolons for statements, variables need to be declared with var or val, type is written after variables, and "new" is not used when instantiating an object... Changes at the habit level only require more code, and you will naturally adapt.

Finally, at the learning method level, since Kotlin will eventually be compiled into bytecode and run on the JVM, you can use Java as a comparison when you first start. For example, you may not know what the companion object means in Kotlin, but you know that since Kotlin will eventually be converted into bytecode that jvm can run, you will definitely find the corresponding thing in Java.

Android Studio also provides very convenient tools. Select the menu Tools -> Kotlin -> Show Kotlin Bytecode to see the bytecode compiled by Kotlin. Click "Decompile" above the window to see the Java code corresponding to this bytecode. ——This tool is particularly important. If a piece of Kotlin code makes you confused, you can know its meaning by looking at its corresponding Java code.

Of course, this is just about getting started or getting started (if you only get started, you can ignore advanced features such as coroutines). It will definitely take a certain amount of time to truly apply it skillfully and even master it completely.

2. Strong rules for NPE

Some articles say that Kotlin helped developers solve NPE (NullPointerException), which is wrong. In my opinion, Kotlin did not help developers solve NPE (Kotlin: I really can't do it), but instead added various strong rules at the language level to force developers to deal with possible null pointers problems by themselves, achieving the goal of minimizing (only reducing but not completely avoiding) the occurrence of NPE.

So how did Kotlin do it? Don't worry, we can first review how we deal with the null pointer problem in Java.

The processing of null pointers in Java can generally be divided into two solutions: "defensive programming" and "contractual programming".

Everyone should be familiar with "defensive programming". The core idea is to distrust any "external" input - whether it is real user input or actual parameters passed in by other modules, the specific point is various judgments. Creating a method requires null counts, creating a logical block requires null counts, and even your own code needs to be null counts (prevent object recycling, etc.). Examples are as follows:

public void showToast(Activity activity) {
  if (activity == null) {
    return;
  }
  
  ......
}

The other is "contractual programming". Each module has a rule agreed upon, and everyone will do things according to the rules. If there is a problem, find someone who does not abide by the rules, so as to avoid a lot of judgment logic. Android provides relevant annotations and the most basic checks to assist developers, as examples are as follows:

public void showToast(@NonNull Activity activity) {
  ......
}

In the example, we added the @NonNull annotation to the Activity, which is to declare a convention to all the people who call this method, that the caller should ensure that the incoming activity is not empty. Of course, you should know that this is a very weak limit. If the caller does not pay attention to or ignores this annotation, the program will still have the risk of crash caused by NPE.

Looking back, for Kotlin, I think it is a way of combining contractual programming and defensive programming to the language level. (It sounds like it's more troublesome than various null or annotations in Java? Keep reading and you will find that it's indeed more troublesome...)

In Kotlin, there are the following constraints:

During the declaration stage, variables need to decide whether they are nullable, such as var time: Long? can accept null, while var time: Long cannot accept null.

In the variable transmission stage, the "emptyability" must be maintained. For example, the formal parameter declaration is not empty, so the actual parameter must be non-empty or converted to non-empty in order to be transmitted normally. Examples are as follows:

fun main() {
    ......
    // test(isOpen) is called directly, and the compilation is not passed    // It can be passed within the empty check to prove that it is not empty    isOpen?.apply { 
      test(this)
    }
    // It can also be forced to convert to non-empty type    test(isOpen!!)
  }
 
 
  private fun test(open: Boolean) {
    ......
  }

During the use phase, strict emptying is required:

var time: Long? = 1000
   //Although you have assigned a non-empty value, you cannot do this during use:   //()
   //There must be empty   time?.toInt()

In general, Kotlin has made a lot of strong language levels to solve NPE, which can indeed reduce the occurrence of NPE. But this solution, which is both "contractual" (decision-based) and "defensive" (declaration-based and non-empty) will allow developers to do more work and will be a little more "troubled".

Of course, in order to reduce the hassle, Kotlin simplifies the logic of null judgment with "?" - or null judgment. We can view the Java equivalent code of time?.toInt() through the tool:

if (time != null) {
  int var10000 = (int)time;
}

This simplification is particularly convenient when it is very deep in the data level and requires writing a large number of null statements. This is why, although logically Kotlin allows developers to do more work, it does not feel more troublesome in writing the code.

3. NPE issues under strong rules

With Kotlin's tight defense, has the NPE problem been ended? The answer is of course no. In the practice process, we found that there are mainly the following scenarios that are prone to NPE:

1. The data class (meaning corresponds to the model in Java) declares non-empty

For example, in the scenario where json data is taken from the backend, it is impossible for the client to control which field in the backend may be empty. In this case, our expectation must be that each field may be empty, so that there will be no problem when converted to json object:

data class User(
    var id: Long?,
    var gender: Long?,
    var avatar: String?)

If there is a field that forgets to add "?", the backend will throw a null pointer exception if it does not pass the value.

2. Over-dependence on Kotlin's null value check

private lateinit var mUser: User

...

private fun initView() {
 mUser = <User>("key_user")
}

In Kotlin's system, you will rely too much on Android Studio's null value checking. In the code prompt, the getParcelableExtra method of Intent returns non-null, so there will be no warnings when you directly use the method result assignment. But click on the getParcelableExtra method inside you will find that its implementation is like this:

public <T extends Parcelable> T getParcelableExtra(String name) {
    return mExtras == null ? null : mExtras.<T>getParcelable(name);
  }

Other internal codes will not be expanded. In short, it may return null, and there will obviously be problems with direct assignment.

I understand that this is the inadequacy of the Kotlin compilation tool for Java code checking. It cannot accurately determine whether the Java method will return empty and choose unconditional trust, even if the method itself may also declare @Nullable.

3. Variable or formal parameter declared as non-empty

This point is very similar to the first and second points. It is mainly because during the use process, you must further think about whether the passed value is really not empty.

Some people might say, then I'm just going to declare all of them as nullable types - doing so will make you need to null everywhere you use the variable, and the convenience of Kotlin itself is gone.

My point of view is not to give up on food because of choking. Pay more attention when using it to avoid most problems.

4. !! Forced to non-empty

When assigning a nullable type to a non-empty type, you need to judge the null type to ensure that it is not empty to assign a value (Kotlin constraint).

We use !! to convert "nullable" to "non-empty", but if the nullable variable value is null, it will crash.

Therefore, it is recommended to use it only when ensuring that it is not empty!!:

param!!

Otherwise, try to put it in the empty code block:

param?.let {
 doSomething(it) 
}

4. Problems encountered in practice

From Java's empty processing to Kotlin's empty processing, we may subconsciously look for the way to write the verdict of Java's empty:

if (n != null) {
 //How about non-empty} else {
 //What if it is empty}

There is indeed a similar way to write in Kotlin, which is to combine higher-order functions let, apply, run... to handle empty judgments. For example, the above Java code can be written as:

n?.let {
 //How about non-empty} ?: let {
 //What if it is empty}

But there are a few small pits here.

1. Two code blocks are not mutually exclusive

If it is Java writing, no matter what the value of n is, the two code blocks are mutually exclusive, that is, "black or white". But Kotlin's writing method is not (not sure if this writing method is best practice, if there is a better solution, please leave a message to point it out).

?: This operator can be understood as if (a != null) a else b , that is, the value before it is not empty returns the previous value, otherwise the value after it is returned.

These higher-order functions in the above code all have return values, see the following table for details:

function Return value
let Returns the specified return or last line in the function
apply Return to the object itself
run Returns the specified return or last line in the function
with Returns the specified return or last line in the function
also Return to the object itself
takeIf The condition is true and returns the object itself, but does not.
takeUnless Return null if the condition is true, return the object itself if it is not true.

If you are using let, please note that its return value is "specified return or the last line in the function", then you will encounter the following situation:

val n = 1
var a = 0
n?.let {
 a++
 ...
 null //The last action null} ?: let {
 a++
}

You will magically discover that the value of a is 2, which means that both the previous code block and the next code block are executed.

You may not agree with the above writing method, because it is obviously a reminder to pay attention to the last line, but what if you didn’t pay attention to this detail before or the following writing method?

n?.let {
 ...
 (key, value) // anMap is a HashMap} ?: let {
 ...
}

Few people will notice that the put method of Map has a return value and may return null. Then it is easy to get into a pit in this case.

2. The objects of the two code blocks are different

Take let as an example. In the let code block, it can be used to refer to the object (other higher-order functions may use this, similar). Then we may write it as follows when writing the following code:

activity {
 n?.let {
 () // it is n } ?: let {
 () // it is activity } 
}

The results will naturally reveal that the value is different. The previous code block it refers to n, and the next code block refers to this pointing to the entire code block.

The reason is that there is no . between ?: and let , that is, the object called let in the latter code block is not an empty object, but this. (However, the probability of errors in this scenario is not high, because in the latter code block, many objects n methods cannot be used, and you will notice the problem)

postscript

In general, it was still smoother and more comfortable than expected to switch to Kotlin. I was a little uncomfortable when I got used to Kotlin and went back to write Java. I will write this first today and share other things that need to be summarized later.

The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.