Public number: byte array, hope to help you πŸ˜‡πŸ˜‡

A long time ago, a colleague asked me a question about Gson and Kotlin dataClass. I couldn’t answer it at that time, but I kept it in my heart. Today, I will seriously explore the reason and output a summary, hoping to help you avoid this pit πŸ˜‚πŸ˜‚

Let’s take a quick example and guess what the result will be

/ * * *@Author: leavesC
 * @Date: 2020/12/21 "*@Desc: * GitHub:https://github.com/leavesC * /
data class UserBean(val userName: String, val userAge: Int)

fun main(a) {
    val json = """{"userName":null,"userAge":26}"""  
    val userBean = Gson().fromJson(json, UserBean::class.java) / / the first step
    println(userBean) / / the second step
    printMsg(userBean.userName) / / the third step
}

fun printMsg(msg: String){}Copy the code

UserBean is a dataClass whose userName is declared to be a non-NULL value. The value of userName in the json string is null. Can the program successfully run the above three steps?

In fact, the program runs normally until the second step, but when it executes the third step, it directly reports an NPE exception

UserBean(userName=null, userAge=26)
Exception in thread "main" java.lang.NullPointerException: Parameter specified as non-null is null: method temp.TestKt.printMsg, parameter msg
	at temp.TestKt.printMsg(Test.kt)
	at temp.TestKt.main(Test.kt:16)
	at temp.TestKt.main(Test.kt)
Copy the code

First, why is NEP thrown?

The printMsg method takes an argument and does nothing. Why does it throw an NPE?

If printMsg is decompilated into a Java method using IDEA, NullPointerException will be thrown if null is found inside the method

   public static final void printMsg(@NotNull String msg) {
      Intrinsics.checkNotNullParameter(msg, "msg");
   }
Copy the code

This makes sense, because Kotlin’s type system makes a strict distinction between nullable and non-nullable types, and one of the ways it does that is by automatically inserting some type checking logic into our code that automatically adds non-null assertions, NPE is thrown immediately if null is passed to a non-null argument, even if the argument is not used

Of course, the auto-insert validation logic is only generated in the Kotlin code. If we had passed userbean.username to the Java method, we would not have this effect. Instead, we would have waited until we had used the parameter

Is Kotlin’s nullSafe invalid?

Since the userName field in the UserBean has already been declared as a non-NULL type, why can deserialization succeed? How did Gson get around Kotlin’s null check when my first instinct was to throw an exception when unsequencing?

So this is how does Gson implement antisequence

Through a breakpoint, can discover the UserBean is in ReflectiveTypeAdapterFactory complete build, here’s the main steps are divided into two steps:

  • throughconstructor.construct()You get a UserBean object with the default values for the properties inside the object
  • The JsonReader is iterated over, corresponding to the key value inside the Json to the fields contained in the UserBean, and assigned to the fields that are due

The second step is easy to understand, but what about the first step? Again, how is constructive.construct () implemented

The constructor method can be seen in the ConstructorConstructor class

There are three possibilities:

  • NewDefaultConstructor. The object is generated by reflecting the no-argument constructor
  • NewDefaultImplementationConstructor. Generate objects for Collection frame types such as Collection and Map by reflection
  • NewUnsafeAllocator. Generating objects through the Unsafe package is a last-ditch solution

First of all, the second one definitely doesn’t qualify. Just look at the first one and the third one

As a dataClass, does the UserBean have a parameterless constructor? The decompilation shows that there is no such thing as a constructor with two arguments, so the first step will definitely fail

public final class UserBean {
   @NotNull
   private final String userName;
   private final int userAge;

   @NotNull
   public final String getUserName() {
      return this.userName;
   }

   public final int getUserAge() {
      return this.userAge;
   }

   public UserBean(@NotNull String userName, int userAge) {
      Intrinsics.checkNotNullParameter(userName, "userName");
      super(a);this.userName = userName;
      this.userAge = userAge; }...}Copy the code

In addition, there is a way to verify that the UserBean is not called to the constructor. We can declare a parent class for the UserBean, and then check whether the parent init block prints logs to see if the UserBean has been called to the constructor

open class Person() {

    init {
        println("Person")}}data class UserBean(val userName: String, val userAge: Int) : Person()
Copy the code

How does newUnsafeAllocator work

Unsafe is a class under the Sun. misc package. Unsafe provides methods for performing low-level, insecure operations, such as accessing and managing memory resources. The Unsafe class, however, gives the Java language the ability to manipulate memory space in the same way that Pointers do in C, which increases the risk of pointer-related problems. Improper use of the Unsafe class in applications increases the risk of errors, making Java, a secure language, Unsafe. Therefore, beware of Unsafe applications

Unsafe provides an unconventional way to instantiate objects: AllocateInstance, which provides the ability to create an instance of a Class object without calling its constructor, initialization code, JVM security checks, etc., even if the constructor is private

Gson’s UnsafeAllocator class initializes the UserBean through the allocateInstance method, so its constructor is not called

To sum up:

  • The UserBean constructor has only one constructor, which contains two construction parameters. In the constructor, the userName field is also checked for null. If null is found, the NPE is directly thrown
  • Gson instantiates the UserBean object through the Unsafe package and does not call its constructor, bypassing Kotlin’s null check, so even a null userName can be deserialized

Is the default value of the construction parameter invalid?

Let’s do another example

If we set a default value for the userName field of the UserBean and the KEY is not included in the JSON, we will find that the default value will not take effect and will still be null

/ * * *@Author: leavesC
 * @Date: 2020/12/21 "*@Desc: * GitHub:https://github.com/leavesC * /
