1, the preface

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

episode

I am participating in the list of the most popular creators of the Nuggets in 2020, I hope you can help me vote, 2021 will bring you more quality articles.

2. Bug recurrence

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

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

List

= List

= List

= List

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

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

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

//2, deserialize the List
      
        object, and any other class with a generic type
      
val type = object : TypeToken<List<User>>() {}.type
val users: List<User>? = fromJson("[{...}, {...}]", type)
Copy the code

This is a translation of the Java syntax, but it has the drawback that you have to pass a generic through another class. We use the TypeToken class to do this, which is also unacceptable to many people. As a result, Kotlin has introduced a new keyword reified as well. Kotlin’s inline function allows you to retrieve a specific generic type directly from within the method.

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

As you can see, we preceded the method with the inline keyword to indicate that this is an inline function; Then add reified before the generic T and remove the Type parameter that is not required in the method. Finally, we pass the specific generic type through T::class.java as follows:

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

When we tested the above code with confidence, the problem arose that the deserialization of the List

failed, as follows:

The List object is not User, but LinkedTreeMap. This is the Kotlin bug. Of course not!

If we go back to the fromJson method, we see that we’re passing a T::class.java object internally, which is a class object, and if a class object has a generic type, the generic type is erased at runtime, so if it’s a List

object, it’s a List. Class object at runtime. Gson automatically deserializes the JSON object to a LinkedTreeMap object when it receives an undefined generality.

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

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

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

As you can see, this time both the User and the List

objects are deserialized successfully.

At this point, one might wonder, after all this talk, what about the Kotlin bug? Don’t worry, keep reading, the bug is about to appear.

One day, your leader came to you and asked if the fromJson method could be optimized. Now every time you deserialize the List collection, you need to write > after fromJson. There are many such scenarios, and it is a little tedious to write.

FromJson

>>, fromJson

>>, fromJson

>>

This opens the way for optimization, decoupling the common generic classes, and you end up writing code like this:

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

Test it, huh? Stunned, deja vu question, as follows:

Why is that? FromJson2List internal call only fromJson method, why can fromJson, fromJson2List failed, a mystery.

Is this the Kotlin bug? Responsibly enough to tell you, yes;

What’s the magic? Keep reading

3. The magic of bugs

Let’s rearrange the whole event. We defined two methods above and put them in json.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}}Copy the code

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

package com.example.bean

class User {
    val name: String? = null
}
Copy the code

Create a jsontest. kt file and complete the code as follows:

@file:JvmName("JsonTest")

package com.example.test

fun main(a) {
    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("")}Copy the code

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

Finally, the main method is executed, and the said bug is discovered.

Copy the json. 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}}Copy the code

Then we add a test method to the json. kt file in the app Module, as follows:

fun test(a) {
    val users = fromJson2List<User>("""[{"name": "zhang SAN "},{"name":" Li Si "}]""" ")
    val userList = com.example.base.fromJson2List<User>("""[{"name": "zhang SAN "},{"name":" Li Si "}]""" ")
    print("")}Copy the code

Note: There is no such method in the json. kt file in the base Module

Let’s guess the expected result of the fromJson2List method in the app Module and fromJson2List method in the Base Module

The first statement, with the above example, obviously returns the List object; What about number two? You should return a List object, but instead, do the following:

As you can see, the fromJson2List method in the App Module failed to deserialize the List

, while the fromJson2List method in the Base Module succeeded.

The same code, but the module is not the same, the execution result is not the same, you say god is not magic?

4. Find out

Now that you know the bug, and you know the magic of the bug, let’s explore, why does it happen? Where to start?

Obviously, to see the bytecode file of the json. kt class, let’s first look at the json. class file in the Base Module.

Note: The following bytecode files 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$typeThe $1extends TypeToken<T> {}
 
  public static final class Json$fromJson2List$$inlined$fromJsonThe $1extends TypeToken<List<? extends T>> {}}Copy the code

As you can see, the two inline methods in js.kt, when compiled into a bytecode file, become two static inner classes, and both inherit from the TypeToken class, which looks fine.

Take a look at the bytecode file for the app Module json. kt file, 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(a) {
    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$typeThe $1extends TypeToken<T> {}
  
  public static final class Json$fromJson2List$$inlined$fromJsonThe $1extends 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$fromJson2ListThe $1extends TypeToken<List<? extends User>> {}}Copy the code

In the bytecode file, there is 1 test method + 4 static inner classes; The first two static inner classes are the compiled results of the two inline methods in the json.kt file, which can be ignored.

Next, look at the test method, which is deserialized twice. The first time it calls the static inner class JsonfromJson2List$$inlinedfromJson$2, and the second time it calls the static inner class Jsontest$$inlinedfromJson2List$1. The third and fourth static inner classes, respectively, are called to retrieve the specific generic type. The two static inner classes declare different generic types: > and < < List? Extends User>> extends User>> extends User>> extends User>> extends User>> >

If you want to use this module’s method, the generic T will be erased when it is combined with a specific class. If you want to use this module’s method, please leave a comment in the comments section.

5, extension,

If your project does not depend on Gson, you can customize a class to retrieve the specific generic type, as follows:

open class TypeLiteral<T> {
    val type: Type
        get() = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]}// Replace the TypeToken class code with the following code
val type = object : TypeLiteral<T>() {}.type
Copy the code

For combinations of generics, you can also use the ParameterizedTypeImpl class from the RxHttp library.

// Get the List
      
        type
      
val type: Type = ParameterizedTypeImpl[List::class.java, User::class.java]
Copy the code

Detailed usage can see Android, Java generic literacy

6, summary

For now, to get around this problem, simply move the code to the submodule and call the submodule code without generic erasers.

In fact, I found this problem in the 1.3.x version of Kotlin, and it has been existing in the latest version till now. I consulted The Great God Bennyhuo during the period.

If this article helped you, please vote for me. Thank you! Vote for me on the Nuggets’ top creators of the year list in 2020, and there will be more great articles coming in 2021.

Finally, I recommend a network request library RxHttp, support Kotlin coroutine, RxJava2, RxJava3, any request three steps to get, so far there are 2.7K + STAR, really good a library, highly recommended