Android-Gson详细学习

2017/7/27 posted in  Android

前言

最近在项目中发现网络请求回来的Json数据都用Gson进行解析,以前没有对其进行一个系统的了解,所以这里做一个知识点的归纳整理。

Gson(又称Google Gson)是Google公司发布的一个开放源代码的Java库,主要用途为序列化Java对象为JSON字符串,或反序列化JSON字符串成Java对象。而JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成,广泛应用于各种数据的交互中,尤其是服务器与客户端的交互。

基本概念

  • Serialization:序列化,使Java对象到Json字符串的过程。
  • Deserialization:反序列化,字符串转换成Java对象。
  • JSON数据中的JsonElement有下面这四种类型: JsonPrimitive —— 例如一个字符串或整型JsonObject—— 一个以 JsonElement 名字(类型为 String)作为索引的集合。也就是说可以把 JsonObject 看作值为 JsonElement 的键值对集合。JsonArray—— JsonElement 的集合。注意数组的元素可以是四种类型中的任意一种,或者混合类型都支持。JsonNull—— 值为null

Gson解决的问题

  • 提供一种像toString()和构造方法的很简单的机制,来实现Java 对象和Json之间的互相转换。
  • 允许已经存在的无法改变的对象,转换成Json,或者Json转换成已存在的对象。
  • 允许自定义对象的表现形式
  • 支持任意的复杂对象
  • 能够生成可压缩和可读的Json的字符串输出。

Gson处理对象的几个重要点

  • 推荐把成员变量都声明称private
  • 没有必要用注解(@Expose 注解)指明某个字段是否会被序列化或者反序列化,所有包含在当前类(包括父类)中的字段都应该默认被序列化或者反序列化
  • 如果某个字段被 transient 这个Java关键词修饰,就不会被序列化或者反序列化
  • 下面的实现方式能够正确的处理null
    • 当序列化的时候,如果对象的某个字段为null,是不会输出到Json字符串中的。
    • 当反序列化的时候,某个字段在Json字符串中找不到对应的值,就会被赋值为null
  • 如果一个字段是 synthetic的,他会被忽视,也即是不应该被序列化或者反序列化
  • 内部类(或者anonymous class(匿名类),或者local class(局部类,可以理解为在方法内部声明的类))的某个字段和外部类的某个字段一样的话,就会被忽视,不会被序列化或者反序列化

Gson的基本用法

Gson提供了fromJson()toJson()两个直接用于解析和生成的方法,前者实现反序列化,后者实现了序列化。同时每个方法都提供了重载方法,我常用的总共有5个。

基本数据类型的解析

Gson gson = new Gson();
int i = gson.fromJson("100", int.class);              //100
double d = gson.fromJson("\"99.99\"", double.class);  //99.99
boolean b = gson.fromJson("true", boolean.class);     // true
String str = gson.fromJson("String", String.class);   // String

注:不知道你是否注意到了第2、3行有什么不一样没

基本数据类型的生成

Gson gson = new Gson();
String jsonNumber = gson.toJson(100);       // 100
String jsonBoolean = gson.toJson(false);    // false
String jsonString = gson.toJson("String"); //"String"

POJO类的生成与解析

public class User {
    //省略其它
    public String name;
    public int age;
    public String emailAddress;
}

生成JSON:

Gson gson = new Gson();
User user = new User("怪盗kidou",24);
String jsonObject = gson.toJson(user); // {"name":"怪盗kidou","age":24}

解析JSON:

Gson gson = new Gson();
String jsonString = "{\"name\":\"怪盗kidou\",\"age\":24}";
User user = gson.fromJson(jsonString, User.class);

Gson中使用泛型

上面了解的JSON中的Number、boolean、Object和String,现在说一下Array。
例:JSON字符串数组

["Android","Java","PHP"]

当我们要通过Gson解析这个json时,一般有两种方式:使用数组,使用List。而List对于增删都是比较方便的,所以实际使用是还是List比较多。

数组比较简单

Gson gson = new Gson();
String jsonArray = "[\"Android\",\"Java\",\"PHP\"]";
String[] strings = gson.fromJson(jsonArray, String[].class);

但对于List将上面的代码中的 String[].class 直接改为 List<String>.class 是行不通的。对于Java来说List<String>List<User> 这俩个的字节码文件只一个那就是List.class,这是Java泛型使用时要注意的问题 泛型擦除。

为了解决的上面的问题,Gson为我们提供了TypeToken来实现对泛型的支持,所以当我们希望使用将以上的数据解析List<String>时需要这样写。

Gson gson = new Gson();
String jsonArray = "[\"Android\",\"Java\",\"PHP\"]";
String[] strings = gson.fromJson(jsonArray, String[].class);
List<String> stringList = gson.fromJson(jsonArray, new TypeToken<List<String>>() {}.getType());

注:TypeToken的构造方法是protected修饰的,所以上面才会写成new TypeToken<List<String>>() {}.getType() 而不是 new TypeToken<List<String>>().getType()泛型解析对接口POJO的设计影响泛型的引入可以减少无关的代码,如我现在所在公司接口返回的数据分为两类:

{"code":"0","message":"success","data":{}}
{"code":"0","message":"success","data":[]}

我们真正需要的data所包含的数据,而code只使用一次,message则几乎不用。如果Gson不支持泛型或不知道Gson支持泛型的同学一定会这么定义POJO。

public class UserResponse {
    public int code;
    public String message;
    public User data;
}

