Kotlin: Null pointer check

Compared to Java, Kotlin has a big improvement that is null pointer checking.

We first look at a very simple Java code:

public void doStudy(Study study) {
    
    
    study.readBooks();
    study.doHomeWork();
}

The above code does not have any complicated logic, it just receives a Study parameter and calls the readBooks() and doHomeWork() methods of the parameter.

Is this code safe? Not necessarily. Because it depends on the parameters passed by the caller, if we pass a null parameter to the doStudy method, then there is no doubt that a null pointer exception will occur here. Therefore, a more secure approach is to perform a null processing before calling the parameter method, as shown below:

public void doStudy(Study study) {
    
    
   if (study!=null) {
    
    
       study.readBooks();
       study.doHomeWork(); 
    }
  }

Such a simple piece of code has the potential risk of generating a null pointer exception. If it is a large project, it is almost impossible to completely avoid null pointers.

1. Nullable Type System

However, Kotlin has solved this problem very scientifically. It uses the compile-time null check mechanism to almost eliminate the null pointer exception. Although the mechanism of null checking at compile time sometimes makes the code more difficult to write, don't worry, Kotlin provides a series of auxiliary tools that allow us to easily handle various null situations. Let's start learning step by step.

Go back to the doStudy() function just now, and now write this function as a Kotlin version. The code is as follows:

fun doStudy(study: Study) {
    
    
     study.readBooks()
     study.doHomeWork()
}

This code looks no different from the Java code just now, but in fact it has no risk of null pointers, because Kotlin defaults that all parameters and variables are not nullable, so the study parameters passed in here must not Is empty, we can safely call any of its functions. If you try to pass a null parameter to the doStudy function, you will be prompted with an error as shown in the figure below: In
Insert picture description here
other words, Kotlin advances the check of null pointer exceptions to the compile time. If our program is at risk of null pointer exceptions, then An error will be reported directly when compiling, and it can run successfully after correction, so that you can ensure that the program will not have a null pointer exception during runtime.

Seeing this, you may have a huge doubt, all parameters and variables cannot be empty? This is really unheard of, so what if our business logic requires a certain parameter or variable to be empty? Don't worry, Kotlin provides another set of nullable type system, but when using a nullable type system, we need to deal with all potential null pointer exceptions at compile time, otherwise the code will fail Compile and pass.

So what does the nullable type system look like? It's very simple, just add a question mark after the class name . For example, Int represents a non-nullable integer type, and Int? It means a nullable integer; String means a non-nullable character string, and String? It means an empty string.

Going back to the doStudy() function just now, if we want the passed-in parameter to be empty, then we should change the type of the parameter from Study to Study? .

test.doStudy(null)

You can pass in null here, but when you call the readBooks and doHomeWork methods, a red wavy line error message appears. As follows:
Insert picture description here
Why is this?

In fact, the reason is very simple, because we changed the parameter to a nullable Study? Type, call the readBooks, doHomeWork methods of the parameters at this time may cause a null pointer exception, so Kotlin does not allow compilation in this case.

So how to solve it? Very simple, as long as the null pointer exception is handled. For example, make a non-empty judgment.

   fun doStudy(study: Study?) {
    
    
        if (study != null) {
    
    
            study.readBooks()
            study.doHomeWork()
        }
    }

Now the code can be compiled and passed normally, and it can be guaranteed that there will be no null pointer exception at all.

In fact, in addition to adding if judgments, Koltin specifically provides a series of auxiliary tools to make it easier for developers to perform null processing.

2. Blanking aids

2.1 ?.Operator

First, learn the most common ?.operator.

The function of this operator is very easy to understand, that is, when the object is not empty, the corresponding method is called normally, and when the object is empty, it does nothing. For example, the following null processing code:

   if (a != null) {
    
    
       a.doSomeThing()
   }

This code uses? The operator can be simplified to:

a?.doSomeThing()

Understand the ?.role of the operators, if we look at the following use of this operator doStudy () function to optimize the code as follows:

   fun doStudy(study: Study?) {
    
    
        study?.readBooks()
        study?.doHomeWork()
    }

As you can see, so we can use it? The operator removes the if judgment statement. You may think that it is not complicated to use if statements to perform null processing. That is because the current code is still relatively simple. When the functions we develop in the future become more and more complex and more and more objects need to be judged, Will you feel it? The operator is particularly easy to use.

2.2 ?:Operator

?: Both the left and right sides of this operator receive an expression. If the result of the left expression is not empty, it returns the result of the left expression, otherwise, it returns the result of the right expression.

Observe the following code:

val c=if (a!=null){
    
    
            a
        }else {
    
    
            b
        }

Using code logic ?:operators can be simplified to:

val c=a ?:b

Next we come to combine through a concrete example ?.and ?:these two operators, allowing you a deeper understanding of them.

For example, now we want to write a function to get the length of a piece of text, using the traditional way of writing, we can write it like this:

fun getTextLength(text: String?): Int {
    
    
   if (text != null) {
    
    
       return text.length
   }
   return 0
   }

