首页 > 编程语言 >如何复制一个java对象(浅克隆与深度克隆)

如何复制一个java对象(浅克隆与深度克隆)

时间:2023-02-17 15:06:58浏览次数:32  
标签:java 克隆 Person arm person 复制 anotherPerson public Arm


在项目中,有时候有一些比较重要的对象经常被当作参数传来传去,和C语言的值传递不同,java语言的传递都是引用传递,在任何一个地方修改了这个对象的值,就会导致这个对象在内存中的值被彻底改变。但是很多时候我们并不想去真正的改变这个对象,只是使用它的某些属性,却因为不小心改变后忘记了恢复,或者被团队中不知情的别人给改变了。这样的话,后果将是不可预料的,可能会花上很久也发现不了自己的对象在哪被改了,尤其在大型项目中,很多人都在操作同一个对象,一旦有人在对象的主人不知情的情况下,修改了这个对象的值,那么很有可能在系统上线时也发现不了这个隐藏的bug。举个小例子,我定义了一个Person对象,里面有个age属性,然后有人在我不知道的情况下,想看看我的age加上10后是多少,那么他在自己也不知道后果的情况下执行了person.age+=10,后来,我在任何使用age的地方,都发现age值被修改了,并且不知道在哪被谁修改的。
    事实情况中,要比例子上严重的多,有一些复杂的对象的某些属性值被改变后很难被注意到,那么这些都是系统的极大隐患。我们有一些对象是压根不想让别人去修改的,只想让别人去看看,别人的任何操作都不应该改变这个对象原本的值。当然我们可以采取优秀的封装来实现属性的隐藏,但很多情况下我们不得不公开一些改变对象属性的方法,那么如果想完全的封装自己的对象,我们可以采用克隆一份完全一样的对象。然后把这个克隆出来的对象公开给别人访问,这样保证了目标对象的封装和它的不可改变。那么怎么去克隆一个对象呢?
    首先举一个简单的对象克隆例子,有一个Person对象,它有三个属性:

public class Person {



private int age;

private String name;

private String sex;



public int getAge() {

return age;

}

public void setAge(int age) {

this.age = age;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getSex() {

return sex;

}

public void setSex(String sex) {

this.sex = sex;

}



}

然后定义一个它的对象

public class Test {



public static void main(String[] args) {

Person person = new Person();

person.setAge(10);

person.setName("wolf");

person.setSex("man");

}

}


现在我们拥有了一个person对象了,它具备上面的几个属性。该怎样去创建另一个和它所有属性一模一样的Person对象呢?
在我的经历中,碰到过很多人是这样做的
Person anotherPerson = new Person();
anotherPerson = person;
他们认为new了一个新的Person,然后将已经有值的person赋给这个new出来的Person就ok了,这样内存中就有两个互不干扰的Person对象了。对此我只能说,你去修改一下anotherPerson的值,看看person的值是否跟着变了。具体为什么这种做法是错的,我就不提了,我只说对的。那就是下面这种写法:

public class Test {



public static void main(String[] args) {

Person person = new Person();

person.setAge(10);

person.setName("wolf");

person.setSex("man");



Person anotherPerson = new Person();

anotherPerson.setAge(person.getAge());

anotherPerson.setName(person.getName());

anotherPerson.setSex(person.getSex());



System.out.println(anotherPerson.getAge());

System.out.println(anotherPerson.getName());

System.out.println(anotherPerson.getSex());



anotherPerson.setAge(20);

System.out.println("修改值后:");

System.out.println(anotherPerson.getAge());

System.out.println(person.getAge());

}

}


我们看到一个新的Person对象anotherPerson完全具备了person的所有属性,并且修改anotherPerson后,person对象完全不受影响。这样anotherPerson就是person的一个完美克隆。
   下面我们的Person对象变复杂了,里面拥有了很多的属性,像下面这样:

public class Person {



private int age;

private String name;

private String sex;

private List<String> hobbies = new ArrayList<String>();

private Set<String> friends = new HashSet<String>();

private Set<String> homes = new HashSet<String>();



public int getAge() {

return age;

}

public void setAge(int age) {

this.age = age;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getSex() {

return sex;

}

public void setSex(String sex) {

this.sex = sex;

}

public List<String> getHobbies() {

return hobbies;

}

public void setHobbies(List<String> hobbies) {

this.hobbies = hobbies;

}

public Set<String> getFriends() {

return friends;

}

public void setFriends(Set<String> friends) {

this.friends = friends;

}

public Set<String> getHomes() {

return homes;

}

public void setHomes(Set<String> homes) {

this.homes = homes;

}



}


我们的对象变成了一个比较复杂的对象,里面有很多基础集合属性,现在如果我们还想通过上面的方法来克隆这个对象时会发现,工作量非常大,我们需要遍历对象的所有集合属性,然后再一个个的添加到新的anotherPerson中,这将会增加很多代码,而且很枯燥。那么我们该怎么去克隆这个对象呢?
这就需要用到clone方法
我们让Person类实现Cloneable接口,然后覆盖clone方法,代码如下

public class Person implements Cloneable {



private int age;

private String name;

private String sex;

private List<String> hobbies = new ArrayList<String>();

private Set<String> friends = new HashSet<String>();

private Set<String> homes = new HashSet<String>();



@Override

public Person clone() {

Person person = null;

try {

person = (Person) super.clone();

} catch (CloneNotSupportedException e) {

e.printStackTrace();

}

return person;

}



public int getAge() {

return age;

}

public void setAge(int age) {

this.age = age;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getSex() {

return sex;

}

public void setSex(String sex) {

this.sex = sex;

}

public List<String> getHobbies() {

return hobbies;

}

public void setHobbies(List<String> hobbies) {

this.hobbies = hobbies;

}

public Set<String> getFriends() {

return friends;

}

public void setFriends(Set<String> friends) {

this.friends = friends;

}

public Set<String> getHomes() {

return homes;

}

public void setHomes(Set<String> homes) {

this.homes = homes;

}



}


这里实现了clone方法,并且返回了一个Person对象,现在我们检验这个方法是否创建了一个完全相同的Person对象给我们:

public class Test {



public static void main(String[] args) {

Person person = new Person();

person.setAge(10);

person.setName("wolf");

person.setSex("man");



Person anotherPerson = person.clone();



System.out.println(anotherPerson.getAge());

System.out.println(anotherPerson.getName());

System.out.println(anotherPerson.getSex());



anotherPerson.setAge(20);

System.out.println("修改值后:");

System.out.println(anotherPerson.getAge());

System.out.println(person.getAge());

}

}


输出结果为
10
wolf
man
修改值后:
20
10


大家看到了,当修改克隆出来的person对象时,原来的person对象并没有被修改,这说明person和anotherPerson是完全不同的两个对象,说明我们成功的复制出来了一个新的和原来的对象各属性相同的对象。但是事实真的是这样吗?他们的所有属性真的完全相同吗?修改其中一个的时候另一个真的会完全不受影响吗?
现在我再定义一个Person

public class Person implements Cloneable {



private Arm arm;



@Override

public Person clone() {

Person person = null;

try {

person = (Person) super.clone();

} catch (CloneNotSupportedException e) {

e.printStackTrace();

}

return person;

}





public Arm getArm() {

return arm;

}



public void setArm(Arm arm) {

this.arm = arm;

}



}


由于我们已经验证过基础属性的克隆,现在我们验证一下别的情况,现在的Person对象里有了一个别的对象Arm,Arm类的实现如下:

public class Arm {

private String armName;



public String getArmName() {

return armName;

}



public void setArmName(String armName) {

this.armName = armName;

}



}


Arm也很简单,只有一个属性。下面验证一下Person的克隆情况。

public class Test {



public static void main(String[] args) {

Person person = new Person();



Arm arm = new Arm();

arm.setArmName("person arm");

person.setArm(arm);



Person anotherPerson = person.clone();



System.out.println(anotherPerson.getArm().getArmName());



anotherPerson.getArm().setArmName("anotherPerson arm");

System.out.println("修改值后:");

System.out.println(person.getArm().getArmName());



System.out.println(anotherPerson.getArm());

System.out.println(person.getArm());

}

}


输出结果如下:
person arm
修改值后:
anotherPerson arm
xiao.a.Arm@119dc16
xiao.a.Arm@119dc16


我们看到当修改克隆出来的anotherPerson的Arm属性时,原来的person对象的arm也被修改,接着我们打印了它们两个各自的arm的内存地址,发现它们居然用的是同一个Arm,这说明当clone一个对象时,只clone了里面的某些属性,对象里面的对象并没有被克隆出来。我们的目标并没有达成,那么该怎么连深层次的属性也复制出来呢?
可能有的人已经想明白了,如果我把Arm也写一个clone方法不就行了吗?现在来验证一下。
修改后的Arm如下:

public class Arm implements Cloneable {

private String armName;



public String getArmName() {

return armName;

}



public void setArmName(String armName) {

this.armName = armName;

}



public Arm clone() {

Arm arm = null;

try {

arm = (Arm)super.clone();

} catch (CloneNotSupportedException e) {

e.printStackTrace();

}

return arm;

}

}




然后修改Person类,

public class Person implements Cloneable {



private Arm arm;



@Override

public Person clone() {

Person person = null;

try {

person = (Person) super.clone();

} catch (CloneNotSupportedException e) {

e.printStackTrace();

}

person.setArm(this.arm.clone());

return person;

}





public Arm getArm() {

return arm;

}



public void setArm(Arm arm) {

this.arm = arm;

}



}


下面再来验证一下:

public class Test {



public static void main(String[] args) {

Person person = new Person();



Arm arm = new Arm();

arm.setArmName("person arm");

person.setArm(arm);



Person anotherPerson = person.clone();



System.out.println(anotherPerson.getArm().getArmName());



anotherPerson.getArm().setArmName("anotherPerson arm");

System.out.println("修改值后:");

System.out.println(person.getArm().getArmName());

System.out.println(anotherPerson.getArm().getArmName());



System.out.println(anotherPerson.getArm());

System.out.println(person.getArm());

}

}


结果如下:
person arm
修改值后:
person arm
anotherPerson arm
xiao.a.Arm@119dc16
xiao.a.Arm@c05d3b


很明显,我们修改anotherPerson的Arm属性时,并没有影响到person的Arm属性,它们各自的Arm也对应不同的内存地址,这说明连Arm属性也已经成功克隆出来了,这已经达到了我们的目标,完全的复制一个对象出来,不管这个对象里是否还有别的对象。
到这个时候,大家估计要明白了,我又要加大难度了。
刚才的情况是很简单的情况,要克隆的对象里只嵌套了一层对象,我们只需要对嵌套的对象也实现clone方法即可。事实情况下,一个类可能会很复杂,譬如一个Person类,类下面有一个Address对象,而Address类呢又嵌套了一层Room对象,Room类下面还有一堆属性……然后Person类下面和Address平级的又有Arm对象,Leg对象等等一大堆属性,此时可能我们头都大了,难不成要给所有的类都实现clone方法,才能真正的克隆出一个Person对象出来?
答案当然是否定的,当然是有简单的方法来完成这个工作。
java有一个接口不太常用,可能很多人不知道这个接口是用来做什么的,下面就谈一下这个接口Serializable--序列化。
通俗一点说就是:序列化可以将一个对象物理化为一个文件,反序列化可以将这个文件还原为一个对象。下面来看新的Person类:

public class Person implements Serializable {



private static final long serialVersionUID = 1L;



private Arm arm;



/**

* 深度克隆

*/

public Object deepClone() throws IOException, OptionalDataException,

ClassNotFoundException {

// 将对象写到流里

ByteArrayOutputStream bo = new ByteArrayOutputStream();

ObjectOutputStream oo = new ObjectOutputStream(bo);

oo.writeObject(this);

// 从流里读出来

ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());

ObjectInputStream oi = new ObjectInputStream(bi);

return (oi.readObject());

}

public Arm getArm() {

return arm;

}



public void setArm(Arm arm) {

this.arm = arm;

}



}


这里有一个deepClone方法,该方法将对象序列化后再反序列化,然后得到了一个新的Person对象,下面来验证一下这个新的对象是否是完全的克隆。我们将Arm类中的clone方法删除,仅保留armName属性,然后也实现Serializable接口即可。

public class Arm implements Serializable {

private static final long serialVersionUID = 1L;



private String armName;



public String getArmName() {

return armName;

}



public void setArmName(String armName) {

this.armName = armName;

}





}


测试类如下:

public class Test {



public static void main(String[] args) throws OptionalDataException, ClassNotFoundException, IOException {

Person person = new Person();



Arm arm = new Arm();

arm.setArmName("person arm");

person.setArm(arm);



Person anotherPerson = (Person) person.deepClone();



System.out.println(anotherPerson.getArm().getArmName());



anotherPerson.getArm().setArmName("anotherPerson arm");

System.out.println("修改值后:");

System.out.println(person.getArm().getArmName());

System.out.println(anotherPerson.getArm().getArmName());



System.out.println(anotherPerson.getArm());

System.out.println(person.getArm());

}

}


结果如下:
person arm
修改值后:
person arm
anotherPerson arm
xiao.a.Arm@785d65
xiao.a.Arm@1afae45


我们看到经过deepClone方法的Person对象,完全复制出了一份新的Person对象,修改新的anotherPerson完全影响不到原来的person,这说明我们的deepClone方法完全的复制出了一个新的person。
这种序列化后反序列化的克隆方法,非常的简单,不需要再一一赋值,也不需要去一个个的实现clone方法,就可以完整的克隆出新对象,实在是大大有利于我们的开发工作。不管嵌套了多少层,都可以一句话搞定,实在是杀人越货之必备利器。
那么这种做法有什么弊端呢?
很显然,这个对象必须得能序列化,并且实现序列化接口。哪些东西不可以序列化呢?这里需要记住的一点就是静态的(static)不能序列化。因为我们序列化的是一个对象,所有的属性都是对象属性,而static的是类属性,类属性会在jvm首先被加载,它不属于对象属性,当然不能被序列化。
有了上面的deepClone方法,相信已经能解决绝大部分项目中碰到的克隆对象的问题。但是新的问题又出现了,举个例子,譬如Person类中有两个对象,一个是Arm,一个是Address,Address类大家自行发挥去创建。

public class Person implements Serializable {



private static final long serialVersionUID = 1L;



private Arm arm;

private Address address;



public Arm getArm() {

return arm;

}



public void setArm(Arm arm) {

this.arm = arm;

}



public Address getAddress() {

return address;

}



public void setAddress(Address address) {

this.address = address;

}



}


现在我们的需求是,克隆一个Person对象,但是不要它的Address属性,仅克隆除Address外的所有属性,这该怎么办呢?
方法依旧是很简单,我们在不想被序列化的属性上加一个关键字transient
如 private transient Address address;只需这样就可以了,当序列化时系统会自动跳过带transient关键字的属性,deepClone之后,该属性就为null,自然不会被克隆出来了。具体的大家可以自行实验。


该文章详细介绍了对象的克隆,在大型项目开发中会很常用的一个功能点。

标签:java,克隆,Person,arm,person,复制,anotherPerson,public,Arm
From: https://blog.51cto.com/u_13706148/6064098

相关文章

