Interesting, I found a magical bug in Kotlin!

Posted by Madatan on Thu, 06 Jan 2022 08:11:42 +0100

1. Foreword

This article will introduce a bug in Kotlin from simple to deep through specific business scenarios, tell you the magic of the bug, then lead you to find the cause of the bug, and finally avoid the bug.

2. bug recurrence

In real development, we often have the problem of deserializing the Json string into an object. Here, we use Gson to write a piece of deserialization code, as follows:

fun <T> fromJson(json: String, clazz: Class<T>): T? {
    return try {                                            
        Gson().fromJson(json, clazz)                  
    } catch (ignore: Exception) {                           
        null                                                
    }                                                       
}     

The above code is only applicable to classes without generics. For classes with generics, such as list < T >, we need to modify them as follows:

fun <T> fromJson(json: String, type: Type): T? {
    return try {                                
        return Gson().fromJson(json, type)      
    } catch (e: Exception) {                    
        null                                    
    }                                           
}                                                  

At this point, we can use the TypeToken class in Gson to realize the deserialization of any type, as follows:

//1. Deserialize User object
val user: User? = fromJson("{...}}", User::class.java)

//2. Deserialize the list < user > object. Other classes with generics can be serialized with this method
val type = object : TypeToken<List<User>>() {}.type
val users: List<User>? = fromJson("[{..},{...}]", type)

The above wording is translated from Java syntax. It has a disadvantage that the transfer of generic types must be realized through another class. We use the TypeToken class above. I believe this is unacceptable to many people. Therefore, a new keyword modified appears on kotlin (I don't introduce it here. If you don't understand it, please refer to relevant materials by yourself), Combined with the characteristics of kotlin's inline function, it can directly obtain specific generic types inside the method. We transform the above method again, as follows:

inline fun <reified T> fromJson(json: String): T? {
    return try {
        return Gson().fromJson(json, T::class.java)
    } catch (e: Exception) {
        null
    }
}

You can see that we added the inline keyword before the method, indicating that it is an inline function; Then add the modified keyword in front of generic T and remove the unnecessary Type parameter in the method; Finally, we pass T:: class Java passes specific generic types, which are used as follows:

val user = fromJson<User>("{...}}")
val users = fromJson<List<User>>("[{..},{...}]")

When we test the above code with confidence, the problem occurs, and the deserialization of list < user > fails, as follows:

The object in the List is not a User, but a LinkedTreeMap. What's the matter? Is this the Kotlin bug mentioned in the title? Of course not!

We go back to the fromjason method and see that the internal transfer is t:: class Java object, that is, class object. If the class object has generics, the generics will be erased during operation. Therefore, if it is a list < user > object, it will become a list during operation Class object, and when Gson receives ambiguous generics, it will automatically deserialize the json object into a LinkedTreeMap object.

How? Easy to handle. We can pass generics through the TypeToken class. This time, we only need to write once inside the method, as follows:

inline fun <reified T> fromJson(json: String): T? {
    return try {
        //Get the specific generic type with the help of TypeToken class
        val type = object : TypeToken<T>() {}.type
        return Gson().fromJson(json, type)
    } catch (e: Exception) {
        null
    }
}

At this point, let's test the above code again, as follows:

You can see that this time, both the User and the list < User > object are deserialized successfully.

At this point, some people will have questions. After talking so much, what about the good Kotlin bug? Don't worry. Keep looking down. The bug is about to appear.

Suddenly one day, your leader came to tell you whether the fromjason method can be optimized. Now every time you deserialize the List collection, you need to write < List < > > after fromjason. There are many scenarios, and it's a little cumbersome to write.

At this time, 10000 things jump in your mind, but calm down and think about it. What the leader said is not unreasonable. If you encounter multi-layer generics, it will be more cumbersome to write, such as: fromjason < baseresponse < list < user > >,

Therefore, you open the way of optimization and decouple the commonly used generic classes. Finally, you write the following code:

inline fun <reified T> fromJson2List(json: String) = fromJson<List<T>>(json)

Test, eh? Stunned, deja vu questions are as follows:

Why? fromJson2List only calls the fromjason method internally. Why can fromjason, but fromJson2List fails? I can't think about it.

Is this the Kotlin bug in the title? Very responsible to tell you, yes;

Where is the magic of bug? Keep looking down

3. The magic of bug s

Let's re sort out the whole event. We first defined two methods and put them in Jason KT file, the complete code is as follows:

@file:JvmName("Json")

package com.example.test

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

inline fun <reified T> fromJson2List(json: String) = fromJson<List<T>>(json)

inline fun <reified T> fromJson(json: String): T? {
    return try {
        val type = object : TypeToken<T>() {}.type
        return Gson().fromJson(json, type)
    } catch (e: Exception) {
        null
    }
}

Then create a new User class. The complete code is as follows:

package com.example.bean

class User {
    val name: String? = null
}

Then create a new jsontest KT file, the completion code is as follows:

@file:JvmName("JsonTest")

package com.example.test

fun main() {
    val user = fromJson<User>("""{"name": "Zhang San"}""")
    val users = fromJson<List<User>>("""[{"name": "Zhang San"},{"name": "Li Si"}]""")
    val userList = fromJson2List<User>("""[{"name": "Zhang San"},{"name": "Li Si"}]""")
    print("")
}

Note: these three classes are under the same package name and in the same Module

Finally, execute the main method and you will find the bug.

Attention, high energy ahead: we put Jason Copy a copy of the KT file to the Base Module as follows:

