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:
- through
constructor.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