当其它接口的时候又重新定义一个XXResponsedata的类型改成XX,很明显code,和message被重复定义了多次,通过泛型的话我们可以将codemessage字段抽取到一个Result的类中,这样我们只需要编写data字段所对应的POJO即可,更专注于我们的业务逻辑。如:

public class Result<T> {
    public int code;
    public String message;
    public T data;
}

那么对于data字段是User时则可以写为 Result<User> ,当是个列表的时候为 Result<List<User>>,其它同理。

手动方式

手动的方式就是使用stream包下的JsonReader类来手动实现反序列化,和Android中使用pull解析XML是比较类似的。

String json = "{\"name\":\"怪盗kidou\",\"age\":\"24\"}";
User user = new User();
JsonReader reader = new JsonReader(new StringReader(json));
reader.beginObject(); // throws IOException
while (reader.hasNext()) {
    String s = reader.nextName();
    switch (s) {
        case "name":
            user.name = reader.nextString();
            break;
        case "age":
            user.age = reader.nextInt(); //自动转换
            break;
        case "email":
            user.email = reader.nextString();
            break;
    }
}
reader.endObject(); // throws IOException
System.out.println(user.name);  // 怪盗kidou
System.out.println(user.age);   // 24
System.out.println(user.email); // ikidou@example.com

其实自动方式最终都是通过JsonReader来实现的,如果第一个参数是String类型,那么Gson会创建一个StringReader转换成流操作。

2017101215077695811912.png
2017101215077695811912.png

Gson的流式序列化

自动方式

20171012150776961842252.png
20171012150776961842252.png

所以啊,学会利用IDE的自动完成是多么重要这下知道了吧!
可以看出用红框选中的部分就是我们要找的东西。

提示:PrintStream(System.out) 、StringBuilderStringBuffer*Writer都实现了Appendable接口。

Gson gson = new Gson();
User user = new User("怪盗kidou",24,"ikidou@example.com");
gson.toJson(user,System.out); // 写到控制台

手动方式

JsonWriter writer = new JsonWriter(new OutputStreamWriter(System.out));
writer.beginObject() // throws IOException
        .name("name").value("怪盗kidou")
        .name("age").value(24)
        .name("email").nullValue() //演示null
        .endObject(); // throws IOException
writer.flush(); // throws IOException
//{"name":"怪盗kidou","age":24,"email":null}

提示:除了beginObjectendObject还有beginArrayendArray,两者可以相互嵌套,注意配对即可。beginArray后不可以调用name方法,同样beginObject后在调用value之前必须要调用name方法。

使用GsonBuilder导出null值、格式化输出、日期时间

一般情况下Gson类提供的 API已经能满足大部分的使用场景,但我们需要更多更特殊、更强大的功能时,这时候就引入一个新的类 GsonBuilder
GsonBuilder从名上也能知道是用于构建Gson实例的一个类,要想改变Gson默认的设置必须使用该类配置Gson。

GsonBuilder用法

Gson gson = new GsonBuilder()
               //各种配置
               .create(); //生成配置好的Gson

Gson在默认情况下是不动导出值null的键的,如:

public class User {
    //省略其它
    public String name;
    public int age;
    public String email;
}
Gson gson = new Gson();
User user = new User("怪盗kidou",24);
System.out.println(gson.toJson(user)); //{"name":"怪盗kidou","age":24}

可以看出,email字段是没有在json中出现的,当我们在调试是、需要导出完整的json串时或API接中要求没有值必须用Null时,就会比较有用。

使用方法:

Gson gson = new GsonBuilder()
        .serializeNulls()
        .create();
User user = new User("怪盗kidou", 24);
System.out.println(gson.toJson(user)); //{"name":"怪盗kidou","age":24,"email":null}

格式化输出、日期时间及其它:

这些都比较简单就不一一分开写了。

Gson gson = new GsonBuilder()
        //序列化null
        .serializeNulls()
        // 设置日期时间格式,另有2个重载方法
        // 在序列化和反序化时均生效
        .setDateFormat("yyyy-MM-dd")
        // 禁此序列化内部类
        .disableInnerClassSerialization()
        //生成不可执行的Json(多了 )]}' 这4个字符)
        .generateNonExecutableJson()
        //禁止转义html标签
        .disableHtmlEscaping()
        //格式化输出
        .setPrettyPrinting()
        .create();

注意:内部类(Inner Class)和嵌套类(Nested Class)的区别

Gson中的一些注解

@SerializedName注解

该注解能指定该字段在JSON中对应的字段名称

public class Box {

  @SerializedName("w")
  private int width;

  @SerializedName("h")
  private int height;

  @SerializedName("d")
  private int depth;

  // Methods removed for brevity
}

也就是说{"w":10,"h":20,"d":30} 这个JSON 字符串能够被解析到上面的width,height和depth字段中。

@Expose注解

该注解能够指定该字段是否能够序列化或者反序列化,默认的是都支持(true)。简单说来就是需要导出的字段上加上@Expose 注解,不导出的字段不加。注意是不导出的不加。

public class Account {

  @Expose(deserialize = false)
  private String accountNumber;

  @Expose
  private String iban;

  @Expose(serialize = false)
  private String owner;

  @Expose(serialize = false, deserialize = false)
  private String address;

  private String pin;
}

该注解在使用new Gson() 时是不会发生作用。毕竟最常用的API要最简单,所以该注解必须和GsonBuilder配合使用。需要注意的通过 builder.excludeFieldsWithoutExposeAnnotation()方法使该注解生效。

final GsonBuilder builder = new GsonBuilder();
builder.excludeFieldsWithoutExposeAnnotation();
final Gson gson = builder.create();

