1. 概述

在这个教程中,我们将讨论如何将一个JSON数组转换为等效的java.util.List对象。Google提供的Gson库是一个Java库,它帮助我们将JSON字符串转换为Java对象,反之亦然。

这个库中的Gson类有一个方法fromJson(),它接受两个参数:第一个是JSON字符串,第二个是java.lang.reflect.Type类型的参数。该方法会将JSON字符串转换为由其第二个参数表示的Java对象类型。

我们将创建一个通用方法,如convertJsonArrayToListOfAnyType(String jsonArray, T elementType),它可以将JSON数组转换为List<T>,其中TList元素的类型。

让我们深入了解这一点。

2. 问题描述

假设我们有两个JSON数组,一个是关于Student的,另一个是关于School的:

final String jsonArrayOfStudents =
    "["
      + "{\"name\":\"John\", \"grade\":\"1\"}, "
      + "{\"name\":\"Tom\", \"grade\":\"2\"}, "
      + "{\"name\":\"Ram\", \"grade\":\"3\"}, "
      + "{\"name\":\"Sara\", \"grade\":\"1\"}"
  + "]";
final String jsonArrayOfSchools =
    "["
      + "{\"name\":\"St. John\", \"city\":\"Chicago City\"}, "
      + "{\"name\":\"St. Tom\", \"city\":\"New York City\"}, "
      + "{\"name\":\"St. Ram\", \"city\":\"Mumbai\"}, "
      + "{\"name\":\"St. Sara\", \"city\":\"Budapest\"}"
  + "]";

通常,我们可以使用Gson类将数组转换为List对象:

@Test
void givenJsonArray_whenListElementTypeDynamic_thenConvertToJavaListUsingTypeToken() {
    Gson gson = new Gson();
    TypeToken<List<Student>> typeTokenForListOfStudents = new TypeToken<List<Student>>(){};
    TypeToken<List<School>> typeTokenForListOfSchools = new TypeToken<List<School>>(){};
    List<Student> studentsLst = gson.fromJson(jsonArrayOfStudents, typeTokenForListOfStudents.getType());
    List<School> schoolLst = gson.fromJson(jsonArrayOfSchools, typeTokenForListOfSchools.getType());
    assertAll(
      () -> studentsLst.forEach(e -> assertTrue(e instanceof Student)),
      () -> schoolLst.forEach(e -> assertTrue(e instanceof School))
    );
}

我们为List<Student>List<School>创建了TypeToken对象。最后,使用这些TypeToken对象,我们得到了Type对象,并成功地将JSON数组转换为了List<Student>List<School>

为了促进重用,让我们尝试创建一个带有一个方法的通用类,该方法可以接收List元素的类型并返回Type对象:

class ListWithDynamicTypeElement<T> {
    Type getType() {
        TypeToken<List<T>> typeToken = new TypeToken<List<T>>(){};
        return typeToken.getType();
    }
}

对于给定的元素类型T,我们实例化了一个泛型的TypeToken<List<T>>。然后,我们返回相应的Type对象。列表的元素类型只在运行时可用。

让我们看看这个方法的实际应用:

@Test
void givenJsonArray_whenListElementTypeDynamic_thenConvertToJavaListUsingTypeTokenFails() {
    Gson gson = new Gson();
    List<Student> studentsLst = gson.fromJson(jsonArrayOfStudents, new ListWithDynamicTypeElement<Student>().getType());
    assertFalse(studentsLst.get(0) instanceof Student);
    assertThrows(ClassCastException.class, () -> studentsLst.forEach(e -> assertTrue(e instanceof Student)));
}

尽管方法编译通过,但在运行时却失败了,当我们试图遍历studentLst时,会抛出ClassCastException。此外,我们看到列表中的元素并非Student类型。

3. 使用TypeToken.getParameterized()解决方案

