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个数据。

posted @ 2020-03-22 22:55  少年小白  阅读(427)  评论(0编辑  收藏  举报