@Since和@Until注解

Since代表“自从”,Until 代表”一直到”。它们都是针对该字段生效的版本。比如说@Since(1.2)代表从版本1.2之后才生效,@Until(0.9)代表着在0.9版本之前都是生效的。

public class SoccerPlayer {

  private String name;

  @Since(1.2)
  private int shirtNumber;

  @Until(0.9)
  private String country;

  private String teamName;

  // Methods removed for brevity
}

也就是说我们利用方法builder.setVersion(1.0)定义版本1.0,如下:

final GsonBuilder builder = new GsonBuilder();
    builder.setVersion(1.0);

    final Gson gson = builder.create();

    final SoccerPlayer account = new SoccerPlayer();
    account.setName("Albert Attard");
    account.setShirtNumber(10); // Since version 1.2
    account.setTeamName("Zejtun Corinthians");
    account.setCountry("Malta"); // Until version 0.9

    final String json = gson.toJson(account);
    System.out.printf("Serialised (version 1.0)%n  %s%n", json);

由于shirtNumbercountry作用版本分别是1.2之后,和0.9之前,所以在这里都不会得到序列化,所以输出结果是:

Serialised (version 1.0)
  {"name":"Albert Attard","teamName":"Zejtun Corinthians"}

基于访问修饰符

什么是修饰符? publicstaticfinalprivateprotected 这些就是,所以这种方式也是比较特殊的。使用方式:

class ModifierSample {
    final String finalField = "final";
    static String staticField = "static";
    public String publicField = "public";
    protected String protectedField = "protected";
    String defaultField = "default";
    private String privateField = "private";
}

使用GsonBuilder.excludeFieldsWithModifiers构建gson,支持int形的可变参数,值由java.lang.reflect.Modifier提供,下面的程序排除了privateFieldfinalFieldstaticField 三个字段。

ModifierSample modifierSample = new ModifierSample();
Gson gson = new GsonBuilder()
        .excludeFieldsWithModifiers(Modifier.FINAL, Modifier.STATIC, Modifier.PRIVATE)
        .create();
System.out.println(gson.toJson(modifierSample));
// 结果:{"publicField":"public","protectedField":"protected","defaultField":"default"}

到此为止,Gson提供的所有注解就还有一个@JsonAdapter没有介绍了,而@JsonAdapter将和TypeAdapter将作为该系列第4篇也是最后一篇文章的主要内容。

基于策略(自定义规则)

上面介绍的了3种排除字段的方法,说实话我除了@Expose以外,其它的都是只在Demo用上过,用得最多的就是马上要介绍的自定义规则,好处是功能强大、灵活,缺点是相比其它3种方法稍麻烦一点,但也仅仅只是想对其它3种稍麻烦一点而已。

基于策略是利用Gson提供的ExclusionStrategy接口,同样需要使用GsonBuilder,相关API 2个,分别是addSerializationExclusionStrategyaddDeserializationExclusionStrategy 分别针对序列化和反序化时。这里以序列化为例。

例如:

Gson gson = new GsonBuilder()
        .addSerializationExclusionStrategy(new ExclusionStrategy() {
            @Override
            public boolean shouldSkipField(FieldAttributes f) {
                // 这里作判断,决定要不要排除该字段,return true为排除
                if ("finalField".equals(f.getName())) return true; //按字段名排除
                Expose expose = f.getAnnotation(Expose.class); 
                if (expose != null && expose.deserialize() == false) return true; //按注解排除
                return false;
            }
            @Override
            public boolean shouldSkipClass(Class<?> clazz) {
                // 直接排除某个类 ,return true为排除
                return (clazz == int.class || clazz == Integer.class);
            }
        })
        .create();

POJO与JSON的字段映射规则

还是之前User的例子,已经去除所有注解:

User user = new User("怪盗kidou", 24);
user.emailAddress = "ikidou@example.com";

GsonBuilder提供了FieldNamingStrategy接口和setFieldNamingPolicysetFieldNamingStrategy 两个方法。

默认实现
GsonBuilder.setFieldNamingPolicy 方法与Gson提供的另一个枚举类FieldNamingPolicy配合使用,该枚举类提供了5种实现方式分别为:

FieldNamingPolicy 结果(仅输出emailAddress字段)
IDENTITY {"emailAddress":"ikidou@example.com"}
LOWER_CASE_WITH_DASHES {"email-address":"ikidou@example.com"}
LOWER_CASE_WITH_UNDERSCORES {"email_address":"ikidou@example.com"}
UPPER_CAMEL_CASE {"EmailAddress":"ikidou@example.com"}
UPPER_CAMEL_CASE_WITH_SPACES {"Email Address":"ikidou@example.com"}

自定义实现
GsonBuilder.setFieldNamingStrategy 方法需要与Gson提供的FieldNamingStrategy接口配合使用,用于实现将POJO的字段与JSON的字段相对应。上面的FieldNamingPolicy实际上也实现了FieldNamingStrategy接口,也就是说FieldNamingPolicy也可以使用setFieldNamingStrategy方法。

用法:

Gson gson = new GsonBuilder()
        .setFieldNamingStrategy(new FieldNamingStrategy() {
            @Override
            public String translateName(Field f) {
                //实现自己的规则
                return null;
            }
        })
        .create();

注意: @SerializedName注解拥有最高优先级,在加有@SerializedName注解的字段上FieldNamingStrategy不生效!

Gson 序列化

英文Serialize和format都对应序列化,这是一个Java对象到JSON字符串的过程。接着看一个例子,下面分别是java类和以及我们期望的JSON数据:

public class Book {
  private String[] authors;
  private String isbn10;
  private String isbn13;
  private String title;
  //为了代码简洁,这里移除getter和setter方法等
}
{
  "title": "Java Puzzlers: Traps, Pitfalls, and Corner Cases",
  "isbn-10": "032133678X",
  "isbn-13": "978-0321336781",
  "authors": [
    "Joshua Bloch",
    "Neal Gafter"
  ]
}

你肯定能发现JSON数据中出现了isbn-10isbn-13, 我们怎么把字段数据isbn10isbn13转化为JSON数据需要的isbn-10isbn-13,Gson当然为我们提供了对应的解决方案

序列化方案1

采用上面提到的@SerializedName注解。

public class Book {
  private String[] authors;

  @SerializedName("isbn-10")
  private String isbn10;

  @SerializedName("isbn-13")
  private String isbn13;
  private String title;
  //为了代码简洁,这里移除getter和setter方法等
}

序列化方案2

利用JsonSerializer

public class BookSerialiser implements JsonSerializer {
    @Override
    public JsonElement serialize(final Book book, final Type typeOfSrc, final JsonSerializationContext context) {

        final JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("title", book.getTitle());
        jsonObject.addProperty("isbn-10", book.getIsbn10());
        jsonObject.addProperty("isbn-13", book.getIsbn13());

        final JsonArray jsonAuthorsArray = new JsonArray();
        for (final String author : book.getAuthors()) {
            final JsonPrimitive jsonAuthor = new JsonPrimitive(author);
            jsonAuthorsArray.add(jsonAuthor);
        }
        jsonObject.add("authors", jsonAuthorsArray);

        return jsonObject;
    }
}

下面对序列化过程进行大致的分析:

  • JsonSerializer是一个接口,我们需要提供自己的实现,来满足自己的序列化要求。
public interface JsonSerializer<T> {

/**
 *Gson 会在解析指定类型T数据的时候触发当前回调方法进行序列化
 *
 * @param T 需要转化为Json数据的类型,对应上面的Book
 * @return 返回T指定的类对应JsonElement
 */
public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context);
}
  • 首先在上面的代码中,我们需要创建的是一个JsonElement对象,这里对应Book是一个对象,所以创建一个JsonObject类型。
    final JsonObject jsonObject = new JsonObject();
  • 然后我们将相应字段里面的数据填充到jsonObject里面。
    java jsonObject.addProperty...
    jsonObject.add...
  • 下面是jsonObject中的添加方法:jsonObj.add(String property,JsonElement value)
  • 所以最后返回的还是一个JsonElement 类型,这里对应的是jsonObject。完成了javaBean->JSON数据的转化。
  • 同样需要配置,
// Configure GSON
  final GsonBuilder gsonBuilder = new GsonBuilder();
  gsonBuilder.registerTypeAdapter(Book.class, new BookSerialiser());
  gsonBuilder.setPrettyPrinting();
  final Gson gson = gsonBuilder.create();

  final Book javaPuzzlers = new Book();
  javaPuzzlers.setTitle("Java Puzzlers: Traps, Pitfalls, and Corner Cases");
  javaPuzzlers.setIsbn10("032133678X");
  javaPuzzlers.setIsbn13("978-0321336781");
  javaPuzzlers.setAuthors(new String[] { "Joshua Bloch", "Neal Gafter" });

  // Format to JSON
  final String json = gson.toJson(javaPuzzlers);
  System.out.println(json);

,这里对应的是gsonBuilder.registerTypeAdapter(Book.class, new BookSerialiser())方法进行JsonSerializer的配置。在上面例子中,通过调用gsonBuilder.setPrettyPrinting();方法还告诉了 Gson 对生成的 JSON 对象进行格式化

Gson 反序列化

英文parsedeserialise对应反序列化,这是一个字符串转换成Java对象的过程。我们同样采用上面一小节的代码片段,只不过现在我们需要做的是将:

{
  "title": "Java Puzzlers: Traps, Pitfalls, and Corner Cases",
  "isbn-10": "032133678X",
  "isbn-13": "978-0321336781",
  "authors": [
    "Joshua Bloch",
    "Neal Gafter"
  ]
}

转化为对应的Book实体类。

反序列化方案1

利用@SerializedName 注解也就是说我们的实体类Book.java可以这么写:

public class Book {
  private String[] authors;

  @SerializedName("isbn-10")
  private String isbn10;

  @SerializedName(value = "isbn-13", alternate = {"isbn13","isbn.13"})
  private String isbn13;
  private String title;
  //为了代码简洁,这里移除getter和setter方法等
}

可以看到这里我们在@SerializedName 注解使用了一个value, alternate字段,value也就是默认的字段,对序列化和反序列化都有效,alternate只有反序列化才有效果。也就是说一般服务器返回给我们JSON数据的时候可能同样的一个图片,表示"image","img","icon"等备选属性名,我们利用@SerializedName 中的alternate字段就能解决这个问题,全部转化为我们实体类中的图片字段。

反序列化方案2

我们在序列化的时候使用的是JsonSerialize ,这里对应使用JsonDeserializer我们将解析到的json数据传递给Book的setter方法即可。

public class BookDeserializer implements JsonDeserializer<Book> {