@file:JvmName("Json")

package com.example.base

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

inline fun <reified T> fromJson2List(json: String) = fromJson<List<T>>(json)

inline fun <reified T> fromJson(json: String): T? {
    return try {
        val type = object : TypeToken<T>() {}.type
        return Gson().fromJson(json, type)
    } catch (e: Exception) {
        null
    }
}

Then, we found Jason in the app module KT file, add a test method as follows:

fun test() {
    val users = fromJson2List<User>("""[{"name": "Zhang San"},{"name": "Li Si"}]""")
    val userList = com.example.base.fromJson2List<User>("""[{"name": "Zhang San"},{"name": "Li Si"}]""")
    print("")
}

Note: JSON. In base module This method is not in the KT file

In the above code, the fromJson2List method in app module and base module is executed respectively. Let's guess the expected result of the above code execution

The first statement, with the above case, will obviously return the list < linkedtreemap > object; What about the second one? It is reasonable to return the list < linkedtreemap > object. However, it backfires. Let's take a look, as follows:

As you can see, the fromJson2List , method in app module failed to deserialize list < user >, while the fromJson2List , method in base module succeeded.

The same code, but the module s are different, and the execution results are also different. Do you think God is magical?

4. Find out

I know the bug and the magic of the bug. Next, I'll explore why it happened? Where to start?

Obviously, I'm going to see Jason KT class bytecode file. Let's take a look at JSON. In base module Class file, as follows:

Note: for the following bytecode files, some annotation information will be deleted for easy viewing

package com.example.base;

import com.google.gson.reflect.TypeToken;
import java.util.List;

public final class Json {

  public static final class Json$fromJson$type$1 extends TypeToken<T> {}

  public static final class Json$fromJson2List$$inlined$fromJson$1 extends TypeToken<List<? extends T>> {}
}

You can see, Jason After the two inline methods in KT are compiled into bytecode files, they become two static internal classes, and both inherit the TypeToken class. It seems that there is no problem,

Continue to look at the jason.com of app module The bytecode file corresponding to KT file is as follows:

package com.example.test;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;

public final class Json {
  public static final void test() {
    List list;
    Object object = null;
    try {
      Type type = (new Json$fromJson2List$$inlined$fromJson$2()).getType();
      list = (List)(new Gson()).fromJson("[{\"name\": \"\"},{\"name\": \"\"}]", type);
    } catch (Exception exception) {
      list = null;
    } 
    (List)list;
    try {
      Type type = (new Json$test$$inlined$fromJson2List$1()).getType();
      object = (new Gson()).fromJson("[{\"name\": \"\"},{\"name\": \"\"}]", type);
    } catch (Exception exception) {}
    (List)object;
    System.out.print("");
  }

  public static final class Json$fromJson$type$1 extends TypeToken<T> {}

  public static final class Json$fromJson2List$$inlined$fromJson$1 extends TypeToken<List<? extends T>> {}

  public static final class Json$fromJson2List$$inlined$fromJson$2 extends TypeToken<List<? extends T>> {}

  public static final class Json$test$$inlined$fromJson2List$1 extends TypeToken<List<? extends User>> {}
}

In the bytecode file, there are 1 test Method + 4 static internal classes; The first two static inner classes are JSON KT file two inline methods compiled results, this can be ignored.

Next, let's take a look at the test method. This method has two deserialization processes. The first time, it calls the static internal class JsonfromJson2List$$inlinedfromJson `, and the second time, it calls the static internal class' Jsontest$$inlinedfromJson2List ', that is, it calls the third and fourth static internal classes respectively to obtain specific generic types, The generic types declared by these two static inner classes are different, which are < list <? Extensions T > > and < list <? Extensions user > >, it is estimated that everyone understands. Obviously, the generics in the first deserialization process were erased, resulting in deserialization failure.

As for why we rely on the method of this module, when we encounter the combination of generic T and specific classes, generic T will be erased. This needs to be answered on the official website of Kotlin. If you know the reason, you can leave a message in the comment area.

5. Expand

If your project does not rely on Gson, you can customize a class to obtain specific generic types, as follows:

open class TypeLiteral<T> {
    val type: Type
        get() = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
}

//Replace the relevant code of TypeToken class with the following code
val type = object : TypeLiteral<T>() {}.type

For the combination of generic types, you can also use the ParameterizedTypeImpl class in the RxHttp library. The usage is as follows:

//Get list < user > type
val type: Type = ParameterizedTypeImpl[List::class.java, User::class.java]

For detailed usage, you can view Android and Java generic literacy

6. Summary

At present, to avoid this problem, move the relevant code to the sub module, and there will be no generic erasure problem when calling the sub module code;

This problem is actually in kotlin 1.3 I found it in version x, and the latest version has always existed. During this period, I consulted the great God Bennyhuo. Later, I avoided this problem, so I can't rest assured. In the near future, I will submit this problem to kotlin officials and hope to repair it as soon as possible.

Finally, I recommend a network request library RxHttp, which supports Kotlin collaboration, RxJava2 and RxJava3. Any request can be completed in three steps. Up to now, there are 2.7k+ star, which is a really good library. I strongly recommend it

Advanced notes of Android advanced development system, latest interview review notes PDF, My GitHub

end of document

Your favorite collection is my greatest encouragement!
Welcome to follow me, share Android dry goods and exchange Android technology.
If you have any opinions on the article or any technical problems, please leave a message in the comment area for discussion!

Topics: Android kotlin