cloneable和Serializable的应用(java深复制、浅复制)
普通类
//普通类
class Clazz {
int myId;
...
}
//测试
public static void main(String[] args) throws Exception {
Clazz zz = new Clazz();
zz.setMyId(1);
System.out.println("原始id: " + zz.getMyId());
//添加新引用
Clazz clazz = zz;
System.out.println("新引用的id: " + clazz.getMyId());
//改变原类的属性
zz.setMyId(2);
System.out.println("原始id: " + zz.getMyId() + "; 新引用的id: " + clazz.getMyId());
}
想必大家都很熟悉,我们拥有指向同一个对象的两个引用,通过任何一个引用改变对象的内容,对于另外的引用都即时可见。
但是当我们想要复制一份该怎么办呢?于是就有了下边的接口
Cloneable类
//克隆类
class CloneClass implements Cloneable {
int myId;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
...
}
//测试
public static void main(String[] args) throws Exception {
CloneClass cc = new CloneClass();
cc.setMyId(1);
System.out.println("原始id: " + cc.getMyId());
//克隆
CloneClass cloneClass = (CloneClass) cc.clone();
System.out.println("克隆后的id: " + cloneClass.getMyId());
//改变原类的属性
cc.setMyId(2);
System.out.println("原始id: " + cc.getMyId() + "; 克隆后的id: " + cloneClass.getMyId());
}
//结果
原始id: 1
克隆后的id: 1
原始id: 2; 克隆后的id: 1
改变原始类的id后,复制的类的信息保持不变,说明拷贝的类是一个不同的类,也就达到了复制一份的目的。
我要想克隆类,就要调用clone方法,由于Object类的clone方法是protect的,所以要想我们的类在所有包中都能够克隆,就要将其实现为public的。有clone方法后,如果我们不实现Cloneable接口,编译就会报不能克隆的错。所以重写方法和实现接口必须共存。
关于protect修饰符,我们知道可以子类调用,但同时还有一个条件是,只能在子类所在的包中调用,在其他地方调不到。
但是如果类里面有对象的话,就存在问题了
//类内引用的类
class User {
int id;
String name;
...
}
//克隆类
class CloneClass implements Cloneable {
int myId;
User user;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
...
}
//测试
public static void main(String[] args) throws Exception {
CloneClass cc = new CloneClass();
User user = new User(100, "张三");
cc.setUser(user);
System.out.println("原始User: " + cc.getUser());
//克隆
CloneClass cloneClass = (CloneClass) cc.clone();
System.out.println("克隆后的User: " + cloneClass.getUser());
//改变原类引用的类的属性
cc.getUser().setName("赵二麻子");
System.out.println("原始User: " + cc.getUser() + "; 克隆后的User: " + cloneClass.getUser());
}
//结果
原始User: 张三
克隆后的User: 张三
原始User: 赵二麻子; 克隆后的User: 赵二麻子
通过clone一个类,类内有引用类的时候,并不会重新拷贝一份(仅仅是复制了一个引用),指向的还是原类内引用指向的对象。所以改变原类的属性,对于克隆的类的引用即时可见。这就是所谓的浅拷贝。
可以用如下的方法进行克服。
//类内引用的类, 让其实现Cloneable
class User implements Cloneable {
int id;
String name;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
...
}
//改变克隆类的clone方法
class CloneClass implements Cloneable {
User user;
@Override
protected Object clone() throws CloneNotSupportedException {
CloneClass cloneClass = (CloneClass) super.clone();
cloneClass.user = (User) cloneClass.user.clone();
return cloneClass;
}
...
}
//测试,同上
//结果
原始User: 张三
克隆后的User: 张三
原始User: 赵二麻子; 克隆后的User: 张三
内部引用对象的复制问题是解决了,但是类里每次新引用一个类,就得在clone里增加一段代码,是不是很繁琐,假如这个类很多类里都用到了呢,是不是一场灾难。
Serializable接口将其操作简化了。
Serializable类
先引入一个工具类,不用看具体内容,作用是给一个类(Serializable类),返回这个类的副本
abstract class BeanUtil {
@SuppressWarnings("unchecked")
public static <T> T cloneTo(T src) throws RuntimeException {
ByteArrayOutputStream memoryBuffer = new ByteArrayOutputStream();
ObjectOutputStream out = null;
ObjectInputStream in = null;
T dist = null;
try {
out = new ObjectOutputStream(memoryBuffer);
out.writeObject(src);
out.flush();
in = new ObjectInputStream(new ByteArrayInputStream(memoryBuffer.toByteArray()));
dist = (T) in.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (out != null)
try {
out.close();
out = null;
} catch (IOException e) {
throw new RuntimeException(e);
}
if (in != null)
try {
in.close();
in = null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return dist;
}
}
Serializable类
//类内引用的类, 让其实现Serializable类
class User implements Serializable {
int id;
String name;
...
}
//序列化类
class SerializClass implements Serializable {
User user;
...
}
//测试
public static void main(String[] args) throws Exception {
//复制类
SerializClass sc = new SerializClass();
User user = new User(100, "张三");
sc.setUser(user);
System.out.println("原始User: " + sc.getUser());
SerializClass serializClass = BeanUtil.cloneTo(sc);
System.out.println("克隆后的User: " + serializClass.getUser());
sc.getUser().setName("赵二麻子");
System.out.println("原始User: " + sc.getUser() + "; 克隆后的User: " + serializClass.getUser());
}
//结果
原始User: 张三
克隆后的User: 张三
原始User: 赵二麻子; 克隆后的User: 张三
达到了复制一份的目的,同时代码是不是也清爽了很多(工具类是复杂了点,但是只写一份就够了)。
这种复制的方法,连同上边比较复杂的一种clone,都成为深拷贝。
但这么好的方法也有失效的时候。
//序列化类,有transient修饰符的字段
class SerializClass implements Serializable {
transient int i;
...
}
//测试
public static void main(String[] args) throws Exception {
//复制类
SerializClass sc = new SerializClass();
sc.setI(1);
System.out.println("原始i: " + sc.getI());
SerializClass serializClass = BeanUtil.cloneTo(sc);
System.out.println("克隆后的i: " + serializClass.getI());
sc.setI(2);
System.out.println("原始i: " + sc.getI() + "; 克隆后的i: " + serializClass.getI());
}
//结果
原始i: 1
克隆后的i: 0
原始i: 2; 克隆后的i: 0
也就是这种序列化会跳过带有transient修饰符的字段(该修饰符只能修饰字段,不能修饰方法),那么还能有什么方法可以复制吗?答案是有
//序列化类
class SerializPlusClass implements Serializable {
transient int i;
private void writeObject(java.io.ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeInt(i);
}
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
i = s.readInt();
}
...
}
//测试
public static void main(String[] args) throws Exception {
//复制类
SerializPlusClass sc = new SerializPlusClass();
sc.setI(1);
System.out.println("原始i: " + sc.getI());
SerializPlusClass serializClass = BeanUtil.cloneTo(sc);
System.out.println("克隆后的i: " + serializClass.getI());
sc.setI(2);
System.out.println("原始i: " + sc.getI() + "; 克隆后的i: " + serializClass.getI());
}
//结果
原始i: 1
克隆后的i: 1
原始i: 2; 克隆后的i: 1
可以看到我们实现了writeObject和readObject方法,但这个方法既不是Object的方法,也不是Serializable的方法,那为什么这样就可以呢?
我们可以回头看看那个工具类,在序列化的时候我们用到了ObjectInputStream和ObjectOutputStream类,就是说在序列化和反序列化的时候,这两个类调用了我们写的方法。从而用这种方法也可以自定序列化内容,包括transient修饰的字段。
由于序列化对static修饰的字段也无效,所以也可以在这里实现。由于实验相对麻烦,这里没有给出。如果有兴趣可以序列化包含静态变量的类到文件,然后重启虚拟机(重新运行),执行反序列化。
因为静态变量属于类,所以同一个虚拟机反序列化后还是原来的值,并不是反序列化出来的。
后话
我们知道transient修饰符,就是为了标识出不需要反序列化的字段,那为什么我们还要费尽心思来序列化它呢?
考虑这样一种情况,有一个长度为100的list,里面只有一个有值,其他的都是null,如果全序列化出来,岂不是很浪费空间。所以通过这种方法有选择地序列化。具体实例参见ArrayList的实现,只序列化elementData的size个数据。