  @Override
  public Book deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
      throws JsonParseException {
    final JsonObject jsonObject = json.getAsJsonObject();

    final JsonElement jsonTitle = jsonObject.get("title");
    final String title = jsonTitle.getAsString();

    final String isbn10 = jsonObject.get("isbn-10").getAsString();
    final String isbn13 = jsonObject.get("isbn-13").getAsString();

    final JsonArray jsonAuthorsArray = jsonObject.get("authors").getAsJsonArray();
    final String[] authors = new String[jsonAuthorsArray.size()];
    for (int i = 0; i < authors.length; i++) {
      final JsonElement jsonAuthor = jsonAuthorsArray.get(i);
      authors[i] = jsonAuthor.getAsString();
    }

    final Book book = new Book();
    book.setTitle(title);
    book.setIsbn10(isbn10);
    book.setIsbn13(isbn13);
    book.setAuthors(authors);
    return book;
  }
}

和Gson序列化章节一样,我们这里接着分析我们是怎么将JSON数据解析(反序列化)为实体类的:

  • 因为我们可以发现上面的JSON数据是一个{}大括号包围的,也就意味着这是一个Json对象。所以首先我们通过final JsonObject jsonObject = json.getAsJsonObject();将我们的JsonElement转化为JsonObject
  • 通过jsonObject.get("xxx").getAsString()的形式获取相应String的值
  • 通过jsonObject.get("xx").getAsJsonArray();获取相应的json数组,并遍历出其中的相应字段值
  • 通过setter方法,将获取到的值设置给Book类。
  • 最终返回的是 Book的对象实例。完成了JSON->javaBean的转化
  • 同样需要配置
  • 关于从本地流中读取Json数据可以使用 InputStreamReader完成
// Configure Gson
  GsonBuilder gsonBuilder = new GsonBuilder();
  gsonBuilder.registerTypeAdapter(Book.class, new BookDeserializer());
  Gson gson = gsonBuilder.create();

  // The JSON data
  try(Reader reader = new InputStreamReader(Main.class.getResourceAsStream("/part1/sample.json"), "UTF-8")){

    // Parse JSON to Java
    Book book = gson.fromJson(reader, Book.class);
    System.out.println(book);
  }

TypeAdapter

TypeAdapter介绍

之前在上一篇文中提到的JsonSerializerJsonDeserializer解析的时候都利用到了一个中间件-JsonElement,比如下方的序列化过程。可以看到我们在把Java对象转化为JSON字符串的时候都会用到这个中间件JsonElement

20171012150776994644553.png
20171012150776994644553.png

TypeAdapter的使用正是去掉了这个中间层,直接用流来解析数据,极大程度上提高了解析效率。

New applications should prefer TypeAdapter, whose streaming API is more efficient than this interface’s tree API.应用中应当尽量使用TypeAdapter,它流式的API相比于之前的树形解析API将会更加高效。
TypeAdapter作为一个抽象类提供两个抽象方法。分别是write()read()方法,也对应着序列化和反序列化,其它的方法都是final方法并最终调用这两个抽象方法。

如下图所示:

20171012150776998438554.png
20171012150776998438554.png

下面就让我们来一起使用和了解TypeAdapter吧。

TypeAdapter实例

为了便于理解,这里还是统 一 一 下,采用和上面一篇文章同样的例子。

Book.java实体类:

package com.javacreed.examples.gson.part1;

public class Book {

  private String[] authors;
  private String isbn;
  private String title;

//为了代码简洁,这里移除getter和setter方法等
}

直接贴代码,具体序列化和反序列化的TypeAdapter类,这里是BookTypeAdapter.java

package com.javacreed.examples.gson.part1;
import java.io.IOException;
import org.apache.commons.lang3.StringUtils;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

public class BookTypeAdapter extends TypeAdapter {

  @Override
  public Book read(final JsonReader in) throws IOException {
    final Book book = new Book();

    in.beginObject();
    while (in.hasNext()) {
      switch (in.nextName()) {
      case "isbn":
        book.setIsbn(in.nextString());
        break;
      case "title":
        book.setTitle(in.nextString());
        break;
      case "authors":
        book.setAuthors(in.nextString().split(";"));
        break;
      }
    }
    in.endObject();

    return book;
  }

  @Override
  public void write(final JsonWriter out, final Book book) throws IOException {
    out.beginObject();
    out.name("isbn").value(book.getIsbn());
    out.name("title").value(book.getTitle());
    out.name("authors").value(StringUtils.join(book.getAuthors(), ";"));
    out.endObject();
  }
}

同样这里设置TypeAdapter之后还是需要配置(注册),可以注意到的是gsonBuilder.registerTypeAdapter(xxx)方法进行注册在我们之前的JsonSerializerJsonDeserializer中也有使用:

final GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(Book.class, new BookTypeAdapter());
    final Gson gson = gsonBuilder.create();

下面对两个write方法和read方法进行分别的阐述:

TypeAdapter中的write方法

write()方法中会传入JsonWriter,和需要被序列化的Book对象的实例,采用和PrintStream类似的方式 写入到JsonWriter中。

  @Override
  public void write(final JsonWriter out, final Book book) throws IOException {
    out.beginObject();
    out.name("isbn").value(book.getIsbn());
    out.name("title").value(book.getTitle());
    out.name("authors").value(StringUtils.join(book.getAuthors(), ";"));
    out.endObject();
  }

