简述
需要将对象持久化到我们的存储设备上或者通过网络传输到其他客户端,那么我们就需要序列化。
Serializable为Java中带的一种序列化方式,使用起来非常简单。只需要让类实现一个Serializable接口就可以进行序列化了。
使用
先讲初步使用,后面会讲到serialVersionUID。
- 定义一个User类,实现Serializable接口
public class User implements Serializable {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
- 操作User类,持久化到cache.txt文件中,并从文件中读出来
//创建到User类
User user = new User("大灰狼", 18);
try {
File file = new File("cache.txt");
if (!file.exists()) {
file.createNewFile();
}
//将User类序列化到文件中
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
objectOutputStream.writeObject(user);
System.out.println("序列化成功");
//从文件中读出来
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
User user1 = (User) objectInputStream.readObject();
System.out.println("" + user1);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
就这样,最简单到使用就完成了。
serialVersionUID
如果,咱们某一天改了User类的内部结构,如增加了一个字段:boolean isMan,但是文件类已经持久化的数据并没有改动,我们需要反序列化读出来
- 读取代码
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
User user1 = (User) objectInputStream.readObject();
System.out.println("" + user1);
我们会很明显的看到报出一个错误:
java.io.InvalidClassException: serial.User; local class incompatible: stream classdesc serialVersionUID = -3951066851478225822, local class serialVersionUID = -8381808064053356693
- 原因:因为咱们存储的时候,有帮我们自动生成一个serialVersionUID就是-3951066851478225822,可以当作存储在文件中的类的版本标示。而我们本地类版本升级了,自动帮我们修改了版本标示。在反序列化的时候会检测这个版本标示是否相同,所以在反序列化的时候就通不过了,这时候就不能反序列化了。
我们自然不想看到这种情况出现,所以通常情况下,我们应该手动指定serialVersionUID的值,如123L
只需要在User中添加:
private static final long serialVersionUID = 123L;
那我们重新试试(前面的文件不能用了,删除后再来).
-
先写文件,只包含name和age。
-
然后我们再升级User类,在其中添了两个字段如下:
private boolean isMan;
private String address;
- 再读一次文件,打印如下
User{name='大灰狼', age=18, isMan=false, address='null'}
可以看到能成功读取了,只是缺失字段使用了默认值代替,做到了最大限度的恢复数据。
注意:类升级指的是内部结构,如果改了类名,那是怎么都反序列化不回来的,因为类结构有了毁灭性的改变,那错误将是这样的:
java.lang.ClassNotFoundException: serial.User
重写序列化和反序列化
就如同我们调用一样,序列化过程其实就是通过ObjectOutputStream的writeObject、readObject来体现的。我们跟踪下流程,可以看到调用到了要序列化对象的私有方法writeObject,部分代码如下:
- 从ObjectOutputStream进行写方法
public final void writeObject(Object obj) throws IOException {
try {
//调用该方法执行写
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
- writeObject0:
private void writeObject0(Object obj, boolean unshared){
for (;;) {
// REMIND: skip this check for strings/arrays?
Class<?> repCl;
//查找类信息,这里明显使用到了反射来查找
desc = ObjectStreamClass.lookup(cl, true);
if (!desc.hasWriteReplaceMethod() ||
(obj = desc.invokeWriteReplace(obj)) == null ||
(repCl = obj.getClass()) == cl)
{
break;
}
cl = repCl;
}
}
- ObjectStreamClass.lookup
static ObjectStreamClass lookup(Class<?> cl, boolean all) {
try {
//创建流对象
entry = new ObjectStreamClass(cl);
} catch (Throwable th) {
entry = th;
}
}
- 创建流对象,寻找到要序列化对象中的私有方法,得到writeObjectMethod和readObjectMethod
private ObjectStreamClass(final Class<?> cl) {
//获取对象中的私有方法,writeObject和readObject
writeObjectMethod = getPrivateMethod(cl, "writeObject",
new Class<?>[] { ObjectOutputStream.class },
Void.TYPE);
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class<?>[] { ObjectInputStream.class },
Void.TYPE);
}
- 在执行写序列化数据时调用到方法:writeSerialData
private void writeSerialData(Object obj, ObjectStreamClass desc)
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
//执行到了创建好流对象的方法
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
curContext.setUsed();
curContext = oldContext;
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
- invokeWriteObject调用到了writeObjectMethod.invoke
void invokeWriteObject(Object obj, ObjectOutputStream out)
throws IOException, UnsupportedOperationException
{
if (writeObjectMethod != null) {
try {
//通过反射执行了要序列化对象的writeObject方法
writeObjectMethod.invoke(obj, new Object[]{ out });
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof IOException) {
throw (IOException) th;
} else {
throwMiscException(th);
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
到这里我们就能看到我们要序列化对象的writeObject也被调用了,readObject过程分析也是一样的。通过这样我们可以想象,是不是我们就可以自己实现这个方法来重写写数据呢?答案肯定是的,哈哈哈.
重写writeObject和readObject:
- 改isMan默认不序列化
- 重写写和读方法
public class User implements Serializable {
private static final long serialVersionUID = 123L;
private String name;
private int age;
private transient boolean isMan;
private String address;
public User(String name, int age, boolean isMan, String address) {
this.name = name;
this.age = age;
this.isMan = isMan;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public boolean isMan() {
return isMan;
}
public void setMan(boolean man) {
isMan = man;
}
public static long getSerialVersionUID() {
return serialVersionUID;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", isMan=" + isMan +
", address='" + address + '\'' +
'}';
}
/**
* 重写写方法
*
* @param oos
* @throws IOException
*/
private void writeObject(ObjectOutputStream oos) throws IOException {
//调用默认的序列化方法,如里面注释一样,可以把非静态和非transient字段给序列化了
oos.defaultWriteObject();
//把isMan也序列化一下
oos.writeBoolean(isMan);
System.out.println("序列化成功");
}
/**
* 重写读方法
*
* @param ois
* @throws IOException
* @throws ClassNotFoundException
*/
private void readObject(ObjectInputStream ois) throws IOException,
ClassNotFoundException {
//调用默认的反序列化方法,如里面注释一样,可以把非静态和非transient字段给反序列化了
ois.defaultReadObject();
//读取序列化的字段
isMan = ois.readBoolean();
System.out.println("反序列化成功");
}
}
再进行序列化和反序列化,就能看到isMan咱们也能够正常写入和读了。
序列化是持久化的一个必备品,记录到这里大概对Serializable就更熟悉了。
网友评论