Since the text may be empty, we need to perform a null operation first. If the text is not empty, return its length, if the text is empty, return 0.

This code does not look complicated, but we can make it easier with the help of operators, as shown below:

fun getTextLength(text: String?) = text?.length ?: 0

Here we will ?.and ?:operator combination to be used together, first of all because the text is likely to be empty, so we need to use when you call its length field ?.operator, and when the text is empty, text? .Length returns a null value this time we'll help ?:operators it returns 0.

However, Kotlin's null pointer checking mechanism is not always so smart. Sometimes we may logically handle null pointer exceptions, but Kotlin's compiler does not know that it will still fail to compile at this time.

Observe the following code example:

var content: String? = "hello"

override fun onCreate(savedInstanceState: Bundle?) {
    
    
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)

  if (content != null) {
    
    
      printUpperCase()
  }
}

fun printUpperCase() {
    
    
   val toUpperCase = content.toUpperCase()
   print(toUpperCase)
}

Here we define a nullable global variable content, and then perform a null operation in the onCreate() function. Only when the content is not empty will the printUpperCase() function be called. In the printUpperCase() function, we Convert content to uppercase mode, and finally print it out.

It seems that there is nothing wrong with the logic, but unfortunately, this code must not run. Because the printUpperCase() function does not know that the external content variable has been checked for non-nullness, when the toUpperCase() method is called, it also believes that there is a risk of a null pointer, and it cannot be compiled.

In this case, if we want to compile forcibly, we can use the non-empty assertion tool, which is written after the object !!, as shown below:

fun printUpperCase() {
    
    
   val toUpperCase = content!!.toUpperCase()
   print(toUpperCase)
}

This is a risky way of writing. It is intended to tell Kotlin that I am very sure that the objects here will not be null, so I don’t need you to help me check for null pointers. If there is a problem, you can directly throw a null pointer exception and the consequences It's my responsibility.

Although the code written in this way can indeed be compiled, but when you want to use a non-empty assertion tool, it is best to remind yourself if there is a better way to achieve it. When you are most confident that this object will not be empty, In fact, it may be when a potential null pointer exception occurs.

2.3 Auxiliary tools let

let is not an operator, nor a keyword, but a function.

The sample code of let is as follows:

obj.let {
    
     obj2 ->
   //编写具体的业务逻辑
}

As you can see, the let function of the obj object is called here, and then the code in the Lambda expression will be executed immediately, and the obj object itself will be passed to the Lambda expression as a parameter. However, in order to prevent the variable names from being duplicated, here I changed the parameters to obj2, but in fact they are the same object, which is what the let function does.

So, what is the relationship between let function and null pointer check?

In fact, with the characteristic function let ?.operator can play a significant role in the null pointer check the time.

Let's go back to the doStudy() function, the current code is as follows:

   fun doStudy(study: Study?) {
    
    
        study?.readBooks()
        study?.doHomeWork()
    }

Here we use the ?.following operators can optimize normal compiler, but in fact this expression is somewhat long-winded. If judged by if, the code is as follows:

   fun doStudy(study: Study?) {
    
    
        if (study != null) {
    
    
            study.readBooks()
         }
         
         if (study != null) {
    
    
            study.doHomeWork()
         }
        }
    }

In other words, we can call any method of the study object at will by performing an if judgment, but subject to the limitation of the ?. operator, now it becomes an if judgment every time the method of the study object is called.

At this time, you can use a combination of ?. operator and let function to optimize the code, as shown below:

fun doStudy(study: Study?) {
    
    
  study?.let {
    
     stu ->
      stu.readBooks()
      stu.doHomework()
  }
}

Let me briefly explain the above code. The ?.operator indicates that when the object is empty, it does nothing. When the object is not empty, the let function is called, and the let function passes the study object itself as a parameter to the Lambda expression. The study object is definitely not empty. We can safely call any of its methods.

In addition, according to the grammatical characteristics of the Lambda expression , when there is only one parameter in the parameter list of the Lambda expression, it is not necessary to declare the parameter name, and the it keyword can be used instead . That is, the code can be further simplified to:

fun doStudy(study: Study?) {
    
    
   study?.let {
    
    
     it.readBooks()
     it.doHomework()
    }
  }

In addition, it should be noted that the let function can handle the nullification problem of global variables, but the if judgment statement cannot do this. For example, we change the parameter in the doStudy() function into a global variable and use let The function still works normally. But using the if judgment statement will prompt an error.

var study: Study? = null

if (study != null) {
    
    
    study.readBooks()
    study.doHomework()
}

Insert picture description here

Smart cast to 'xxx' is impossible, because 'xxx' is a mutable property that could have been changed by this time

The reason why an error is reported here is that the value of global variables may be changed by other threads. Even if null processing is done, there is still no guarantee that the study variable in the if language has no risk of null pointers. From this point of view, the advantages of let function can also be reflected.

So far, the Kotlin null pointer check auxiliary tool learning is over.

Guess you like

Origin blog.csdn.net/gaolh89/article/details/105906061