- 1. 使用对象流实现序列化
- 2. Serializable接口有何用?
- 3. serialVersionUID号有何用?
- 4. 两种特殊情况
序列化的原本意图是希望对一个Java对象作一下“变换”,变成字节序列,这样一来方便持久化存储到磁盘,避免程序运行结束后对象就从内存里消失,另外变换成字节序列也更便于网络运输和传播,所以概念上很好理解:
- 序列化:把Java对象转换为字节序列。
- 反序列化:把字节序列恢复为原先的Java对象。
java内置的序列化机制仅仅适用于Java语言,与其他序列化方式,比如Json序列化,Xml序列化,Protobuf序列化方式没有任何关系。
如果需要将某个对象保存到磁盘上或者通过网络传输,那么这个类应该实现Serializable接口或者Externalizable接口之一。使用Serializable来实现序列化非常简单,主要让目标类实现Serializable标记接口即可,无须实现任何方法。一旦某个类实现了Serializable接口,该类的对象就是可序列化的。
(1)创建一个ObjectOutputStream,这个输出流是一个处理流,所以必须建立在其他节点流的基础之上。
(2)调用ObjectOutputStream对象的writeObject()方法输出可序列化对象。
(3)调用ObjectInputStream对象的readObject()方法读取流中的对象,该方法返回一个Object类型的Java对象,如果程序知道该Java对象的类型,则可以将该对象强制类型转换成其真实的类型。
@Data
public class Student implements Serializable {
private String name;
private Integer age;
private Integer score;
}
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
serialize();
deserialize();
}
private static void serialize() throws IOException {
Student student = new Student();
student.setName("linko");
student.setAge( 18 );
student.setScore( 1000 );
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("student.txt"));
// 将Student对象写入输出流
objectOutputStream.writeObject(student);
objectOutputStream.close();
System.out.println("序列化成功!已经生成student.txt文件");
System.out.println("==============================================");
}
private static void deserialize() throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream("student.txt"));
Student student = (Student) objectInputStream.readObject();
objectInputStream.close();
System.out.println("反序列化结果为:");
System.out.println( student );
}
}
测试结果:
序列化成功!已经生成student.txt文件 ============================================== 反序列化结果为: Student(name=linko, age=18, score=1000)2. Serializable接口有何用?
上面在定义Student类时,实现了一个Serializable接口,然而当我们点进Serializable接口内部查看,发现它竟然是一个空接口,并没有包含任何方法!
public interface Serializable {
}
试想,如果上面在定义Student类时忘了加implements Serializable时会发生什么呢?
实验结果是:此时的程序运行会报错,并抛出NotSerializableException异常:
我们按照错误提示,由源码一直跟到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 {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
如果一个对象既不是字符串、数组、枚举,而且也没有实现Serializable接口的话,在序列化时就会抛出NotSerializableException异常!
原来Serializable接口也仅仅只是做一个标记用!!!
它告诉代码只要是实现了Serializable接口的类都是可以被序列化的!然而真正的序列化动作不需要靠它完成。
3. serialVersionUID号有何用?相信你一定经常看到有些类中定义了如下代码行,即定义了一个名为serialVersionUID的字段:
private static final long serialVersionUID = -4392658638228508589L;
继续来做一个简单实验,还拿上面的Student类为例,我们并没有人为在里面显式地声明一个serialVersionUID字段。
首先,按照上面的方式将Student对象序列化到student.txt文件中,然后改变Student类,比如添加一个字段studentId:
@Data
public class Student implements Serializable {
private String name;
private Integer age;
private Integer score;
private Integer studentId;
}
运行发现报错了,并且抛出了InvalidClassException异常:
Exception in thread "main" java.io.InvalidClassException: com.example.serializer.Student; local class incompatible: stream classdesc serialVersionUID = 8071311942157237447, local class serialVersionUID = -2800223051458730964 at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:621) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371) at com.example.serializer.Main.deserialize(Main.java:28) at com.example.serializer.Main.main(Main.java:8)
这地方提示的信息非常明确了:序列化前后的serialVersionUID号码不兼容!
从这地方最起码可以得出两个重要信息:
1、serialVersionUID是序列化前后的唯一标识符
2、默认如果没有人为显式定义过serialVersionUID,那编译器会为它自动声明一个!
第1个问题: serialVersionUID序列化ID,可以看成是序列化和反序列化过程中的“暗号”,在反序列化时,JVM会把字节流中的序列号ID和被序列化类中的序列号ID做比对,只有两者一致,才能重新反序列化,否则就会报异常来终止反序列化的过程。
第2个问题: 如果在定义一个可序列化的类时,没有人为显式地给它定义一个serialVersionUID的话,则Java运行时环境会根据该类的各方面信息自动地为它生成一个默认的serialVersionUID,一旦像上面一样更改了类的结构或者信息,则类的serialVersionUID也会跟着变化!
所以,为了serialVersionUID的确定性,写代码时还是建议,凡是implements Serializable的类,都最好人为显式地为它声明一个serialVersionUID明确值!
4. 两种特殊情况1、凡是被static修饰的字段是不会被序列化的
2、凡是被transient修饰符修饰的字段也是不会被序列化的
对于第一点,因为序列化保存的是对象的状态而非类的状态,所以会忽略static静态域也是理所应当的。
对于第二点,就需要了解一下transient修饰符的作用了。
如果在序列化某个类的对象时,就是不希望某个字段被序列化(比如这个字段存放的是隐私值,如:密码等),那这时就可以用transient修饰符来修饰该字段。
比如在之前定义的Student类中,加入一个密码字段,但是不希望序列化到txt文本,则可以:
@Data
public class Student implements Serializable {
private String name;
private Integer age;
private Integer score;
private transient String password;
}
这样在序列化Student类对象时,password字段会设置为默认值null,这一点可以从反序列化所得到的结果来看出:
序列化成功!已经生成student.txt文件 ============================================== 反序列化结果为: Student(name=linko, age=18, score=1000, password=null)
参考文章:https://mp.weixin.qq.com/s/0EfIUB9E-0Oh_Clwuxswuw



