【Java】关于序列化的事和Gson工具以及泛型

it2024-07-20  40

文章目录

序列化什么是序列化序列化的必要性及应用Java 中序列化ID的作用关键字transient JSONJSON概述JSON与XML比较JSON语法规则 GsonGson概念及基本用法由Gson产生的工具ArgumentMaker 泛型泛型擦除泛型擦除的应用


序列化

序列化:将对象可以写入IO流中,以便于传输和存储 反序列化:从IO流中恢复成序列化之前的对象,以便从流中得到数据进行解析 意义:可以将Java对象转换为字节序列,就方便保存在磁盘或者网络传输了,也可以将已存在的字节序列恢复为原来的对象。所有在网络上传输的对象和在磁盘中保存的对象都需要序列化。

什么是序列化

  序列化是一种将对象以一连串的字节描述的过程;   Java 平台允许我们在内存中创建可复用的Java 对象,使用Java 对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。反序列化是一种将这些字节重建成一个对象的过程。

序列化的必要性及应用

对象的序列化主要有两种用途: (1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中; (2) 在网络上传送对象的字节序列。

  当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java 对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java 对象。

  Java 序列化机制就是为了解决这个问题而产生。

Java 中序列化ID的作用

  简单来说,Java 的序列化机制是通过在运行时判断类的serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的serialVersionUID 与本地相应实体(类)的serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。

关键字transient

  transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient修饰的变量的值被设为初始值,如int 型的是0,对象型的是null。

给如下例子可以清晰感受到序列化的意义。

用户类

public class UserInfo { private String id; private String password; private int age; private String name; public UserInfo() { } /*相关的setter和getter省略*/ public UserInfo(String id, String password, int age, String name) { this.id = id; this.password = password; this.age = age; this.name = name; } @Override public String toString() { return "UserInfo [id=" + id + ", password=" + password + ", age=" + age + ", name=" + name + "]"; } }

把类产生的对象写到文件里去。需要借助ObjectOutputStream这个类。

import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; public class SerialDemo { public static void main(String[] args) { String path = "./tag"; String fileName = "user.inf"; File fileDir = new File(path); if (!fileDir.exists()) { fileDir.mkdirs(); } File file = new File(fileDir, fileName); if (!file.exists()) { try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } try { UserInfo user = new UserInfo("123456", "qwer", 20, "张三"); FileOutputStream fos = new FileOutputStream(file); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(user); oos.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }

但是出现了异常。 说我们的UserInfo类没有序列化,同时可以说明ObjectOutputStream的writeObject方法可以让我们将对象向文件或者网络写。但是写的这个对象的类必须有序列号这个属性。

那我们就给相关类加上序列号。

public class UserInfo implements Serializable { private static final long serialVersionUID = -3696056911082690713L; /*下面内容省略*/ }

再去运行我们的SerialDemo,没有发生异常,去查看相关文件,用UltraEdit打开。 文件用138字节仅仅才描述了一个对象的信息。可以仔细观察文件详细内容,确实有id,password,age,name等信息。但最重要的是,用红框勾出来的,我们将刚才产生的序列号转化为16进制,发现在写入的文件中也找到了这串序列号,那么这个序列号到底用来干什么呢?下面继续做实验。我们从写好的文件读取并形成对象。

import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.ObjectInputStream; public class ReSerialDemo { public static void main(String[] args) { File file = new File("./tag/user.inf"); try { FileInputStream fis = new FileInputStream(file); ObjectInputStream ois = new ObjectInputStream(fis); UserInfo user = (UserInfo) ois.readObject(); System.out.println(user); ois.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } /*UserInfo [id=123456, password=qwer, age=20, name=张三]*/

确实准确的从文件中读取我们刚才序列化的对象。但是,如果将序列号一改呢?将对象写文件的时候是一个序列号,读取文件获得对象是另一个序列号会怎么样呢?继续实验。

public class UserInfo implements Serializable { private static final long serialVersionUID = -1L; }

再去运行ReSerialDemo,出现了异常,说流中的序列号和本地类的序列号不匹配。

java.io.InvalidClassException: com.mec.serial.test.UserInfo; local class incompatible: stream classdesc serialVersionUID = -3696056911082690713, local class serialVersionUID = -1 at java.base/java.io.ObjectStreamClass.initNonProxy(Unknown Source) at java.base/java.io.ObjectInputStream.readNonProxyDesc(Unknown Source) at java.base/java.io.ObjectInputStream.readClassDesc(Unknown Source) at java.base/java.io.ObjectInputStream.readOrdinaryObject(Unknown Source) at java.base/java.io.ObjectInputStream.readObject0(Unknown Source) at java.base/java.io.ObjectInputStream.readObject(Unknown Source) at com.mec.serial.test.ReSerialDemo.main(ReSerialDemo.java:17)

从这里我们分析可以得出序列号为了ObjectInputStream的readObject()来用的。就是读了这文件的这么多信息,这些信息如何转为一个userinfo对象,读入的这些数据都是二进制数据,这些数据需要转换回成对象。其中这个号就是用来唯一标识这个类的,区别于其他的类。

把对象转换为纯二进制,把纯二进制转换为对象。就需要一个序列号,这个序列号的目的就是为了准确定位一个类。

通过上述例子想讲明一个问题,当我们把一个对象向外写到流中时,必须要先建立序列号,而且写的内容包含序列号,这个序列号成为识别这段二进制的重要的信息。

在网络流中写对象。

Socket socket = new Socket(); ObjectOutputStream netOos = new ObjectOutputStream(socket.getOutputStream()); ObjectInputStream netOis = new ObjectInputStream(socket.getInputStream());

但是我们在网络上传输消息,发送一个对象需要发送刚才UE看到的那么多二进制消息,内部效率相较于下面的方式十分低。

尽量把对象转换成JSON保存更为稳妥。

JSON

JSON概述

JSON指的是JavaScript对象的表示法(JavaScript Object Notation)。是轻量级的文本数据交换格式。在网络传输中,客户端和服务器两个之间传输数据。具有自我描述性,更容易理解虽然使用JavaScript语法来描述数据对象,但是JSON任然独立于语言和平台。JSON解析器和JSON库支持许多不同的编程语言。

综上,JSON是独立于语言,可以作为存储数据和交换数据的格式。从这功能我们就不得不想到XML。

JSON与XML比较

作为配置文件:XML因为具有规范的标签,所以更能得知层次结构,JSON却不行。(XML优于JSON

作为数据交换和存储:同样描述一个对象,JSON比XML更小,更快,更容易解析,网络传输更省带宽。(JSON优于XML

JSON语法规则

数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组

JSON的值可以是数字(整数或者浮点数),字符串(双引号中),逻辑值(true/false),数组(方括号中),对象(花括号中),null。

例子 例子的视图

Gson

Gson概念及基本用法

  Gson是Google公司提供的用来在Java对象和JSON数据之间进行映射的Java类库。主要用途为序列化Java对象为JSON字符串,或反序列化JSON字符串成Java对象。

基本用法:提供了两个方法,toJson生成JSON字符串,fromJson解析JSON字符串生成对象。

例子:需要gson的jar包

import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.mec.serial.test.UserInfo; public class GsonDemo { public static void main(String[] args) { UserInfo user = new UserInfo("123456", "qwer", 20, "张三"); Gson gson = new GsonBuilder().create(); String userGsonStr = gson.toJson(user); System.out.println("生成的JSON字符串:" + userGsonStr); UserInfo newUser = gson.fromJson(userGsonStr, UserInfo.class); System.out.println("由Gson字符串解析得到的对象:" + newUser); } } /* 生成的JSON字符串:{"id":"123456","password":"qwer","age":20,"name":"张三"} 由Gson字符串解析得到的对象:UserInfo [id=123456, password=qwer, age=20, name=张三] */

我有个对象,想发送给网络远端,发送字符串,readUTF,writeUTF,我希望对端接收到这个字符串能很方便的转换回成原对象。Gson就起到了这个作用,避免我们来回解析了。不仅如此,任意的,复杂的类型都可以处理,这个工具很强悍。

由Gson产生的工具ArgumentMaker

我们知道方法的调用需要参数,假如方法的参数从远端传来,我们应该怎样将它管理和传输呢?例如userLogin(String id,String password)。

仔细观察方法,多仔细观察每种方法的共同点,处处都是映射/键值对。我们可以键是形参的名字,值是参数真正的值,构成键值对的一个map,再把map对象变成JSON字符串,就方便传输了!

import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; public class ArgumentMaker { private Map<String, String> argumentMap; //键是参数名字,值是参数值所形成的JSON对象! public static final Gson gson = new GsonBuilder().create(); private static final Type mapType = new TypeToken<Map<String, String>> () {}.getType(); //引入了泛型之后虽然要多写一句话用于获取泛型信息 TypeToken public ArgumentMaker() { argumentMap = new HashMap<>(); } public ArgumentMaker addArg(String name, Object value) { argumentMap.put(name, gson.toJson(value)); //第一次toJson,将参数的值转为JSON字符串放到map去 return this; } @Override public String toString() { return gson.toJson(argumentMap);//第二次toJson,将map转换为JSON字符串,方便传输 } /*上述只完成一半,对象->JSON字符串,接下里完成解析*/ /*字符串反向解析成Map,用到了Gson高级用法TypeToken*/ public ArgumentMaker(String parameter) { this.argumentMap = gson.fromJson(parameter, mapType); } /*Type可以处理泛型,避免泛型擦除问题*/ public Object getArgumentByName(String name, Type type) { String json = argumentMap.get(name); return gson.fromJson(json, type); } /*Class只能处理普通类型,不能处理泛型*/ public Object getArgumentByName(String name, Class<?> klass) { String json = argumentMap.get(name); return gson.fromJson(json, klass); } }

用法:

public class ArgumentMakerDemo { public static void main(String[] args) { //对于发送userLogin(String id, String password)发送端就可以这样构造参数 String argumentMakerStr = new ArgumentMaker() .addArg("id", "id的值") .addArg("password", "password的值").toString(); System.out.println(argumentMakerStr); //对于接收端接收到argumentMakerStr,可以这样解析 ArgumentMaker argumentMaker = new ArgumentMaker(argumentMakerStr); String id = (String) argumentMaker.getArgumentByName("id", String.class); System.out.println("收到的id的值:" + id); String password = (String) argumentMaker.getArgumentByName("password", String.class); System.out.println("收到的password的值:" + password); } } /* {"password":"\"password的值\"","id":"\"id的值\""} 收到的id的值:id的值 收到的password的值:password的值 */

关于ArgumentMaker的问题与回答。

Q:为什么参数的值需要转换成JSON(为什么要进行第一次JSON转换)?

A:不同的方法参数不尽相同,类型也是不尽相同的。值的类型是不确定的,模糊的,抽象的,以后用的时候,才能确定。要是直接写Object那就惨了,不安全,容易遭受破坏。Object是所有类的基类,它的能力越大,责任就越大,越容易遭人破坏,捣乱,随意放东西进去。(工具保护思想)

Q:引入Gson高级用法TypeToken解决泛型擦除问题。

A:解释这个问题就要讲泛型和泛型擦除了(在下面)。然后还有Gson解决在运行时获得泛型信息的类TypeToken。

private static final Type mapType = new TypeToken<Map<String, String>> () {}.getType();

如上面所看到的,创建一个TypeToken的匿名继承类。由于匿名类的申明信息中保留了泛型信息,通过反射可得。

具体的如何操作需要转到这篇博文去看。具体如何获取泛型信息。他的例子很好,讲到了字节码的深度,对于初入Java的我现在是不能理解的,只能以后等见的多了再去了解。

所以Google给我们的Gson工具太强大了,我们要好好膜拜大佬。

泛型

  泛型是JDK1.5的一项新增特性,它的本质是参数化类型(ParameterizedType),也就是说所操作的数据类型被指定为一个参数。这种参数类型可被应用于类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。在泛型出现之前,Java是通过Object是所有类型的父类和类型强制转换这两个特性来实现类型泛化的。 Java语言引入泛型的好处是安全简单(少了强制类型转换)。

  泛型通俗的来说就是为了把类型当成参数传递给一个类或者方法。

泛型擦除

首先给一个最经典的例子

import java.util.ArrayList; import java.util.List; public class Demo { public static void main(String[] args) { List<String> sList = new ArrayList<>(); List<Integer> iList = new ArrayList<>(); System.out.println(sList.getClass() == iList.getClass()); } }

  上述测试结果输出的为true?你也许会很疑惑,明明两个放不同类型的List,它们的class怎么会一样呢?

  造成这样的结果就是因为泛型擦除。泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,JVM并不知道泛型的存在。所以不管是 ArrayList<Integer> 还是 ArrayList<String>,在编译完成后都会被编译器擦除成了 ArrayList。

擦除具体规则:

若泛型类型没有指定具体类型,用Object作为原始类型;若有限定类型< T exnteds Class1 >,使用Class1作为原始类型;若有多个限定< T exnteds Class1 & Class2 >,使用第一个边界类型Class1作为原始类型;

  Java 泛型擦除是 Java 泛型中的一个重要特性,其目的是避免过多的创建类而造成的运行时的过度消耗。所以, ArrayList<Integer > 和 ArrayList<String> 这两个实例,其类实例是同一个。

泛型擦除的应用

对于一些编译型错误的问题(例如:List list = new ArrayList<>( ),如何把Activity对象放进list集合里去?),可以先略过,用反射机制进行。

举个例子吧。

public class Demo { public static void main(String[] args) { List<String> sList = new ArrayList<>(); sList.add("abc"); sList.add(456);//错误。The method add(int, String) in the type List<String> is not applicable for the arguments (int) } }

也正是因为泛型擦除:泛型信息是在编译前起作用的,编译后,对于运行时,就不起作用了。只要让他跳过编译阶段,我们可以利用反射绕过编译器去调用相关方法。

上述问题这么改就解决了

import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; public class Demo { public static void main(String[] args) { List<String> sList = new ArrayList<>(); sList.add("abc"); Class<?> klass = sList.getClass(); try { Method method = klass.getMethod("add", Object.class); //根据擦除规则,可知是Object method.invoke(sList, 456); for (Object o : sList) { //遍历的时候也只可以for each了,因为get方法返回值是泛型,int 不能强转为String System.out.println(o); } } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } }
最新回复(0)