序列化原理
序列化就是把对象转化为可以持久化,可以网络传输字节数组,如果对象没有序列化,那么当进程停止的时候也就消失了
反序列化是序列化的逆向过程,将字节数组反序列化,恢复为原来对象
serialVersionUID 的作用
Java 的序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化
时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进
行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的
异常,即是 InvalidCastException。
因此,只要我们自己指定了 serialVersionUID,就可以在序列化后,去添加一
个字段,或者方法,而不会影响到后期的还原,还原后的对象照样可以使用,而且还多了方
法或者属性可以用
Transient 关键字
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变
量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是
0,对象型的是 null。
java 序列化的一些简单总结
- Java 序列化只是针对对象的状态进行保存,至于对象中的方法,序列化不关心
- 当一个父类实现了序列化,那么子类会自动实现序列化,不需要显示实现序列化接口
- 当一个对象的实例变量引用了其他对象,序列化这个对象的时候会自动把引用的对象也进
行序列化(实现深度克隆) - 当某个字段被申明为 transient 后,默认的序列化机制会忽略这个字段
- 被申明为 transient 的字段,如果需要序列化,可以添加两个私有方法:writeObject 和
readObject
常见的序列化技术:
-
xml 案例: Webservice,就是采用 XML 格式对数据进行序列化的
-
json FastJson GSON
-
hessian dubbo
-
protobuf
Protobuf 使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要
求高的 RPC 调用。 另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应
用在对象的持久化场景中
但是要使用 Protobuf 会相对来说麻烦些,因为他有自己的语法,有自己的编译器,如果需要
用到的话必须要去投入成本在这个技术的学习中 -
Avro 序列化 是一个数据序列化系统,设计用于支持大批量数据交换的应用
kyro 序列化框架 是一种非常成熟的序列化实现,已经在 Hive、Storm)中使用得比较广泛,不过它不能
跨语言. 目前 dubbo 已经在 2.6 版本支持 kyro 的序列化机制。它的性能要优于之前的
hessian2
package com.reflect.singleton;
import java.io.Serializable;
/**
* if (obj != null &&
* handles.lookupException(passHandle) == null &&
* desc.hasReadResolveMethod())
* {
* Object rep = desc.invokeReadResolve(obj);
* if (unshared && rep.getClass().isArray()) {
* rep = cloneArray(rep);
* }
* if (rep != obj) {
* handles.setObject(passHandle, obj = rep);
* }
* }
*
* hasReadResolveMethod:如果实现了serializable 或者 externalizable接口的类中包含readResolve则返回true
*
* invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve方法。
*
* 所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。
*/
public class TestProtoy implements Serializable {
private String name;
private static final long serialVersionUID = -5615139646023391991L;
// 再ObjectInputputStream 的readObject ,序列化会通过反射调用无参数的构造方法创建一个新的对象。
/**
*Object obj;
* try {
* obj = desc.isInstantiable() ? desc.newInstance() : null;
* } catch (Exception ex) {
* throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);
* }
*
* desc.newInstance() 这里最终也是通过构造方法反射出来一个对象
*/
// 解决加入readResolve
// 反序列时候
private volatile static TestProtoy testProtoy;
private TestProtoy(){}
public static TestProtoy getInstance(){
if(testProtoy != null){
return testProtoy;
}
synchronized (TestProtoy.class){
if(testProtoy != null){
return testProtoy;
}
return testProtoy = new TestProtoy();
}
}
public void setName(String name){
this.name = name;
}
public String getName() {
return name;
}
private Object readResolve() {
return getInstance();
}
}
double check 单例模式中,变量为什么定义成volatile?
比如定义一个person: Person p = new Person().
这里大概涉及到三个操作。
第一:new person 在内存开辟空间。
第二:调用person构造方法
第三:将new person的引用复制给变量p.
由于jvm的内部优化,执行过程可能会指令重排。 也就是说,将变量的赋值操作和对象的构造方法操作,可能会重排序,那么double check在并发的时候,可能拿到的一个空的对象,也就是没有属性值的对象。
所以可以使用volatile,禁止指令重排序,相当于是加了一个内存屏障,数据每次读都是从主存中读,写会更新到主存,刷新缓存。
但是volatile不能保证原子,类似变量的自增操作这种,不是一个原子操作。只能保证每次读的都是最新数据,不会说存在线程A更新了,线程B却读的老的。
枚举
package com.reflect.singleton;
import java.io.Serializable;
import java.util.Date;
public enum Way implements Serializable {
INSTANCE(0,0),
up(1,2),
down(3,3);
private static final long serialVersionUID = -5615139646023391991L;
private Date date;
private String name;
private int code;
private int desc;
Way(int code, int desc) {
this.code = code;
this.desc = desc;
}
private Way() {
}
public void dosomething(String name){
System.out.println(this.hashCode()+" "+name +"说:"+ this.getCode()+" "+this.getDesc());
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public void setDesc(int desc) {
this.desc = desc;
}
public int getDesc() {
return desc;
}
}
test
package com.reflect.singleton;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class Test2 {
public static void main(String[] args) throws Exception {
//
// System.out.println(Way.up.getCode());
// Way.up.setCode(99);
// Way.up.setName("sfafa");
//
// System.out.println(Way.up.getCode());
//
// System.out.println(Way.up.getName());
//
// Way.up.dosomething("小王");
//
// Way way = Way.up;
// System.out.println(way.hashCode());
//
// ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("e:/way"));
// objectOutputStream.writeObject(way);
// objectOutputStream.flush();
// objectOutputStream.close();
//
// ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("e:/way"));
//
// Way way1 = (Way)objectInputStream.readObject();
//
// System.out.println("way1==:"+way1.hashCode());
// // 检验枚举反射是否破坏单例
// Way way = Way.up;
// System.out.println(way.hashCode());
// // 获取构造方法
// Constructor<Way> constructor = Way.class.getDeclaredConstructor(String.class,int.class);
// constructor.setAccessible(true);
// Way way1 = constructor.newInstance(); // 这里会直接报错,因为进入这个方法可以发现
// /**
// * if ((clazz.getModifiers() & Modifier.ENUM) != 0)
// * throw new IllegalArgumentException("Cannot reflectively create enum objects");
// *
// * 会检查这个类是否被ENUM修饰,如果是则抛出异常,反射失败
// */
// System.out.println(way1.hashCode());
//
// double check 构造方法破坏单例
// Constructor<TestProtoy> constructor = TestProtoy.class.getDeclaredConstructor();
// constructor.setAccessible(true);
// TestProtoy testProtoy = constructor.newInstance();
// System.out.println(testProtoy.hashCode());
// System.out.println(TestProtoy.getInstance().hashCode());
//
// TestProtoy testProtoy = TestProtoy.getInstance();
// testProtoy.setName("发发发");
// System.out.println(testProtoy.hashCode() + " " + testProtoy.getName());
//
//
// ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("e:/testProtoy"));
// objectOutputStream.writeObject(testProtoy);
// objectOutputStream.flush();
// objectOutputStream.close();
//
//
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("e:/testProtoy"));
//
TestProtoy sfaga = (TestProtoy)objectInputStream.readObject();
// System.out.println("sfaga==:"+sfaga.hashCode() + " "+ sfaga.getName());
}
}
内部类
public class InnerClassLazySingleton {
// 构造方法私有化
private InnerClassLazySingleton() {
//作用防止通过反射破坏单例
// 如果不加入这个判断,则可以通过反射机制,获取私有的构造方法,暴力破坏单例
if (LazyInnerClass.LAZY != null) {
throw new RuntimeException("不允许创建多个实例");
}
}
// //默认使用 LazyInnerClassGeneral 的时候,会先初始化内部类
// 如果没使用的话,内部类是不加载的
// static 是为了使单例的空间共享
public static InnerClassLazySingleton getInstance() {
// //在返回结果以前,一定会先加载内部类
return LazyInnerClass.LAZY;
}
//默认不加载
private static class LazyInnerClass {
public static final InnerClassLazySingleton LAZY = new InnerClassLazySingleton();
}
}
Enum支持序列化吗
支持,在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。
Enum底层实现
当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承,这个类中有几个属性和方法都是static类型的,因为static类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。
一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。原文参考深度分析 Java 的枚举类型