  • java异常信息打印
    如果你不仅想在日志中查看异常信息,也想把异常信息保存起来作为数据查看,那么你可以这样做 publicvoidprintException(Exceptione){ByteArrayOutputStr......
  • java的历史 230217
    Java是由SunMicrosystems公司于1995年5月推出的高级程序设计语言。它由JamesGosling和其他Sun工程师们一起研发,主要服务于消费电子产品,如智能手机、平板电脑、家用电器等......
  • Java-webshell 排查
    参考:https://javasec.org/javaweb/MemoryShell/https://goodapple.top/archives/1355简介本次分享为javawebshell排查初级。抛砖引玉java获取web权限的shell......
  • JavaScript常见问题梳理
    1、this指向1、全局函数this指向全局对象window,注意严格模式下,this为undefined//[objectWindow]alert(this);functionf(){alert(this)}f()//undefinedfu......
  • java 内存锁
    importlombok.extern.slf4j.Slf4j;importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.locks.Lock;importjava.util.con......
  • Java: RocketMQ事务消息的优雅使用
    背景在项目中,技术方案需要使用事务消息来保证最终一致性达到实现业务的目的。但在一个服务中有多个业务需要使用事务消息发送不同的消息类型到不同的Topic时,RocketMQ的本......
  • java二维数组
    1.查找1)顺序查找SeqSearch.java2)二分查找【二分法,放在算法讲解】2.顺序查找有一个数列:白眉鹰王、金毛狮王、紫衫龙王、青翼蝠王猜数游戏:从键盘中任意输入一个名称,判......
  • javascript的一些基础知识
    随手记录一些javascript的一些基础知识,之前只是简单用到javascript,并没有了解其中的概念。1. JavascriptObject:InJavaScript,almost"everything"isanobject.......
  • java基础巩固-详解泛型
    java泛型(generics)为jdk5引入的新特性,泛型提供了编译时类型安全检测机制,可以在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参......
  • JAVA 学习笔记(五)
      子类通过方法的重写机制可以隐藏继承父类的方法,把父类的状态和行为改变为子类自己的状态和行为.假如父类中有一个方法myMethod(),一旦子类重写了超类的方法myMethod......