烦恼一般都是想太多了。

0%

Gson使用泛型时候遇到问题

今天在想统一使用 Gson 进行获取服务端数据后出现了一个头疼的问题。对于使用泛型的时候会出现 LinkTreeMap 不能 Cast To 对象的问题。通过谷歌,了解原因。

返回格式

我们的服务端返回的数据格式都是一致的。

{
"state": 200,
"data":
}

data 有的时候会是个对象,有的时候是个列表。很自然的就构造一个 Java 类:

public class ServerApiResult<T> {
private int state;
private T data;
}

我们统一处理网络请求的类:

@Override
public void onResponse(Call<ServerApiResult> call, Response<ServerApiResult> response) {
Response<ServerApiResult> res = response;
ServerApiResult data = response.body();
// 请求成功,http 协议返回代码 200-300
if (res.isSuccessful()) {
if (data.getState() != 200) {
if (data.getData() == null || TextUtils.isEmpty(data.getData().toString())) {
if (!TextUtils.isEmpty(CodeConfig.getCodeTip(data.getState()))) {
onFail(CodeConfig.getCodeTip(data.getState()));
} else {
onFail("请求失败,请重试");
}
} else {
onFail(data.getData().toString());
}
return;
}
Log.d(TAG, "onResponse: " + data.getData());
Log.d(TAG, "onResponse: "+data.getClass());
Log.d(TAG, "onResponse: "+data.getData().getClass());
onSuccessfull(data.getData());
} else {
onFail(res.message());
}
}

结果就是会在下面的调用中抛出异常:

UncaughtException detected: java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to cn.nanming.smartcompany.ui.activity.comregister.requester.entity.ComRegisterBaseInfo

Call<ServerApiResult> req = netClient.getBaseInfo(sn);
req.enqueue(new NetCallback<ComRegisterBaseInfo>() {
@Override
public void onSuccessfull(ComRegisterBaseInfo data) {
callback.success(data);
}
});

通过 Log.d 打印出的日志来看。 返回的数据本身,转换是成功的。而对于 data 字段,其属于泛型,Gson 没有进行转换,而是保存在了一个 LinkedTreeMap 中,直接使用是不行的。

那怎么破?这是因为 JAVA 的类型擦除在做怪。

类型擦除

根据 Java 官方文档

  • 将泛型的所有类型参数都替换为其绑定的类型,若没有绑定的类型,则替换为 object。生成的字节码就只会是普通的类,接口或方法。
  • 插入类型转换来保护类型安全。
  • 生成桥接方法来保护级承了泛型的多态。

具体点来个例子:

public class Node<T> {

private T data;
private Node<T> next;

public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}

public T getData() { return data; }
// ...
}

将会擦除过程中,会替换为:

public class Node<T> {

private T data;
private Node<T> next;

public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}

public T getData() { return data; }
// ...
}

但如果泛型有绑定类型:

public class Node<T extends Comparable<T>> {

private T data;
private Node<T> next;

public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}

public T getData() { return data; }
// ...
}

则会替换成:

public class Node {

private Comparable data;
private Node next;

public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}

public Comparable getData() { return data; }
// ...
}

所以,实际上我们的 ServerApiResult 并不知道我们的 data 类型。

官方文档

查看官方文档针对泛型一节的说明:

当我们调用 toJson(obj) 的时候,Gson 会调用 obj.getClass() 来获取字段信息来序列化。同样,我们调用 fromJson(obj, MyClass.class) 的时候根据传递了一个 Class 对象过去。这工作得很好,但是遇到泛型就悲剧了。当 obj 是泛型的时候,泛型的信息会被擦除Java 类型擦除。例如:

class Foo<T> {
T value;
}
Gson gson = new Gson();
Foo<Bar> foo = new Foo<Bar>();
gson.toJson(foo); // May not serialize foo.value correctly

gson.fromJson(json, foo.getClass()); // Fails to deserialize foo.value as Bar

上面的例子,并不会解析出类型 Bar 的值。因为 Gson 调用 foo.getClass() 来获取类信息,但这个方法返回的是一个 raw 类, Foo.class。也就说 Gson 并不知道这个对象的类型是 Foo,同上,因为类型擦除,其是 Bar 被解释为 Object,存在 LinkedTreeMap 中。

提供的解决方法是使用 TypeToken 类:

Type fooType = new TypeToken<Foo<Bar>>() {}.getType();
gson.toJson(foo, fooType);

gson.fromJson(json, fooType);

fooType之所以能获取,是因为后面的代码是定义了一个匿名本地内部类,且有一个方法 getType() 返回了全部的参数类型。

TypeToken<T> 表示一个类型 T。Java 当前没有提供表示一个泛型的方法,这个类可以。强制建立这么一个类可以让我们在运行时获取类型信息。

Map 的解析

在我们安卓进行解析的时候,有一个比较蛋疼的事情就是,我们的解析器无法解析属于 Map 类型的返回值。

因此我准备用 Gson 来解决这个问题,用 Gson 就很简单了。

new Gson().fromJson(strBody, new TypeToken<Map<String,List<SrvRes>>>(){}.getType());

这样也就 OK 了。