读完这篇文章你将会收获到
Serializable
和Externalizable
的使用- 序列化
ID
问题 - 静态变量序列化
- 父类的序列化
ArrayList
序列化:为啥size
被序列化两次?- 序列化对单例的破坏
序列化就是将对象的状态信息转为可以存储或者传输的形式的过程
比如说将对象序列化之后存储在硬盘上
比如说将对象序列化之后返回给调用方
反序列化则是序列化的反过程
Serializable
我们在 Java
中经常借助 Serializable
和 ObjectOutputStream
和 ObjectInputStream
进行序列化和反序列化操作
public class Person implements Serializable { private String name; private String wish; ............. ............. } 复制代码
private void serialize() throws Exception { Person person = new Person(); person.setName("coderLi"); person.setWish("被关注"); FileOutputStream fileOutputStream = new FileOutputStream("coderLi.per"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(person); objectOutputStream.close(); fileOutputStream.close(); } private void deserialize() throws Exception { FileInputStream fileInputStream = new FileInputStream("coderLi.per"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); Person person = (Person) objectInputStream.readObject(); System.out.println(person); } 复制代码
在 Java
中一个对象想要序列化成功、必须满足两个条件
- 该类必须实现
Serializable
接口 - 该对象的所有属性都是可序列化的,如果不想参与序列化或者不能序列化、则可以使用
transient
修饰
在 Serializable
接口中其实并无任何的方法、只是单纯的一个空接口。它的作用仅仅是作为一个标记。我们可以在 java.io.ObjectOutputStream#writeObject0
中找到其判断
// remaining cases 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) { writeOrdinaryObject(obj, desc, unshared); } else { ....... ....... throw new NotSerializableException(cl.getName()); } 复制代码
可以看到如果你既不是 String
, Array
, Enum
, 也不是 Serializable
, 那么你就等着吃异常吧 !
Externalizable
Externalizable
继承 Serializable
接口并添加了两个方法,通过实现这两个类来序列化或反序列化对象
public interface Externalizable extends java.io.Serializable { void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; } 复制代码
public class Animal implements Externalizable { private String name; private int age; @Override public void writeExternal(ObjectOutput out) throws IOException { System.out.println(out.getClass().getSimpleName()); out.writeInt(age); out.writeObject(name); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { System.out.println(in.getClass().getSimpleName()); age = in.readInt(); name = (String) in.readObject(); } } 复制代码
但是使用 Externalizable
要注意
- 必须提供一个
public
的无参构造方法(因为反序列化的时候是先创建一个对象然后再调用readExternal
方法) - 写入的顺序与读取的顺序要一致
调试发现、序列化的时候 ObjectOutput
的参数对象的类型是 ObjectOutputStream
、反序列化的时候 ObjectInput
的参数对象的类型是 ObjectInputStream
序列化 ID
在上面的例子中、我们都没有加上 serialVersionUID
, 我们现在在 Animal
中加上并随意赋值
private static final long serialVersionUID = 1L; 复制代码
然后我将其序列化到 animal.ani
文件中、然后修改 serialVersionUID
的值变为 2L
、然后看看会发生什么
java.io.InvalidClassException: com.demo.Animal; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2 复制代码
我们可以从异常信息中知道、序列化的时候是有保存其 serialVersionUI
D 的,如果反序列化的时候两个值不一致、则会反序列化失败
那假如我们不指定这个静态常量的值,它是根据什么生成的?
其实这个值的生成是根据这个类的信息去生成的,我尝试了一下、不指定 serialVersionUID
序列化之后、然后为这个类增加一个非 final
的属性,反序列化就报上面的异常了。当然,你也可以为这个类增加方法、同样也会导致 serialVersionUID
不同继而反序列化失败
所以没有什么特殊要求的时候、我们可以将 serialVersionUID
设置为 1
(当然也可以是其他值,只要指定值就行) , 那么就不会说因为服务端升级改东西了,客户端暂时没有升级而导致反序列化失败
静态变量序列化
默认情况下、静态变量不参与序列化。对象的序列化、当然只是序列化对象的属性啦
例子就不贴了
父类的序列化
要将父类的属性也序列化、那就让父类实现 Serializable
接口吧。如果父类不实现呢?那爸爸你给我一个无参的构造方法吧,那么这个时候我就不序列化你了,但是因为又这个无参的构造函数、那么我在反序列化的时候就可以调用这个构造函数、因为 Java
里面、是先有父对象才有子对象嘛,当然反序列完之后的父类属性、如果没有在在无参构造方法中赋值的话、那么就是其类型的默认的值了
ArrayList 序列化
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8683452581122892189L; transient Object[] elementData; private int size; ............ } 复制代码
我们在上面的已经分析了
// remaining cases 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) { writeOrdinaryObject(obj, desc, unshared); } else { ....... ....... throw new NotSerializableException(cl.getName()); } 复制代码
当一个对象是 Serializable
的实例、那么就会进入到 writeOrdinaryObject
中,那么我们看看它这个方法做了什么
private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException { 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 { } } 复制代码
ArrayList
并没有实现 Externalizable
接口,所以直接进入到 writeSerialData
方法中
private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException { ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); for (int i = 0; i < slots.length; i++) { ObjectStreamClass slotDesc = slots[i].desc; if (slotDesc.hasWriteObjectMethod()) { ........... // 这里这里是重点 slotDesc.invokeWriteObject(obj, this); bout.setBlockDataMode(false); bout.writeByte(TC_ENDBLOCKDATA); } finally { curContext.setUsed(); curContext = oldContext; } curPut = oldPut; } else { defaultWriteFields(obj, slotDesc); } } } 复制代码
我们 能看到这里有一个分支,如果你有一个叫做 writeObject
的方法,那么我就调用你这个方法进行序列化,如果你没有则调用 defaultWriteFields
方法。我们看看 slotDesc.invokeWriteObject
方法吧
void invokeWriteObject(Object obj, ObjectOutputStream out) throws IOException, UnsupportedOperationException { requireInitialized(); if (writeObjectMethod != null) { try { writeObjectMethod.invoke(obj, new Object[]{ out }); } catch (InvocationTargetException ex) { ...... throw (IOException) th; } catch (IllegalAccessException ex) { throw new InternalError(ex); } } else { throw new UnsupportedOperationException(); } } /** class-defined writeObject method, or null if none */ private Method writeObjectMethod; /** class-defined readObject method, or null if none */ private Method readObjectMethod; 复制代码
我们看到这两个属性的定义、一个是 writeObject
、一个是 readObject
,这里便去 invoke
我们再回到 ArrayList
这个方法,巧了、在 ArrayList
中还真有这么两个方法
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount = modCount; // 将当前类的非静态(non-static)和非瞬态(non-transient)字段写入流 // 在这里也会将size字段写入。 s.defaultWriteObject(); // 序列化数组包含元素数量,为了向后兼容 // 两次将size写入流 s.writeInt(size); // 按照顺序写入,只写入到数组包含元素的结尾,并不会把数组的所有容量区域全部写入 for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } /** * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is, * deserialize it). */ private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { elementData = EMPTY_ELEMENTDATA; // 将流中的的非静态(non-static)和非瞬态(non-transient)字段读取到当前类 // 包含 size s.defaultReadObject(); // Read in capacity s.readInt(); // ignored if (size > 0) { // be like clone(), allocate array based upon size not capacity int capacity = calculateCapacity(elementData, size); SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity); ensureCapacityInternal(size); Object[] a = elementData; // Read in all elements in the proper order. for (int i=0; i<size; i++) { a[i] = s.readObject(); } } } 复制代码
其实为啥 ArrayList
需要自定义一个序列化和反序列化?其实大概看看其序列化的代
码、就能大概估摸出其意图,ArrayList
中的数组大小本身是比实际存储的元素个数要多的,我们序列化的时候没必要将没有用到的数组空间也序列化下来、显然是浪费性能的
其实代码中可以看到 size
被序列化了两次,而在反序列化的时候却直接丢弃第二次序
列化的 size
? why
其实这么做是为了兼容问题、在旧版本的 JDK
中、ArrayList
的实现有所不同、会对 length
字段进行序列化
而在现在的版本中、不再序列化 length
了。为了能是新版本的序列化的对象能在旧版中能顺利的反序列化、所以就将 size
序列化两次了
序列化对单例的破坏
我们常见的单例模式 比如说 DCL
public class Singleton implements Serializable{ private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } } 复制代码
我们将其序列化、然后反序列化、那么得到的是一个新的对象,而不是原来的对象、也就是说 JVM 中现在同时存在两个 Singleton
对象了
其实挺扯淡的、你想单例还实现 Serializable
接口? 哈哈哈、好吧,那我们把 Serializable
接口去掉,那是不是还可以通过反射创建一个新的实例,那行我们在构造方法中判断一下静态变量 singleton
是否为空、不为 null
就直接抛异常。貌似这样子还行,但是这个判断放在构造函数里面是否会太迟了。
我们回到上面的代码中、假设就是有这么扯淡的代码、要单例还实现了 Serializable
接口、那么我们可以保障其在 JVM
中的唯一性呢
我们查看 ObjectInputStream
的 readObject
方法开始追踪
readObject->readObject0->readOrdinaryObject 复制代码
在这个方法里面看到了一个比较跟上面 invokeWriteObject
类似的方法: invokeReadResolve
点进去看了下、哦吼、我们可以在 Singleton
中实现一个 readResolve
的方法、它会在反序列化的时候被调用到、然后就最终返回给反序列化调用方
public Object readResolve(){ return singleton; } 复制代码
但是这里还是存在这么一个问题、在某个时刻,JVM
确实存在过两个这个单例类的对象、即使它没有被返回给反序列化的调用方,但却是真实存在
那么有没有一个好的单例模式可以用呢? 有、那就是枚举。因为枚举的反序列化最终调用的是 Enum.valueOf
的方法
其实这就是为啥推荐使用枚举作为单例的原因。对于反射调用构造方法、枚举也是做了限制直接抛异常的