天天看点

java cloneable 用途_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 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个数据。