下面是上面代码的步骤:

  • out.beginObject()产生{,如果我们希望产生的是一个数组对象,对应的使用beginArray()
  • out.name("isbn").value(book.getIsbn()); out.name("title").value(book.getTitle());分别获取book中的isbn和title字段并且设置给Json对象中的isbn和title。也就是说上面这段代码,会在json对象中产生:"isbn": "978-0321336781","title": "Java Puzzlers: Traps, Pitfalls, and Corner Cases",
  • out.name("authors").value(StringUtils.join(book.getAuthors(), ";"));则会对应着:"authors": "Joshua Bloch;Neal Gafter"
  • 同理 out.endObject()则对应着}
  • 那么整个上面的代码也就会产生JSON对象:{"isbn": "978-0321336781","title": "Java Puzzlers: Traps, Pitfalls, and Corner Cases","authors": "Joshua Bloch;Neal Gafter"}

这里需要注意的是,如果没有调用 out.endObject()产生},那么你的项目会报出

JsonSyntaxException`错误`Exception in thread "main" com.google.gson.JsonSyntaxException: java.io.EOFException: End of input at line 4 column 40  at com.google.gson.Gson.fromJson(Gson.java:813)  at com.google.gson.Gson.fromJson(Gson.java:768)  at com.google.gson.Gson.fromJson(Gson.java:717)  at com.google.gson.Gson.fromJson(Gson.java:689)  at com.javacreed.examples.gson.part1.Main.main(Main.java:41)Caused by: java.io.EOFException: End of input at line 4 column 40  at com.google.gson.stream.JsonReader.nextNonWhitespace(JsonReader.java:1377)  at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:471)  at com.google.gson.stream.JsonReader.hasNext(JsonReader.java:403)  at com.javacreed.examples.gson.part1.BookTypeAdapter.read(BookTypeAdapter.java:33)  at com.javacreed.examples.gson.part1.BookTypeAdapter.read(BookTypeAdapter.java:1)  at com.google.gson.Gson.fromJson(Gson.java:803)  ... 4 more

TypeAdapter中的read方法

read()方法将会传入一个JsonReader对象实例并返回反序列化的对象。

  @Override
  public Book read(final JsonReader in) throws IOException {
    final Book book = new Book();

    in.beginObject();
    while (in.hasNext()) {
      switch (in.nextName()) {
      case "isbn":
        book.setIsbn(in.nextString());
        break;
      case "title":
        book.setTitle(in.nextString());
        break;
      case "authors":
        book.setAuthors(in.nextString().split(";"));
        break;
      }
    }
    in.endObject();

    return book;
  }

下面是这段代码的步骤:
同样是通过in.beginObject();in.endObject();对应解析{,}
通过while (in.hasNext()) {switch (in.nextName()) {}}来完成每个JsonElement的遍历,并且通过switch...case的方法获取Json对象中的键值对。并通过我们Book实体类Setter方法进行设置。

while (in.hasNext()) {    
    switch (in.nextName()) {    
        case "isbn":      
            book.setIsbn(in.nextString());      
            break;    
        case "title":      
            book.setTitle(in.nextString());      
            break;    
        case "authors":      
            book.setAuthors(in.nextString().split(";"));      
            break;    
    }  
}

同样需要注意的是,如果没有执行in.endObject(),将会出现JsonIOException的错误:

Exception in thread "main" com.google.gson.JsonIOException: JSON document was not fully consumed.  at   com.google.gson.Gson.assertFullConsumption(Gson.java:776)  at com.google.gson.Gson.fromJson(Gson.java:769)  at com.google.gson.Gson.fromJson(Gson.java:717)  at com.google.gson.Gson.fromJson(Gson.java:689)  at com.javacreed.examples.gson.part1.Main.main(Main.java:41)

下面给出使用TypeAdapter的完整代码:

package com.javacreed.examples.gson.part1;

import java.io.IOException;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class Main {
  public static void main(final String[] args) throws IOException {
    final GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(Book.class, new BookTypeAdapter());
    gsonBuilder.setPrettyPrinting();

    final Gson gson = gsonBuilder.create();

    final Book book = new Book();
    book.setAuthors(new String[] { "Joshua Bloch", "Neal Gafter" });
    book.setTitle("Java Puzzlers: Traps, Pitfalls, and Corner Cases");
    book.setIsbn("978-0321336781");

    final String json = gson.toJson(book);
    System.out.println("Serialised");
    System.out.println(json);

    final Book parsedBook = gson.fromJson(json, Book.class);
    System.out.println("\nDeserialised");
    System.out.println(parsedBook);
  }
}

对应的编译结果为:

Serialised
{
  "isbn": "978-0321336781",
  "title": "Java Puzzlers: Traps, Pitfalls, and Corner Cases",
  "authors": "Joshua Bloch;Neal Gafter"
}

Deserialised
Java Puzzlers: Traps, Pitfalls, and Corner Cases [978-0321336781]
Written by:
  >> Joshua Bloch
  >> Neal Gafter

TypeAdapter处理简洁的JSON数据

为了简化JSON数据,其实我们上面的JSON数据可以这么写:

["978-0321336781","Java Puzzlers: Traps, Pitfalls, and Corner Cases","Joshua Bloch","Neal Gafter"]

可以看到的是,这样采用的直接是值的形式。当然这样操作简化了JSON数据但是可能就让整个数据的稳定性下降了许多的,你需要按照一定的顺序来解析这个数据。对应的writeread方法如下:

  @Override
  public void write(final JsonWriter out, final Book book) throws IOException {
    out.beginArray();
    out.value(book.getIsbn());
    out.value(book.getTitle());
    for (final String author : book.getAuthors()) {
      out.value(author);
    }
    out.endArray();
  }
  @Override
  public Book read(final JsonReader in) throws IOException {
    final Book book = new Book();

    in.beginArray();
    book.setIsbn(in.nextString());
    book.setTitle(in.nextString());
    final List authors = new ArrayList<>();
    while (in.hasNext()) {
      authors.add(in.nextString());
    }
    book.setAuthors(authors.toArray(new String[authors.size()]));
    in.endArray();

    return book;
  }