data class UserBean(val userName: String = "leavesC".val userAge: Int)

fun main(a) {
    val json = """{"userAge":26}"""
    val userBean = Gson().fromJson(json, UserBean::class.java)
    println(userBean)
}
Copy the code
UserBean(userName=null, userAge=26)
Copy the code

Setting a default value for a construction parameter is a common requirement to reduce the cost of user initialization of the object, and it is desirable to have a default value for a field that may not be returned by the interface if the UserBean is used as a carrier for the network request interface

From the previous section, the Unsafe package does not call any of the UserBean’s constructors, so the default values will definitely not take effect. Alternative solutions are needed, including:

1. No-parameter constructor

UserBean provides a no-argument constructor that allows Gson to instantiate the UserBean by reflecting it, thereby simultaneously assigning the default value

data class UserBean(val userName: String, val userAge: Int) {

    constructor() : this("leavesC".0)}Copy the code

2. Add annotations

This can be done by adding an @jvMoverloads annotation to the constructor, which in effect solves the problem by providing a no-parameter constructor. The disadvantage is that you need to provide a default value for each constructor parameter in order to generate a no-parameter constructor

data class UserBean @JvmOverloads constructor(
    val userName: String = "leavesC".val userAge: Int = 0
)
Copy the code

3. Declare as a field

This approach is similar to the previous two, and is implemented by indirectly providing a no-parameter constructor. Declare all fields inside the class instead of construction parameters, and the declared fields also have default values

class UserBean {

    var userName = "leavesC"

    var userAge = 0

    override fun toString(a): String {
        return "UserBean(userName=$userName, userAge=$userAge)"}}Copy the code

4. Switch to Moshi

Because Gson is designed for the Java language, it is not currently Kotlin friendly, so the default does not take effect directly. Instead, we can use another Json serialization library: Moshi

Moshi is an open source library provided by Square, which has much more support for Kotlin than Gson

Import dependencies:

dependencies {
    implementation : 'com. Squareup. Moshi moshi - kotlin: 1.11.0'
}
Copy the code

No special operations are required and the default values take effect when deserializing

data class UserBean(val userName: String = "leavesC".val userAge: Int)

fun main(a) {
    val json = """{"userAge":26}"""
    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())
        .build()
    val jsonAdapter: JsonAdapter<UserBean> = moshi.adapter(UserBean::class.java)
    val userBean = jsonAdapter.fromJson(json)
    println(userBean)
}
Copy the code
UserBean(userName=leavesC, userAge=26)
Copy the code

However, if the userName field in the JSON string explicitly returns NULL, the type verification fails and an exception is thrown directly, which is strictly Kotlin style

fun main(a) {
    val json = """{"userName":null,"userAge":26}"""
    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())
        .build()
    val jsonAdapter: JsonAdapter<UserBean> = moshi.adapter(UserBean::class.java)
    val userBean = jsonAdapter.fromJson(json)
    println(userBean)
}
Copy the code
Exception in thread "main" com.squareup.moshi.JsonDataException: Non-null value 'userName' was null at $.userName
	at com.squareup.moshi.internal.Util.unexpectedNull(Util.java:663)
	at com.squareup.moshi.kotlin.reflect.KotlinJsonAdapter.fromJson(KotlinJsonAdapter.kt:87)
	at com.squareup.moshi.internal.NullSafeJsonAdapter.fromJson(NullSafeJsonAdapter.java:41)
	at com.squareup.moshi.JsonAdapter.fromJson(JsonAdapter.java:51)
	at temp.TestKt.main(Test.kt:21)
	at temp.TestKt.main(Test.kt)
Copy the code

Fourth, expand knowledge

Let’s look at the extension knowledge, which is not directly related to Gson, but is also an important knowledge point in development

Json is an empty string, in which case Gson can successfully deserialize and the resulting userBean is NULL

fun main(a) {
    val json = ""
    val userBean = Gson().fromJson(json, UserBean::class.java)
}
Copy the code

If you add a type declaration: UserBean? That can also be successfully deserialized

fun main(a) {
    val json = ""
    val userBean: UserBean? = Gson().fromJson(json, UserBean::class.java)
}
Copy the code

If the type declaration is UserBean, then it’s more fun and throws a NullPointerException

fun main(a) {
    val json = ""
    val userBean: UserBean = Gson().fromJson(json, UserBean::class.java)
}
Copy the code
Exception in thread "main" java.lang.NullPointerException: Gson().fromJson(json, UserBean::class.java) must not be null
	at temp.TestKt.main(Test.kt:22)
	at temp.TestKt.main(Test.kt)
Copy the code

What makes these three examples different?

This comes down to Kotlin’s platform type. One of Kotlin’s unique features is its ability to communicate 100 percent with Java. The platform type is Kotlin’s design for Java to be balanced. Kotlin divides the types of objects into nullable and non-nullable types. However, all object types of the Java platform are nullable. When referencing Java variables in Kotlin, if all variables are classified as nullable, there will be many more NULL checks. If both types are considered non-nullable, it is easy to write code that ignores the risks of NPE. To balance the two, Kotlin introduced platform types, where Java variable values referenced in Kotlin can be treated as nullable or non-nullable types, leaving it up to the developer to decide whether or not to null-check

So, when we inherit a variable returned by the Java class Gson from Kotlin, we can treat it as either a UserBean type or a UserBean type, right? Type. If we explicitly declare it as a UserBean type, we know we are returning a non-null type, which triggers Kotlin’s NULL check, causing a NullPointerException to be thrown

Here’s another Kotlin tutorial article from me: 26,000 words to get you started on Kotlin