唠嗑
好久没有写博客了,懈怠了,周一到周五工作,没时间写,周末想着放松放松,也不想写,一拖再拖。
最近意识到正式工作快满一年了(稳定运营的游戏部门,进行玩法开发),想回头想一想这一年有什么进步,却发现好像除了对业务更加熟悉一些,技术上并不知道有哪些明显的提升,问了下与我一起入职的同事,也有这样的困惑。这也许是业务部门的通病,公司论坛中也有很多人表示:一直维护老系统,如何提升技术,如何晋级等。还记得有个观点大概是这样的:
优化项目代码,对重复的代码抽象出通用的模块
我们现在的项目中确实有许多不优雅的、重复的代码,虽然我知道它好像有点问题,但是我暂时没有能力去解决这个问题。
前两天在研究一个并发的问题时,搜到了一个大佬的博客,发现大佬写了很多基础性的文章,意识到别人的成功并不是偶然,所以我还是先从基础入手吧,地基打好了再去尝试解决上层的问题。
序列化与反序列化
这篇文章的主要内容是jdk的序列化机制,主要是与Serializable
有关的内容,序列化主要用在数据存储和网络传输。其实项目中并没有用到Serializable
这个序列化机制,用的主要是protobuf
,JDK自带的性能和拓展性上逊色了一些。
java对象在运行时都是在内存中,程序结束就消失了,如果我们想要在下次启动程序的时候恢复这些对象,就需要把内存中的这些对象的成员变量数据保存到一个地方,比如说文件中,然后下次启动时从文件里读取并放入内存,这边就涉及到java内存对象到文件内容的相互转化,就是序列化。当然你可以有很多不同的序列化协议,比如对内容加密,对内容压缩(传输快,占用空间小),JDK就提供了这样的一种机制。
下面看下如何使用JDK的序列化机制,将User
对象保存进文件,然后再读出来。
// User对象
public class User implements Serializable {
public String name;
public int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
// 省略getter setter
}
public static void main(String[] args) {
User user1 = new User("cfk", 24);
// 写入
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(new File("user.txt")))) {
out.writeObject(user1);
} catch (IOException e) {
logger.error("write error.", e);
}
// 读取
User user2 = null;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(new File("user.txt")))) {
user2 = (User) in.readObject();
} catch (Exception e) {
logger.error("read error.", e);
}
if (user2 != null) {
System.out.println("age:" + user2.getAge());
System.out.println("name:" + user2.getName());
System.out.println("isEqual:" + (user1 == user2));
}
}
输出结果
age:24
name:cfk
isEqual:false
user.txt
文件的内容:
- ���sr�com.chenfeikun.User~�s:�!���I�ageL�namet�Ljava/lang/String;xp�t�cfk
上面是一个简单的使用示例,但是还有很多疑问在里面:
-
Serializable
接口的作用 - serialVersionUID的作用,为什么User里没有定义也可以序列化成功
- 反序列化时使用成员变量与User一模一样的Student类会成功吗
- 序列化后修改User类,再反序列化,会发生什么
- transient修饰符有什么用
后面会通过源码和实验来一一回答这些问题。
Serializable
看下接口的定义
public interface Serializable {}
接口没有定义任何的方法,那User为什么必须实现这个接口呢,那么JDK采用什么方式来进行序列化呢?这些来看下ObjectOutputStream
的writeObject
方法。
public final void writeObject(Object obj) throws IOException {
if (enableOverride) { // 给一些ObjectOutputStream的子类用的
writeObjectOverride(obj);
return;
}
try { // 如果是ObjectOutputStream,则默认该分支
writeObject0(obj, false);
} catch (IOException ex) {
// ignore
}
}
// writeObject0方法
private void writeObject0(Object obj, boolean unshared) throws IOException {
boolean oldMode = bout.setBlockDataMode(false);
depth++;
try {
// 省略一些代码
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
// 如果不是上面列出的几种类型,就必须是Serializable的实例对象
// 否则抛出异常
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
} finally {
depth--;
bout.setBlockDataMode(oldMode);
}
}
上面的这些代码已经可以解释为什么需要序列化的对象需要继承Serializable
接口,下面继续深入下去,看看默认的序列化方法是什么,User.txt
保存了什么内容。
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc, boolean unshared) throws IOException
{
//省略debug日志
try {
desc.checkSerialize();
bout.writeByte(TC_OBJECT); // 首先写入一个标识符,表示对象的开始
writeClassDesc(desc, false);// 写入类相关数据,下面分析
handles.assign(unshared ? null : obj);
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
writeSerialData(obj, desc);//写入对象数据,下面分析
}
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
//writeClassDesc方法
private void writeClassDesc(ObjectStreamClass desc, boolean unshared)
throws IOException {
if (desc == null) {
// 省略
} else {
writeNonProxyDesc(desc, unshared);//最终选择这个分支
}
}
//writeNonProxyDesc方法
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
throws IOException {
bout.writeByte(TC_CLASSDESC);//写入类描述信息开始标记
handles.assign(unshared ? null : desc);
if (protocol == PROTOCOL_VERSION_1) {
desc.writeNonProxy(this);
} else {
// 进入该分支
writeClassDescriptor(desc);//写入类信息,包括serialVersionUID
}
Class<?> cl = desc.forClass();
bout.setBlockDataMode(true);
if (cl != null && isCustomSubclass()) {
ReflectUtil.checkPackageAccess(cl);
}
annotateClass(cl);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);// 写入另一个标记
writeClassDesc(desc.getSuperDesc(), false); // 写入父类信息
}
// writeClassDescriptor最终调用了
void writeNonProxy(ObjectOutputStream out) throws IOException {
out.writeUTF(name);
out.writeLong(getSerialVersionUID());//写入serialVersionUID
// ignore
out.writeShort(fields.length);//写入成员变量的个数
//对于这种可变的数据,一般在开头都会写入个数限制,明确数据的范围
for (int i = 0; i < fields.length; i++) {
// 写入成员变量的名字,数据类型
ObjectStreamField f = fields[i];
out.writeByte(f.getTypeCode());
out.writeUTF(f.getName());
if (!f.isPrimitive()) {
out.writeTypeString(f.getTypeString());
}
}
}
上面的部分粗略的描述了下User.txt
文件中的一部分内容:类相关的描述信息,其实具体往文件写了什么并不重要,这只是JDK自己设计的一个协议,就像http,tcp等也有自己的协议,哪一部分对应了什么,都是协议的一部分。
让我们自己设计其实也很容易想明白,需要写入类的描述信息(类名,成员变量名,数据类型等),父类的描述信息,对象的成员变量值等,下面来看下成员变量是如何写入文件的。
在上面的writeOrdinaryObject
方法中,写完类相关的信息以后,就开始写入成员变量的数据了。
//writeSerialData写入成员变量的方法
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException {
OectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
if (slotDesc.hasWriteObjectMethod()) { // 如果有writeObjecr方法,则调用
// ignore
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} else { // 否则使用默认的序列化方法
// 因为我们的User对象没有实现writeObject方法,所以进入该分支
defaultWriteFields(obj, slotDesc); // 不介绍了,将变量的具体值写入
}
}
}
ObjectStreamClass desc对象介绍
上面的方法中有一个一直出现的desc变量,它保存了类的一些信息:
-
serialVersionUID
的值,如果不存在则会根据类的一些信息,计算出一个值(因此类发生改变,这个值就会变化),如果存在则直接使用我们定义的数值。 - 是否实现了
writeObject
和readObject
方法,从而判断选择默认的序列化方法还是选择自定义的序列化方法 - 有哪些成员变量,剔除了transient和statci修饰的变量
与这些信息相对应的代码如下:
// 获取serialVersionUID的方法
// 首先查看类自己有没有定义
private static Long getDeclaredSUID(Class<?> cl) {
try {
// 没有获取到会抛异常
Field f = cl.getDeclaredField("serialVersionUID");
int mask = Modifier.STATIC | Modifier.FINAL;
if ((f.getModifiers() & mask) == mask) {
f.setAccessible(true);
return Long.valueOf(f.getLong(null));
}
} catch (Exception ex) {
}
return null;
}
// 如果没有,则计算一个
// 计算过程比较复杂,不看了
// 参与计算的对象包括:类名,方法,接口,变量相关信息等
public long getSerialVersionUID() {
if (suid == null) {
suid = AccessController.doPrivileged(
new PrivilegedAction<Long>() {
public Long run() {
return computeDefaultSUID(cl);
}
}
);
}
return suid.longValue();
}
// 何时判断有没有writeObjecet和readObject方法
// 在创建desc对象时的构造函数里
writeObjectMethod = getPrivateMethod(cl, "writeObject",
new Class<?>[] { ObjectOutputStream.class },
Void.TYPE);
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class<?>[] { ObjectInputStream.class },
Void.TYPE);
// 获取成员变量的方法
private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
// 参数cl就是需要序列化的对象的class
Field[] clFields = cl.getDeclaredFields();
ArrayList<ObjectStreamField> list = new ArrayList<>();
int mask = Modifier.STATIC | Modifier.TRANSIENT;
for (int i = 0; i < clFields.length; i++) {
// 剔除transient和statci修饰的变量
if ((clFields[i].getModifiers() & mask) == 0) {
list.add(new ObjectStreamField(clFields[i], false, true));
}
}
int size = list.size();
return (size == 0) ? NO_FIELDS :
list.toArray(new ObjectStreamField[size]);
}
既然文件里存储了SerialVersionUID
,那么显然读取的时候会依靠它来判断是否能够反序列化。如果User没有定义SerialVersionUID
,那么通过类的一些信息可以计算出一个uid,不难想象的是:如果类的定义发生了一些改变,uid的值就会变,反序列化是会失败的。
除了uid,前面也提到了,User.txt
会写入序列化对象的类名,所以反序列化也不可能变成Student
类
反序列化时使用成员变量与User一模一样的Student类会成功吗?
因此这个问题的答案应该是不会成功。下面通过代码看下如果修改User类的定义会怎么样。
代码验证
验证步骤:
- 把上面的User对象序列化后写入文件
- 修改User类,增加一个private变量(删除效果一样)
- 用新的User类反序列化
反序列化代码如下:
// 序列化代码与最上面的一样,反序列化增加了一行print新变量school
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(new File("user.txt")))) {
User user2 = (User) in.readObject();
System.out.println("age:" + user2.getAge());
System.out.println("name:" + user2.getName());
System.out.println("school:" + user2.getSchool());
} catch (Exception e) {
logger.error("read error.", e);
}
验证结果分为自定义serialVersionUid和不定义serialVersionUid两种情况
- 不定义uid
运行后报错,serialVersionUid不同
java.io.InvalidClassException: com.chenfeikun.User; local class incompatible: stream classdesc serialVersionUID = 1261513276839669673, local class serialVersionUID = -8884757256612786415
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at com.chenfeikun.Test1.main(Test1.java:21)
- 定义uid
运行正常,新的变量为null
age:24
name:cfk
school:null
serialVersionUid需要自己定义吗?
我想是需要的,因为无法预知以后的需求,对象很有可能改变,如果使用JDK帮忙计算的uid值,就没有拓展性,只有自定义了serialVersionUid才能够支持对象修改后,依然可以正常的序列化与反序列化。
writeObject与readObject
如果不想使用默认的序列化方式,也可以通过实现这两个方法来定义自己的序列化过程。直接看JDK源码中的例子吧,就拿大家都用过的ArrayList
来看看如何实现。
从上面的分析可以知道static
和transient
修饰的变量不会进行序列化,那么ArrayList
的变量就只剩下一个了
private int size;
而ArrayList存储数据的数组是用transient修饰的
transient Object[] elementData;
如果列表序列化时不存储列表中的数据,那么序列化毫无意义,答案就在writeObject和readObject里。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// modCount用来防止并发修改,以前分析ArrayList源码的文章介绍过
int expectedModCount = modCount;
s.defaultWriteObject();
s.writeInt(size);// 写入数组中数据的实际个数
for (int i=0; i<size; i++) {
// 通过for循环把数据写入
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
// readObject方法
// 差不多就把write方法反过来
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;// 初始化空数组
s.defaultReadObject();
s.readInt();
if (size > 0) {
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
ensureCapacityInternal(size);
Object[] a = elementData;
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
ArrayList这样设计有什么好处呢?
因为数组可能没有塞满数据,如果无脑的全部序列化,会浪费很多没用的空间,所以自己实现writeObject方法只把有用的数据进行序列化。
Externalizable vs Serializable
前面的代码中,写入成员变量时我们只分析了一个分支,其实还有另外一个分支,回顾一下。
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);//这个没介绍
} else {
writeSerialData(obj, desc);//前面介绍了这个
}
上面的分支就是实现了Externalizable
接口的对象会走的分支,两者的区别是:Externalizable
强制要求自己实现序列化的过程,而Serializable
有默认的序列化方式。
可以看下其接口定义:
// 继承了Serializable
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
其他没有太多区别了。
总结
- 需要继承
Serializable
接口 - 对想要序列化的对象定义serialVersionUID值,支持一定的兼容性
- 如果不定义serialVersionUID,JDK会自动计算一个(根据类相关信息)
- static和transient修饰的变量不会序列化