4.4 对象复制
前一章已经讨论过值类型和引用类型,当使用赋值运算符(=)或通过方法的参数传递时,它们的默认处理方式是不同的。其中,值类型会创建数据的副本(深复制),引用类型会传递对象的引用地址(浅复制),String类型则由于其自身的特殊性而与其他引用类型的表现有所不同。
如果代码中需要完全复制一个对象,即实现对象的深复制,可以使用两种方法来实现,分别是让对象的类型实现Cloneable接口或Serializable接口。首先看Cloneable接口的使用。
4.4.1 实现Cloneable接口
实现Cloneable接口的类只需要实现clone()方法,其返回值为Object类型。例如,下面的代码(CTank.java文件)创建CTank类。
在clone()方法中,首先创建一个新的CTank实例,然后将当前实例的各个字段的数据赋值给新的实例,最后返回新的实例对象。
下面的代码测试赋值运算符(=)和clone()方法复制对象的不同。
public static void main(String[] args) { CTank t1 = new CTank(); t1.model = "85"; CTank t2 = t1; t2.model = "99"; CTank t3 = (CTank)t1.clone(); t3.model = "99A"; // System.out.println(t1.model); System.out.println(t2.model); System.out.println(t3.model); }
图4-2 实现Cloneable接口以复制对象
代码执行结果如图4-2所示。
示例中,t1对象通过new CTank()代码创建,并设置model字段为85。然后,使用赋值运算符将其引用赋值给t2对象,此时,t2和t1引用了同一对象体(可以视为内存中某个区域)。接着,重新设置t2对象的model字段,而t1对象的数据同样也会改变,前两个输出结果验证了这一点。
对于t3对象,使用t1.clone()方法进行复制,这样,t3和t1实际上是完全不同的两个对象。修改t3对象的model字段时,并不会改变t1对象的数据,代码的输出结果验证了这一点。
4.4.2 实现Serializable接口
Serializable接口定义在java.io包中,使用时需要导入。下面的代码(CTank.java文件)修改CTank类的定义。
实现Serializable接口时并不需要实现什么成员,它只是告诉编译器,此类型的对象是可序列化(也称为可串行化)的。
实际应用中,序列化操作包括两个方向,即对象的输出与输入。接下来会使用java.io包中的一系列输入(input)和输出(output)类型来完成序列化和反序列化操作。
下面的代码使用序列化来复制CTank对象。
图4-3 使用序列化复制对象
代码输出结果如图4-3所示。
序列化操作(输出)中,主要使用了ByteArray OutputStream类和ObjectOutputStream类。其中,使用ObjectOutputStream对象的writeObject()方法将t1对象写入ByteArrayOut putStream对象,然后使用close()方法关闭ObjectOutputStream对象。
反序列化操作(输入)中,使用ByteArrayInputStream类和ObjectInputStream类。其中,使用ObjectInputStream对象中的readObject()方法从ByteArrayOutputStream对象中读取字节数组数据,强制转换为CTank对象并赋值给t2对象。
代码的最后,先输出t1对象的model和weapon字段,然后。修改t2对象的weapon字段。通过观察输出信息可以看到,t2对象完成了对t1对象的完全复制,修改t2对象的weapon字段时并不会影响t1对象。
关于序列化,需要说明的一点是,如果类型的字段不需要或不允许序列化时,可以使用transient关键字定义。例如,引用外部资源的对象,只能在恢复对象以后重新打开,无法通过序列化来保持资源的连接状态。