这里的解析原理和上面一致,不再赘述。

TypeAdapter解析内置对象

(这里将nested objects翻译为内置对象,其实就是在Book类)
这里对上面的Book实体类进行修改如下,添加Author作者类,每本书可以有多个作者。

package com.javacreed.examples.gson.part3;

public class Book {

  private Author[] authors;
  private String isbn;
  private String title;

class Author {

  private int id;
  private String name;

//为了代码简洁,这里移除getter和setter方法等
}
//为了代码简洁,这里移除getter和setter方法等
}

这里提供JSON对象,

{
  "isbn": "978-0321336781",
  "title": "Java Puzzlers: Traps, Pitfalls, and Corner Cases",
  "authors": [
    {
      "id": 1,
      "name": "Joshua Bloch"
    },
    {
      "id": 2,
      "name": "Neal Gafter"
    }
  ]
}

下面分别展示write和read方法:

  @Override
  public void write(final JsonWriter out, final Book book) throws IOException {
    out.beginObject();
    out.name("isbn").value(book.getIsbn());
    out.name("title").value(book.getTitle());
    out.name("authors").beginArray();
    for (final Author author : book.getAuthors()) {
      out.beginObject();
      out.name("id").value(author.getId());
      out.name("name").value(author.getName());
      out.endObject();
    }
    out.endArray();
    out.endObject();
  }
 @Override
  public Book read(final JsonReader in) throws IOException {
    final Book book = new Book();

    in.beginObject();
    while (in.hasNext()) {
      switch (in.nextName()) {
      case "isbn":
        book.setIsbn(in.nextString());
        break;
      case "title":
        book.setTitle(in.nextString());
        break;
      case "authors":
        in.beginArray();
        final List authors = new ArrayList<>();
        while (in.hasNext()) {
          in.beginObject();
          final Author author = new Author();
          while (in.hasNext()) {
            switch (in.nextName()) {
            case "id":
              author.setId(in.nextInt());
              break;
            case "name":
              author.setName(in.nextString());
              break;
            }
          }
          authors.add(author);
          in.endObject();
        }
        book.setAuthors(authors.toArray(new Author[authors.size()]));
        in.endArray();
        break;
      }
    }
    in.endObject();

    return book;
  }

Gson性能分析

首先来看看我们提供一个大一点的数据来论证下面一些方法的优缺点。 这里提供类LargeData.java,并分为四个部分进行内存消耗的分析:

public class LargeData {

  private long[] numbers;

  public void create(final int length) {
    numbers = new long[length];
    for (int i = 0; i < length; i++) {
      numbers[i] = i;
    }
  }

  public long[] getNumbers() {
    return numbers;
  }

}

第1部分 JsonSerializer的直接使用

看看下面的JsonSerializer:

package com.javacreed.examples.gson.part1;

import java.lang.reflect.Type;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

public class LargeDataSerialiser implements JsonSerializer<LargeData> {

  @Override
  public JsonElement serialize(final LargeData data, final Type typeOfSrc, final JsonSerializationContext context) {
    final JsonArray jsonNumbers = new JsonArray();
    for (final long number : data.getNumbers()) {
      jsonNumbers.add(new JsonPrimitive(number));
    }

    final JsonObject jsonObject = new JsonObject();
    jsonObject.add("numbers", jsonNumbers);
    return jsonObject;
  }
}

上面的代码实现了从java对象>转化>JSON数组的序列化过程。下面的代码实现了配置和初始化的过程,被写入文件。这里可以看到的是对LargeData初始化了10485760个元素:

package com.javacreed.examples.gson.part1;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class Main {
  public static void main(final String[] args) throws IOException {
    // Configure GSON
    final GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(LargeData.class, new LargeDataSerialiser());
    gsonBuilder.setPrettyPrinting();

    final Gson gson = gsonBuilder.create();

    final LargeData data = new LargeData();
    data.create(10485760);

    final String json = gson.toJson(data);

    final File dir = new File("target/part1");
    dir.mkdirs();

    try (PrintStream out = new PrintStream(new File(dir, "output.json"), "UTF-8")) {
      out.println(json);
    }

    System.out.println("Done");
  }
}

这个例子实现了创建java对象并且转化为JSON字符串并写入文件的整个过程。下面的图标展示了内存的消耗情况:

20171012150777023393682.png
20171012150777023393682.png

上面的的LargeData在这里会消耗89MB的内存,从java对象转化为JSON字符串的过程将会消耗大概16s的时间并且需要超过1GB的内存。也就是说,序列化1MB的数据我们需要大约11MB的工作空间。1:11的确实是一个不小的比列。下面的 图片会展示整个过程的几个阶段。

20171012150777025353382.png
20171012150777025353382.png

可以看到的是,这里有四个方块分别代表不同的阶段,(但是IO 缓冲区并没有在这里得到使用,所以以灰色进行标注。)整个过程从java对象(蓝色方块),然后由LargeDataSerialiser类创建的JSONElement对象(红色方块),然后这些临时的对象又被转化为JSON 字符串(绿色方块),上面的示例代码使用PrintStream将内容输出到文件中并没有使用任何缓冲区。
完成了第1部分的分析,接下来下面的分析流程是一样的:

第2 部分 TypeAdapter的直接使用

