Write at the beginning: I plan to write a Kotlin series of tutorials, one is to make my own memory and understanding more profound, two is to share with the students who also want to learn Kotlin. The knowledge points in this series of articles will be written in order from the book “Kotlin In Action”. While displaying the knowledge points in the book, I will also add corresponding Java code for comparative learning and better understanding.

Kotlin Tutorial (1) Basic Kotlin Tutorial (2) Functions Kotlin Tutorial (3) Classes, Objects and Interfaces Kotlin Tutorial (4) Nullability Kotlin Tutorial (5) Types Kotlin Tutorial (6) Lambda programming Kotlin Tutorial (7) Operator overloading and Other conventions Higher-order functions Kotlin tutorial (9) Generics


This chapter is actually chapter 6 in Kotlin Field, after Lambda, but the content of this chapter is actually one of Kotlin’s main features. Therefore, I have summarized the contents of this chapter above.

Nullable sex

NullPointerException is a feature of the Kotlin type system that helps you avoid NullPointerException errors.

Nullable types

If a variable may be null, it is unsafe to call a method on that variable because it will result in a NullPointerException. For example, a Java function like this:

int strLen(String s) {
    return s.length();
}
Copy the code

If this function is called with a null argument, it throws a NullPointerException. Do you need to add null checks to your methods? This depends on whether you expect the function to be called with a null argument. If not, Kotlin can define it like this:

fun strLen(s: String) = s.length
Copy the code

It looks no different from Java, but if you try strLen(null), you will be flagged as an error at compile time. Because in Kotlin String can only represent a String, not null, if you want to support this method to pass null, you need to add? :

fun strLen(s: String?) = if(s ! = null) s.lengthelse 0
Copy the code

? Can be appended to any type to indicate that a variable of that type can store a null reference: String? , Int? , MyCustomType? And so on.

Once you have a nullable value, you are limited in what you can do with it. For example, its methods cannot be called directly:

    val s: String? = ""// s.length // error, only safe(? .). or non-null asserted (!! .). calls are allowed s? .length // the length attribute is called if s is not null. .length // asserts that s is not null and calls the length attribute directly. If s is null, the same crash will occurCopy the code

Nor can it be assigned to a variable of a non-null type:

    val x: String? = null
//    val y: String = x  //Type mismatch
Copy the code

In other words, plus? And unadded can be considered two types, and the compiler will only intelligently convert this type after comparing it to NULL.

fun strLen(s: String?) = if(s ! = null) s.lengthelse 0  
Copy the code

This example compares to null, so String? The type is intelligently converted to String, so you can get the Length property directly.

Java has several tools to help resolve NullPointerException issues. For example, some people use annotations (@nullable and @notnull) to express worthlessness. Some tools can use these annotations to find where nullPointerExceptions might be thrown, but these tools are not part of the standard Java compilation process, so it’s hard to guarantee that they will be applied consistently. And it’s hard to use annotations throughout the code base to mark all the places where errors can occur so that they can be detected.

Kotlin’s nullable type perfectly solves the problem of null Pointers. Note that there is no difference between nullable and non-nullable objects at run time: nullable types are not wrappers for non-nullable types. All checks take place at the compiler. This means that using Kotlin’s nullable types does not incur additional overhead at run time.

Safe call operator: “? .”

One of the most effective tools in Kotlin’s Arsenal is the secure call operator:? ., which allows your dad to combine a null check and a method call into one operation. For example, the expression S? .toupperCase () equals if (s! = null) s.topperCase () else NULL. In other words, if you view calls a non-null worthy method, the method call will execute normally. But if the value is null, the call does not happen, and the entire expression is null. So the expression s? The return type of.toupperCase () is String? .

Secure calls can also be used to access attributes, and multiple levels of attributes can be obtained consecutively:

class Address(val street: String, val city: String, val country: String) class Company(val name: String, val address: Address?) class Person(val name: String, val company: Company?) CountryName (): String {val country = this.pany?.address?. Country // Multiple security calls linked togetherreturn if(country ! = null) countryelse "Unknown"
}
Copy the code

Kotlin can make null checking very concise. In this example you are comparing a value to null and return it if it is not null, otherwise return something else. There’s an easier way to write it in Kotlin.

Elvis operator: “? :”

if (country ! = null) country else “Unknown”

country ? :"Unknown"
Copy the code

The Elvis operator accepts two operands. If the first operand is not null, the result is the first operand, if the first operand is null, the result is the second operand. fun strLen(s: String?) = if(s ! = null) s.length else 0 This example can also be shortened by Elvis: fun strLen(s: String?) = s? .length ? : 0

Safe conversion: “as?”

Earlier we learned about the as operator for type conversions in Kotlin. As with Java, a ClassCastException is thrown if the value being converted is not of the type you are trying to convert. Of course you can combine is checking to make sure the value has the right type. But Kotlin, as a safe and concise language, has elegant solutions. as? The operator attempts to convert a value to the specified type and returns NULL if the value is of an inappropriate type. A common pattern is to combine secure transformations with Elvis operators. The equals method is handy:

class Person(val name: String, val company: Company?) { override fun equals(other: Any?) : Boolean { val o = other as? Person ? :return false// Check for type mismatch and return directlyfalse
        returnO.name == name &&o.com pany == company // O is intelligently converted to Person type after a safe conversion} Override funhashCode(): Int = name.hashCode() * 31 + (company? .hashCode() ? : 0)}Copy the code

Non-empty assertion: “!!”

Non-null assertions are the simplest and most straightforward tool Kotlin provides for handling nullable types, and can convert any value to a non-null type. If a non-null assertion is made on a null value, an exception is thrown. We demonstrated the use of non-null assertions earlier: s!! .length.

You may have noticed that the double exclamation point looks a bit rude, as if you’re yelling at the compiler. This is intentional, and Kotlin’s design view convinces you to think of better solutions that don’t use assertions in ways that the compiler can’t verify.

There are, however, cases where non-null assertions are appropriate for solving certain problems. When you check if a value in a function is null. When this value is used in another function, the compiler does not know if it is safe to use it. If you are sure that such a check must exist in some other function, you may not want to double-check before using the value. This is where you can use non-null assertions.

“Let” function

The let function makes it easier to handle nullable expressions. Along with the secure call operator, it allows you to evaluate an expression, check if the evaluation result is null, and save the result as a variable. All of these actions fall into a neat expression. One of the most common uses of nullable arguments is to be passed to a function that takes non-null arguments. For example, the following function, which takes a String argument and sends an email to the address, looks like this in Kotlin:

fun sendEmailTo(email: String) { ... }
Copy the code

You can’t pass null to this function, so you usually need to check and call the function: if(email! = null) sendEmailTo (email). But there is another way: use the let function and call it with a security call. All the let function does is turn an object that calls it into an argument to a lambda expression: email? .let{email -> sendEmailTo(email)} let will only be called if the email value is not null. If the email value is null, {} code will not be executed. Using the concise syntax of the automatically generated name it, you can write: email? }. {sendEmailTo (it). (Lambda syntax is covered in detail only in the section below)

Lazy initialization property

Many frameworks have special methods for initializing objects after an instance is created. In Android, for example, the initialization of an Activity occurs in the onCreate method. JUnit, on the other hand, requires you to put the initialization logic in the @Brefore annotation method. But you can’t completely abandon non-null initializers in the paparazzi method. Just initialize it in a special method. Kotlin usually requires that you initialize all properties in the constructor, and if a property is of a non-null type, you must provide a non-null initialized value. Otherwise, you must use nullable types. If you do this, each access to the property will require a NULL check or!! Operator.

class Activity {
    var view: View? = null

    fun onCreate() {
        view = View()
    }

    fun other() {
        //use view
        view!!.onLongClickListener = ...
    }
}
Copy the code

This can be cumbersome. To get around this problem, use the LateInit modifier to declare an attribute of a non-empty type that does not require an initializer:

class Activity {
    lateinit var view: View

    fun onCreate() {
        view = View()
    }

    fun other() { //use view view.onLongClickListener = ... }}Copy the code

Note that the lazy initialization properties are var because its value needs to be modified outside the constructor, whereas the val property is compiled into a final field that must be initialized in the constructor. Although this property is a non-null type, you do not need to initialize it in the constructor. If you access the property before it has been initialized, you get the exception “LateInit Property xx has not been initialized”, indicating that the property has not been initialized.

Note that one common use of the LateInit property is dependency injection. In this case, the value of the LateInit property is set externally by the dependency injection framework. To ensure compatibility with various Java frameworks, Kotlin automatically generates a field with the same visibility as the LateInit property. If the visibility of the property is public, the visibility of the LateInit field is also public.

public final class Activity {
   public View view;

   public final View getView() {
      View var10000 = this.view;
      if(this.view == null) {
         Intrinsics.throwUninitializedPropertyAccessException("view");
      }
      return var10000;
   }

   public final void setView(@NotNull View var1) {
      Intrinsics.checkParameterIsNotNull(var1, "
      
       "
      ?>);
      this.view = var1;
   }

   public final void onCreate() {
      this.view = new View();
   }

   public final void other() {}}Copy the code

Expansion of nullability

Defining extension functions for nullable types is a more powerful way to handle null values. You can allow a (extension function) call with a null receiver and handle null in that function, rather than calling its methods after ensuring that the variable is NULL. An example of this is the two String extension functions isEmpty and isBlank defined in the Kotlin library. The first function checks if the string is an empty string “”. The second function determines whether it is empty or contains only whitespace characters. It is often valuable to use these functions to examine a string to ensure that operations on it make sense. As you may realize, it’s also useful to handle NULL in the same way that you handle meaningless empty and blank strings. In fact, you can: the isEmptyOrNull and isNullOrBlank functions can be called by String? Type to be called by the receiver.

fun verifyUserInput(input: String?) {
    if(input.isnullorBlank ()) {// This method is a String? Without securely calling println("Please fill in the required fields")}}Copy the code

No exception is raised whether the input is null or a string. Let’s look at the definition of the isNullOrBlank function:

public inline fun CharSequence? .isNullOrBlank(): Boolean = this == null || this.isBlank()Copy the code

Can you see that the extension function is defined for the CharSequence? (the parent class of String), and therefore does not require a secure call like the method calling String. When you define an extension function for a nullable type, it means that you can call the function on nullable values. And this may be null in the function body, so you must explicitly check. In Java, this is always non-null because it refers to an instance of the class you are currently in. In Kotlin, this is not always true: in an extension function of nullable types, this can be null. The let function discussed earlier can also be called by nullable receivers, but it does not check if the value is null. If you call let directly from a nullable type without using the safe call operator, the lambda argument will be nullable:

val person: Person? =... Person.let {sendEmailTo(it)} // There is no security call, so it is nullable ERROR: Type mismatch:inferredtype is Person? but Person was expected
Copy the code

So if you want to use let to check for non-empty arguments, you have to use the secure call operator, right? . Just like the code you saw earlier: person? }. {sentEmailTo (it).

When you define your extension function, consider whether the extension needs nullable type definitions. By default, it should be defined as an extension function of a non-null type. If you find that you need to use this function on nullable types in most cases, you can safely modify it later (without breaking other code).

Nullability of type parameters

The type parameters of all generics and generic functions in Kotlin are nullable by default. Any type, including nullable types, can replace type parameters. In this case, null is allowed for type declarations using type parameters, even though T does not end in a question mark.

fun <T> printHashCode(t: T) { println(t? .hashCode()) }Copy the code

In this function, the type argument T deduces the nullable type Any? So, although it doesn’t end with a question mark. The argument t is still allowed to hold NULL. To use a non-null parameter, you must specify a non-empty upper bound for it, so that the generic rejects nullable values as arguments:

fun <T: Any> printHashCode(t: T) {
    println(t.hashCode())
}
Copy the code

More generics details will be covered in subsequent chapters, but that’s all you need to remember here.

Nullability and Java

We can handle NULL perfectly in Kotlin with nullability, but what about projects that cross Java? Java’s type system does not support nullity, so what to do about it? Nullability information in Java is usually expressed through annotations, and when it occurs in code, Kotlin recognizes it and converts it to the corresponding Kotlin type. For example: @nullable String -> String? , @notnull String -> String. Kotlin can identify many different styles of nullability annotations, Including JSR – 305 standard annotations (javax.mail. The annotation package), Android annotations (Android. Support. Annitation) and JetBrans tools support annotations (org. Jetbrains. Annotations). So that leaves the question, what if there are no annotations?

Platform type

Java types without annotations become platform types in Kotlin. A platform type is essentially a type for which Kotlin does not know the nullability information. It can be treated as a nullable type or as a non-nullable type. This means that you are fully responsible for what you do on this type, just as you are in Java. The compiler will allow all operations. It will not make null-safe operations redundant, as it normally does for null-safe operations on values of non-null types. Let’s say we define a Person class in Java:

public class Person {
    private  String name;

    public String getName() {
        return name;
    }

    public void setName(String name) { this.name = name; }}Copy the code

We use this class in Kotlin:

Fun yellAt(person: person) {println(person.name.toupperCase ()) // Null is not considered, but if null is thrown println((person.name?:"Anyone").toupperCase ()) // Consider null possibility}Copy the code

We can treat it either as a non-null type or as a nullable type.

Kotlin platform types are expressed as: Type! :

val i: Int = person.name

ERROR: Type mistach: inferred type is String! but Int was expected
Copy the code

But you can’t declare a platform type variable; these types can only come from Java code. You can explain platform types however you like:

val person = Person()
val name: String = person.name
val name2: String? = person.name
Copy the code

Of course, if the platform type is null, assignment to a non-null type will still throw an exception.

Why platform types? Is it safer for Kotlin to treat all values from Java as nullable? This design might work, but it would require a lot of redundant NULL checks for values that are never null, because the Kotlin compiler doesn’t know that information. This is even worse when it comes to generics. For example, in Kotlin, every ArrayList from Java is treated as ArrayList<String? >? , the need to check for null values every time the type is accessed or converted cancels out any security benefits. Writing such checks can be very annoying, so Kotlin’s designers made the more practical choice of putting the developer in charge of properly handling the values from Java.

inheritance

When overriding Java methods in Kotlin, you have the option of defining parameters and return types as nullable or non-nullable. For example, let’s look at an example:

/* Java */
interface StringProcessor {
    void process(String value);
}
Copy the code

Both of the following implementation compilers in Kotlin can accept:

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

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

Note that it is important to be aware of nullability when implementing Java class or interface methods. Because method implementations can be called in non-Kotlin code, the Kotlin compiler generates non-null assertions for every non-empty argument you declare. If Java code passes a null value to this method, the assertion will fire and you will get an exception, even if you have never accessed the value of this parameter in your implementation.

Therefore, it is recommended that you use a non-null type for the receiving platform type only if you are sure that no null value will ever occur when the method is called.