1. 序列化的含义、意义及使用场景
Java平台允许创建在内存中可以复用的java对象,但一般情况下,对象只存在于虚拟机运行期,当JVM结束运行,对象就不存在了。序列化就是将内存中的对象,持久化保存,并可以在将来可以重新恢复成对象。
- **序列化:**将对象写到IO流中
- **反序列化:**从IO流中恢复对象
- **意义:**序列化机制允许将实现序列化的Java对象,转换为字节数组,这些字节数组可以保存到磁盘,或者通过网络传输,已达到后来恢复成原来的对象。序列化机制使得对象可以脱离程序而存在。
- **使用场景:**所以在网络上传输的对象必须要实现序列化(远程方法调用的bean必须实现序列化),所有需要保存到磁盘的java对象都必须是可序列化的。通常建议:程序创建的每个JavaBean类都实现Serializeable接口。
2. 序列化与反序列化
在Java中,只需要给类实现接口Serializable
,标识这个类可以被序列化。
package interview.SerializableDemo;
import java.io.Serializable;
import java.util.Date;
/**
* 创建可以被实例化的类
*
* @author: mahao
* @date: 2019/8/24
*/
public class User implements Serializable {
//指定序列号id,如果不指定JVM会自动计算出来,序列号不变,都可以进行反序列操作。
private static final long serialVersionUID = 8683452581122892180L;
private String name;
private int age;
private Date time;
private boolean isMan;
//static String flag = "static field";
public User(String name, int age, Date time, boolean isMan) {
System.out.println("init method be invoke ... ");
this.name = name;
this.age = age;
this.time = time;
this.isMan = isMan;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", time=" + time +
", isMan=" + isMan +
'}';
}
}
序列化 反序列化
package interview.SerializableDemo;
import org.junit.Test;
import java.io.*;
import java.util.Date;
/**
* 序列化和反序列化对象:
* 步骤:序列化
* 1.创建一个ObjectOutputStream输出流:
* 2.调用ObjectOutputStream对象的writerObject输出可序列化对象
* <p>
* 反序列化:
* 1.创建一个ObjectInputStream输入流
* 2.调用ObjectInputStream对象的readObject得到序列化的对象。
* <p>
* <p>
* 输出告诉我们,反序列化并不会调用构造方法。
* 反序列的对象是由JVM自己生成的对象,不通过构造方法生成。
*
* @author: mahao
* @date: 2019/8/24
*/
public class UserMain {
public static void main(String[] args) {
//序列化对象
User user = new User("mahao", 18, new Date(), true);
ObjectOutputStream out = null;
ObjectInputStream in = null;
try {
out = new ObjectOutputStream(new FileOutputStream("user.obj"));
out.writeObject(user);
//反序列化
in = new ObjectInputStream(new FileInputStream("user.obj"));
User serUser = (User) in.readObject();
System.out.println(user == serUser);
System.out.println(serUser);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (out != null)
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Test
public void test2() throws Exception {
//反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.obj"));
User serUser = (User) in.readObject();
// System.out.println(user == serUser);
System.out.println(serUser);
}
}
输出结果:
init method be invoke ...
false
User{name='mahao', age=18, time=Sat Aug 24 11:32:48 CST 2019, isMan=true}
成员也必须是支持序列化的:
package interview.SerializableDemo;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
*属性中的引用类型也必须是Serializable的子类
*
* @author: mahao
* @date: 2019/8/24
*/
public class MainClass2 {
/*
Exception in thread "main" java.io.NotSerializableException: interview.SerializableDemo.Dog
*/
public static void main(String[] args) throws Exception {
Person p = new Person();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.obj"));
oos.writeObject(p);
}
}
class Person implements Serializable {
private String name;
private Dog dog = new Dog();
}
class Dog {
}
3. 同一对象序列化多次的机制
==同一个对象序列化多次,会将这个对象序列化多次吗?==答案是否定了。
Java序列化算法:
- 所有保存到磁盘的对象都有一个序列化编码号
- 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
- 如果此对象已经序列化过,则直接输出编号即可。
public static void main(String[] args) throws Exception {
User u1 = new User("mahao", 18, new Date(), true);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MainClass3.obj"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("MainClass3.obj"));
oos.writeObject(u1);
oos.writeObject(u1);
User u2 = (User) ois.readObject();
User u3 = (User) ois.readObject();
System.out.println(u1 == u2);//false u2是虚拟机新生成的对象,和u1不是同一个
System.out.println(u2 == u3);//true u3和u2是同一对象,u2 u3是同一个对象序列化而来的,不会将这个对象序列化多次。
/*
结论:
从输出结果可以看出,Java序列化同一对象,并不会将此对象序列化多次得到多个对象。序列化时,如果已经
序列化过某个对象,则第二次序列化时,不会对他进行多次序列化,只会输出编号。
Java序列化算法:
1.所有保存到磁盘的对象都有一个序列化编码号
2.当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有对象从未在此虚拟机上序列化过,
才会将对象对象序列化为字节序列输出;
3.如果已经序列化过了,则会直接输出编号即可。
*/
}
序列化算法的问题:
由于java序利化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号。
/*
java序列化算法也会带来问题,他不会重复序列化一个已经序列化过的对象,只会记录那个序列化后的对象的编号。
如果序列化一个可变对象,更改了对象的内容,再次序列化,则不会再次序列化,而是只保存上次的编号。
*/
@Test
public void testTrouble() throws Exception {
User u1 = new User("mahao", 18, new Date(), true);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MainClass3tr.obj"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("MainClass3tr.obj"));
oos.writeObject(u1);
u1.setName("ABC");
oos.writeObject(u1);
User u2 = (User) ois.readObject();
User u3 = (User) ois.readObject();
System.out.println(u2);
System.out.println(u3);
}
//结果
init method be invoke ...
User{name='mahao', age=18, time=Sat Aug 24 16:03:49 CST 2019, isMan=true}
User{name='mahao', age=18, time=Sat Aug 24 16:03:49 CST 2019, isMan=true}
4. transient
有些时候,我们有这样的需求,某些属性不需要序列化。使用transient关键字选择不需要序列化的字段。
从输出我们看到,使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。
package interview.SerializableDemo;
import java.io.*;
import java.util.Date;
/**
* transient关键字的使用:
* <p>
* 些时候,我们有这样的需求,某些属性不需要序列化。
* 使用transient关键字选择不需要序列化的字段。
* <p>
* 从输出我们看到,使用transient修饰的属性,java序列化时,
* 会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。
* 对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。
*
* @author: mahao
* @date: 2019/8/24
*/
public class TransientTest {
public static void main(String[] args) throws Exception {
User u1 = new User("mahao", 18, new Date(), true);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("TransientTest.obj"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("TransientTest.obj"));
oos.writeObject(u1);
User u2 = (User) ois.readObject();
System.out.println(u1);
System.out.println(u2);
}
static class User implements Serializable {
private String name;
private int age;
private Date time;
private transient boolean isMan;
private transient Dog dog = new Dog();
public User(String name, int age, Date time, boolean isMan) {
System.out.println("init method be invoke ... ");
this.name = name;
this.age = age;
this.time = time;
this.isMan = isMan;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", time=" + time +
", isMan=" + isMan +
", dog=" + dog +
'}';
}
}
}
//结果
init method be invoke ...
User{name='mahao', age=18, time=Sat Aug 24 16:16:30 CST 2019, isMan=true, dog=interview.SerializableDemo.Dog@5b480cf9}
User{name='mahao', age=18, time=Sat Aug 24 16:16:30 CST 2019, isMan=false, dog=null}
5. 自定义序列化
在java集合体系下,很多的类都实现了Serializable
接口,但是内部实现的数据结构却定义了成了transient
,比如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; // non-private to simplify nested class access
private int size;
省略了其他成员变量,从上面的代码中可以知道ArrayList
实现了java.io.Serializable
接口,那么我们就可以对它进行序列化及反序列化。因为elementData
是transient
的,所以我们认为这个成员变量不会被序列化而保留下来。我们写一个Demo,验证一下我们的想法:
public static void main(String[] args) throws Exception {
ArrayList<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
System.out.println("serializable before : " + list);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MainClass4.obj"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("MainClass4.obj"));
oos.writeObject(list);
ArrayList<String> list2 = (ArrayList<String>) ois.readObject();
System.out.println(list2);
}
//
serializable before : [aaa, bbb, ccc]
[aaa, bbb, ccc]
了解ArrayList的人都知道,ArrayList底层是通过数组实现的。那么数组elementData
其实就是用来保存列表中的元素的。通过该属性的声明方式我们知道,他是无法通过序列化持久化下来的。那么为什么code 4的结果却通过序列化和反序列化把List中的元素保留下来了呢?
6. writeObject
和readObject
方法
在ArrayList中定义了来个方法: writeObject
和readObject
。
这里先给出结论:
在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。
如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。
用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。
方法具体实现:
//* Save the state of the <tt>ArrayList</tt> instance to a stream (that
// * is, serialize it).
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);//序列化元素个数
// Write out all elements in the proper order.
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;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
//因为size属性是可以被序列化了,所以size已经是有值的了, s.readInt(); 可以被忽略
//而且可以使用size的原因。
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是动态数组,数组可能扩充到了很多元素,但是实际元素存储的少,导致有很多空的元素,为了保证null的元素浪费空间,自定义实现序列化,把数组定义为transient。
writeObject and readObject
为了防止一个包含了大量空对象的数组序列化,为了优化存储,所以ArrayList使用transient
来声明elementData
。但作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化下来,所以,通过重写writeObject
和 readObject
方法的方式把其中的元素保留下来。
writeObject
方法把elementData
数组中的元素遍历的保存到输出流(ObjectOutputStream)中。
readObject
方法从输入流(ObjectInputStream)中读出对象并保存赋值到elementData
数组中。
自定义序列化策略如何被调用:
通过之前的定义,ObjectOutputStream在调用writeObject(Object obj)方法时,会先去序列化的对象中寻找方法writeObject
,直接给出调用栈:
writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject
这里看一下invokeWriteObject
:
void invokeWriteObject(Object obj, ObjectOutputStream out)
throws IOException, UnsupportedOperationException
{
if (writeObjectMethod != null) {
try {
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();
}
}
所以,会先去判断序列化对象是否存在自定义的序列化方法,如果存在,则通过反射区调用序列化的方法去完成自定义的序列化操作。
7. Serializable 作用
writeObject0方法中有这么一段代码:
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 {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
在进行序列化操作时,会判断要被序列化的类是否是Enum、Array和Serializable类型,如果不是则直接抛出NotSerializableException
。
8. 自定义实现序列化策略
package interview.SerializableDemo;
import org.junit.Test;
import java.io.*;
import java.util.Arrays;
import java.util.List;
/**
* 自定义实现序列化和反序列化策略
*
* @author: mahao
* @date: 2019/8/24
*/
public class MainClass5 {
public static void main(String[] args) throws Exception {
Bean.Dog d1 = new Bean.Dog("d1");
Bean.Dog d2 = new Bean.Dog("d2");
Bean.Dog d3 = new Bean.Dog("d3");
Bean bean = new Bean("mahao", 10.5f, new Bean.Dog[]{d1, d2, d3},
Arrays.asList("aa", "bb", "cc"));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MainClass5.obj"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("MainClass5.obj"));
oos.writeObject(bean);
Bean bean1 = (Bean) ois.readObject();
System.out.println(bean);
System.out.println(bean1);
}
}
//transient加上则属性就不会去序列化了,但是其他的属性仍然会序列化,
//所以transient的使用时,对某些需要自定义序列化的属性,进行加上该关键字,然后在
//writeObject中实现该属性的自定义序列化实现,并不是,自定义了实现,该类就不执行默认的
//序列化了。
class Bean implements Serializable {
private String name;
private float sum;
private Dog[] dogs;
private transient List<String> list;
public Bean(String name, float sum, Dog[] dogs, List<String> list) {
this.name = name;
this.sum = sum;
this.dogs = dogs;
this.list = list;
}
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
System.out.println("writeObject be invoke");
s.defaultWriteObject();
s.writeObject(name);//写入名字
//忽略sum
//写入数组大小
s.writeInt(dogs.length);
for (Dog d : dogs) {//写入数组
s.writeObject(d);
}
s.writeObject(list);//list直接调用他的自定义实现
}
//自定义读取
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
System.out.println("readObject be invoke ");
s.defaultReadObject();
name = (String) s.readObject();
int length = s.readInt();
dogs = new Dog[length];
for (int i = 0; i < length - 1; i++) {
dogs[i] = (Dog) s.readObject();
}
s.readObject();
list = (List<String>) s.readObject();
}
@Override
public String toString() {
return "Bean{" +
"name='" + name + '\'' +
", sum=" + sum +
", dogs=" + Arrays.toString(dogs) +
", list=" + list +
'}';
}
static class Dog implements Serializable {
String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
}
案例地址: https://github.com/mashenghao/javaSe/tree/master/src/interview/SerializableDemo
参考:
- https://www.hollischuang.com/archives/1140
- https://juejin.im/post/5ce3cdc8e51d45777b1a3cdf