在Gson 2.10版本中,TypeToken类新增了getParameterized()方法(https://javadoc.io/static/com.google.code.gson/gson/2.10.1/com.google.gson/com/google/gson/reflect/TypeToken.html#getParameterized\(java.lang.reflect.Type,java.lang.reflect.Type...\)),这使得开发者能够处理在编译时无法获取列表元素类型的情况。

让我们看看这个新方法如何帮助我们获取参数化类的Type信息:

Type getGenericTypeForListFromTypeTokenUsingGetParameterized(Class elementClass) {
    return TypeToken.getParameterized(List.class, elementClass).getType();
}

getParameterized()方法在运行时被调用时,它会返回List对象的实际类型及其元素类型。这将有助于Gson类根据元素的正确类型信息成功将JSON数组转换为对应的List对象。

现在,让我们看看方法的使用:

@Test
void givenJsonArray_whenListElementTypeDynamic_thenConvertToJavaListUsingGetParameterized() {
    Gson gson = new Gson();
    List<Student> studentsLst = gson.fromJson(jsonArrayOfStudents, getGenericTypeForListFromTypeTokenUsingGetParameterized(Student.class));
    List<School> schoolLst = gson.fromJson(jsonArrayOfSchools, getGenericTypeForListFromTypeTokenUsingGetParameterized(School.class));
    assertAll(
      () -> studentsLst.forEach(e -> assertTrue(e instanceof Student)),
      () -> schoolLst.forEach(e -> assertTrue(e instanceof School))
    );
}

我们使用getGenericTypeForListFromTypeTokenUsingGetParameterized()方法获取List<Student>List<School>Type信息。最终,使用fromJson()方法,我们成功将JSON数组转换为它们各自的Java List对象。

4. 使用JsonArray解决方案

Gson库有一个JsonArray类来表示JSON数组。我们将使用它来将JSON数组转换为List对象:

<T> List<T> createListFromJsonArray(String jsonArray, Type elementType) {
    Gson gson = new Gson();
    List<T> list = new ArrayList<>();
    JsonArray array = gson.fromJson(jsonArray, JsonArray.class);
    for(JsonElement element : array) {
        T item = gson.fromJson(element, elementType);
        list.add(item);
    }
    return list;
}

首先,我们使用Gson类的常规fromJson()方法将JSON数组字符串转换为JsonArray对象。然后,我们将JsonArray对象中的每个JsonElement元素转换为目标类型,这是createListFromJsonArray()方法的第二个参数elementType定义的。

将这些转换后的元素放入List中,最后在方法结束时返回。

现在,让我们看看方法的使用:

@Test
void givenJsonArray_whenListElementTypeDynamic_thenConvertToJavaListUsingJsonArray() {
    List<Student> studentsLst = createListFromJsonArray(jsonArrayOfStudents, Student.class);
    List<School> schoolLst = createListFromJsonArray(jsonArrayOfSchools, School.class);
    assertAll(
      () -> studentsLst.forEach(e -> assertTrue(e instanceof Student)),
      () -> schoolLst.forEach(e -> assertTrue(e instanceof School))
    );
}

我们使用createListFromJsonArray()方法成功地将学生和学校的两个JSON数组转换为List<Student>List<School>

5. 使用Guava的TypeToken解决方案

类似于Gson库中的TypeToken类,Guava库中的TypeToken类也允许我们在运行时捕获泛型类型【注:由于Java的类型擦除机制,这在Java中是不可能的,除非使用反射(/guava-reflection)】。

让我们使用Guava的TypeToken类实现一个示例:

<T> Type getTypeForListUsingTypeTokenFromGuava(Class<T> type) {
    return new com.google.common.reflect.TypeToken<List<T>>() {}
      .where(new TypeParameter<T>() {}, type)
      .getType();
}

where()方法通过将类型参数替换为变量type中的类返回一个TypeToken对象。

最后,让我们实际操作一下:

@Test
void givenJsonArray_whenListElementTypeDynamic_thenConvertToJavaListUsingTypeTokenFromGuava() {
    Gson gson = new Gson();
    List<Student> studentsLst = gson.fromJson(jsonArrayOfStudents, getTypeForListUsingTypeTokenFromGuava(Student.class));
    List<School> schoolLst = gson.fromJson(jsonArrayOfSchools, getTypeForListUsingTypeTokenFromGuava(School.class));
    assertAll(
      () -> studentsLst.forEach(e -> assertTrue(e instanceof Student)),
      () -> schoolLst.forEach(e -> assertTrue(e instanceof School))
    );
}

同样,我们使用fromJson()方法将JSON数组转换为相应的List对象。不过,我们借助Guava库在getTypeForListUsingTypeTokenFromGuava()方法中获得了Type对象。

6. 使用Java反射API的ParameterizedType解决方案

ParameterizedType是Java反射API的一部分,它用于表示像Collection<String>这样的参数化类型。

让我们实现ParamterizedType,以代表具有参数化的任何类:

public class ParameterizedTypeImpl implements ParameterizedType {
    private final Class<?> rawType;
    private final Type[] actualTypeArguments;

    private ParameterizedTypeImpl(Class<?> rawType, Type[] actualTypeArguments) {
        this.rawType = rawType;
        this.actualTypeArguments = actualTypeArguments;
    }

    public static ParameterizedType make(Class<?> rawType, Type ... actualTypeArguments) {
        return new ParameterizedTypeImpl(rawType, actualTypeArguments);
    }

    @Override
    public Type[] getActualTypeArguments() {
        return actualTypeArguments;
    }

    @Override
    public Type getRawType() {
        return rawType;
    }

    @Override
    public Type getOwnerType() {
        return null;
    }
}

变量actualTypeArguments存储类型参数的类信息,而rawType表示泛型类。make()方法返回参数化类的Type对象。

最后,让我们实际操作一下:

@Test
void givenJsonArray_whenListElementTypeDynamic_thenConvertToJavaListUsingParameterizedType() {
    Gson gson = new Gson();
    List<Student> studentsLst = gson.fromJson(jsonArrayOfStudents, ParameterizedTypeImpl.make(List.class, Student.class));
    List<School> schoolLst = gson.fromJson(jsonArrayOfSchools, ParameterizedTypeImpl.make(List.class, School.class));
    assertAll(
      () -> studentsLst.forEach(e -> assertTrue(e instanceof Student)),
      () -> schoolLst.forEach(e -> assertTrue(e instanceof School))
    );
}

我们使用make()方法获取参数化的List类的Type信息,并成功地将JSON数组转换为它们对应的List对象形式。

7. 总结

在这篇文章中,我们讨论了四种不同的方法,用于在运行时获取List<T>对象的Type信息。最后,我们利用Gson库的fromJson()方法,使用Type对象将JSON数组转换为List对象。

由于所有这些方法最终都调用了fromJson(),因此它们的性能相近。然而,TypeToken.getParameterized()方法最为简洁,我们建议使用它。

如往常一样,使用的代码可在GitHub上找到:https://github.com/eugenp/tutorials/tree/master/json-modules/gson-2