文章目录
序列化
序列化:将对象可以写入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();
}
}
}