Java中如何实现对象的拷贝
对象的拷贝
简介
对象的拷贝,把一个对象中的数据,复制到另一个相同类型的对象中,在某些情况下,这会很有用,例如,创建线程安全的实例中提到的一个规范,对象应该返回自身数据的拷贝,而不应该返回数据本身,避免外部对于数据的修改影响到对象本身,String类就是这么写的,它会对外返回char数组的拷贝,而不是char数组本身,这样,当用户修改char数组时,不会影响到String对象本身。
String类的toCharArray方法,返回内部char数组的拷贝
public char[] toCharArray() {// Cannot use Arrays.copyOf because of class initialization order issueschar result[] = new char[value.length];System.arraycopy(value, 0, result, 0, value.length);return result;
}
浅拷贝和深拷贝
对象的拷贝,依据类型的不同,可以分为浅拷贝和深拷贝,浅拷贝是引用复制,深拷贝是数据复制。
浅拷贝和深拷贝:
- 浅拷贝:将原对象中的所有字段复制到新对象中,如果字段是基本数据类型,则复制的是值,如果字段是引用数据类型,则复制的是指向引用的地址,此时,新对象和原对象之间存在共享数据,如果某个对象修改了这部分数据,另一个对象也会受影响
- 深拷贝:不仅将源对象中所有字段复制到新对象中,还会递归地复制字段引用的对象,在深拷贝结束后,原对象和新对象之间不存在任何共享数据,某个对象被修改,不会影响到另一个对象。
浅拷贝
Object.clone方法,浅拷贝通常是通过这个方法来实现,它是一个本地方法,位于Object类中,子类也可以重写它来实现深拷贝,但是通常不需要这么做,因为代价比较大,如果希望深拷贝是一项通用能力的话。
方法签名:protected native Object clone() throws CloneNotSupportedException;
clone方法的使用:
- clone()方法是protected的,这意味着只有子类和同一个包中的其他类可以调用它。
- 如果一个类没有实现Cloneable接口,尝试调用它的clone()方法将抛出CloneNotSupportedException。
- 当用需要克隆一个对象时,需要会在子类中重写clone()方法,并确保它是public的,这样外部代码才能调用它。
clone方法的底层原理:clone()方法创建了一个对象的浅拷贝。这意味着新对象和原始对象在内存中是两个独立的副本,但是它们中的引用字段指向相同的对象。
案例:
// Person类
public class Person implements Cloneable{private String name;private int age;@Overridepublic Person clone() throws CloneNotSupportedException {return (Person) super.clone();}// 省略构造方法、getter、setter、toString
}// PersonGroup类:持有Person类的引用,通过这个引用,就可以看出深拷贝和浅拷贝的区别
public class PersonGroup implements Cloneable {private String groupName;private Person person1;@Overridepublic PersonGroup clone() throws CloneNotSupportedException {return (PersonGroup) super.clone();}// 省略构造方法、getter、setter、toString
}// 测试
public class Test3 {public static void main(String[] args) throws CloneNotSupportedException {Person person = new Person("张三", 18);PersonGroup personGroup = new PersonGroup("aaa", person);PersonGroup clone = personGroup.clone();System.out.println("clone = " + clone);}
}
对测试类进行debug,观察克隆出的实例和原实例的内存地址,以及它们中成员变量的内存地址,可以得出结论:Object类中的clone方法,将原实例在堆内存中的数据复制了一份,到新的内存空间,再将新内存空间的地址赋值给clone后的对象,两块内存地址中如果有引用类型的数据,那么它们指向相同的内存空间
关于浅拷贝,有一个地方值得注意,在原对象中有引用类型的数据的情况下,浅拷贝出的新对象,如果修改了这个引用指向的对象,原对象也会受影响,如果是修改了引用本身的指向,那么新对象不受影响。
深拷贝
深拷贝有多种实现方式,包括基于IO流的深拷贝、基于json转换的深拷贝,核心原理都是把原对象和它引用的对象,全部复制一份,然后创建一个新对象,新对象的引用也是新对象。原对象和新对象中不存在任何共享对象。
方式1:使用IO流实现深拷贝
@SuppressWarnings("unchecked")
public static <T> T deepCopyByIO(T object) {if (object == null) {return null;}// ByteArrayOutputStream、ByteArrayInputStream,是内存流,即使不关闭也不会造成资源泄露,例如文件句柄未// 释放等问题,即使如此,随时关闭流依然是一个好习惯,而且关闭输出流会保证缓冲区的数据被正确的写出try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {objectOutputStream.writeObject(object);try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) {return (T) objectInputStream.readObject();}} catch (Exception e) {e.printStackTrace(); // 打印异常信息return null;}
}
apache提供的common-langs包中也提供了类似的实现,SerializationUtils.clone方法
2、使用json实现深拷贝
// 这里使用的是gson框架,
public static <T> T deepCopyByGson(T object) {if (object == null) {return null;}try {Gson gson = new Gson(); // 获取对象的类型Type type = object.getClass();// 将对象转换为 JSON 字符串String jsonString = gson.toJson(object);// 从 JSON 字符串解析回对象return gson.fromJson(jsonString, type);} catch (Exception e) {e.printStackTrace();return null;}
}
3、使用一些更高效的序列化框架,如hession、kryo等,都可以实现深拷贝。
深拷贝和循环引用
循环引用是指,两个对象互相持有对方的引用,这种情况下,即使是简单的toString方法,如果处理不当,也可能会栈内存溢出。
案例:循环引用
// 两个对象互相持有对方的引用
@Getter
@Setter
public static class Person1 implements Serializable {private String name;private Person2 person2;@Overridepublic String toString() {return "Person1{" +"name='" + name + '\'' +", person2=" + person2 +'}';}
}@Getter
@Setter
public static class Person2 implements Serializable {private String name;private Person1 person1;@Overridepublic String toString() {return "Person2{" +"name='" + name + '\'' +", person1=" + person1.toString() +'}';}
}// 测试
@Test
public void test4() {Person1 person1 = new Person1();Person2 person2 = new Person2();person1.setName("zs");person1.setPerson2(person2);person2.setName("ls");person2.setPerson1(person1);System.out.println("person1 = " + person1);
}
这个案例会报栈内存溢出,因为在打印对象时,调用了它的toString方法,打印对象1的时候发现需要打印对象2,打印对象2的时候发现需要打印对象1,造成循环调用,最终栈内存溢出。
所以在深拷贝时,如何解决循环引用的问题?
方式1:使用基于io流的方式,这种方法天然可以避免循环引用,因为对象序列化机制内置了对循环引用的检测和处理
方式2:忽略循环引用相关的字段,某些json框架可以手动配置,忽略某些字段。
案例:使用fastjson来解决循环引用的问题,这是fastjson内置的特性,在最终结果中,它会使用 “ref” 来代替循环引用
@Test
public void test4() {Person1 person1 = new Person1();Person2 person2 = new Person2();person1.setName("zs");person1.setPerson2(person2);person2.setName("ls");person2.setPerson1(person1);// 深拷贝String jsonString = JSON.toJSONString(person1, JSONWriter.Feature.ReferenceDetection);Person1 person3 = JSON.parseObject(jsonString, Person1.class);// {"name":"zs","person2":{"name":"ls","person1":{"$ref":"$"}}}System.out.println("jsonString = " + jsonString);
}
总结
这里介绍了复制一个对象的方法,包括浅拷贝、深拷贝,以及它们的基本使用