之前的系列文章中都对Gson基础的使用进行了很好的讲解,可以回顾一下。
TypeAdapter相比 于上面的方法,并没有使用JSONElement对象,而是直接将Java对象啊转化为了JSON对象。

package com.javacreed.examples.gson.part2;

import java.io.IOException;

import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

public class LargeDataTypeAdapter extends TypeAdapter<LargeData> {

  @Override
  public LargeData read(final JsonReader in) throws IOException {
    throw new UnsupportedOperationException("Coming soon");
  }

  @Override
  public void write(final JsonWriter out, final LargeData data) throws IOException {
    out.beginObject();
    out.name("numbers");
    out.beginArray();
    for (final long number : data.getNumbers()) {
      out.value(number);
    }
    out.endArray();
    out.endObject();
  }
}

同样会需要配置,这里主要使用的方法是gsonBuilder.registerTypeAdapter(LargeData.class, new LargeDataTypeAdapter());

package com.javacreed.examples.gson.part2;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class Main {
  public static void main(final String[] args) throws IOException {
    // Configure GSON
    final GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(LargeData.class, new LargeDataTypeAdapter());
    gsonBuilder.setPrettyPrinting();

    final Gson gson = gsonBuilder.create();

    final LargeData data = new LargeData();
    data.create(10485760);

    final String json = gson.toJson(data);

    final File dir = new File("target/part2");
    dir.mkdirs();

    try (PrintStream out = new PrintStream(new File(dir, "output.json"), "UTF-8")) {
      out.println(json);
    }

    System.out.println("Done");
  }
}

上面的代码完成的是从java对象 >转化>JSON 字符串并最终写入文件的过程。看看下面的性能分析图表:

20171012150777031491897.png
20171012150777031491897.png

和最初的那个方法一样,这里的LargeData对象将会需要89MB的内存,从java对象转化为JSON字符串的过程需要消耗4s的时间,大概650MB的内存。也就是说,序列化1MB的数据,大概需要7.5MB的内存空间。相比于之前的第一种JsonSerializer方法,这里减少了接近一半的内存消耗。同样的,来看看这个方法的几个过程:

20171012150777032621918.png
20171012150777032621918.png

这里的序列化过程主要有两个阶段,相比于之前的JSONSerializer的序列化过程,这里没有了转化为JSONElement的过程,也就完成了内存消耗的减少。

第3部分 TypeAdapter的流式处理

下面的代码,我们使用上面同样的TypeAdapter,只不过我们直接在main()方法中修改Gson的用法,以流的形式进行输出。

package com.javacreed.examples.gson.part3;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class Main {
  public static void main(final String[] args) throws IOException {
    // Configure GSON
    final GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(LargeData.class, new LargeDataTypeAdapter());
    gsonBuilder.setPrettyPrinting();

    final Gson gson = gsonBuilder.create();

    final LargeData data = new LargeData();
    data.create(10485760);

    final File dir = new File("target/part3");
    dir.mkdirs();

    try (BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(dir,
        "output.json")), "UTF-8"))) {
      gson.toJson(data, out);
    }

    System.out.println("Done");
  }
}

这个例子同样是将java对象转化为JSON字符串并且输出,也来看看下面的性能分析图表:

20171012150777036368764.png
20171012150777036368764.png

可以看到的是同样的最初产生的数据是89MB,序列化过程将java对象转化为JSON字符串花了大概三秒钟的时间,消耗大概160MB的内存。也就是说序列化1MB的数据我们需要大概2MB的内存空间。相比于之前的两种方法,有了很大的改进。

20171012150777037225976.png
20171012150777037225976.png

这个方法同样的是使用了两个阶段。不过在上面一个示例中的绿色方块部分在这里没有使用,这里直接完成了java对象到IO 缓冲区的转化并写入文件。
虽然这里并不是Gson的关系,但是我们使用Gson的方法极大的减少了内存消耗,所以说在使用开源库的时候,能够正确高效的使用API也显得尤为重要。

第4部分 JsonSerializer 的流式处理

同样的使用第一个例子中的JsonSerializer,这里的配置需要注意的是gsonBuilder.registerTypeAdapter(LargeData.class, new LargeDataSerialiser());

package com.javacreed.examples.gson.part4;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class Main {
  public static void main(final String[] args) throws IOException {
    // Configure GSON
    final GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(LargeData.class, new LargeDataSerialiser());
    gsonBuilder.setPrettyPrinting();

    final Gson gson = gsonBuilder.create();

    final LargeData data = new LargeData();
    data.create(10485760);

    final File dir = new File("target/part4");
    dir.mkdirs();

    try (BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(dir,
        "output.json")), "UTF-8"))) {
      gson.toJson(data, out);
    }

    System.out.println("Done");
  }
}

经过前面的分析,我们这里也可以这道这里主要分为三个阶段,下面提供性能分析图和JSONSerializer的阶段流程图:

20171012150777041919731.png
20171012150777041919731.png

20171012150777042722167.png
20171012150777042722167.png

这里可以看到三个阶段完成的工作消耗了11s的时间,730MB的内存空间。也就是说1:8的比例。可以相比上面的例子,知道这里使用JSONSerializer产生了JSONElement对象消耗了很多的内存。

结论

在上面的分析过程中,我们采用了GSON的两种不同的方然完成了序列化一个大数据的过程,并且比较了不同的方法之间的差异。上面的第三种方法(TypeAdapter的流式处理)被论证为最合适的,消耗最少内存的一种方法。

Gson主要分成两部分,一个就是数据拆解,一个是数据封装。

参考

你真的会用Gson吗?Gson使用指南(一)