龙空技术网

Java 拷贝,你能说出个 123 么?

CSDN 340

前言:

现时小伙伴们对“java深拷贝和浅拷贝”大概比较关怀,大家都想要剖析一些“java深拷贝和浅拷贝”的相关文章。那么小编在网上搜集了一些有关“java深拷贝和浅拷贝””的相关知识,希望朋友们能喜欢,同学们一起来了解一下吧!

作者 | sowhat1412 责编 | 张文

头图 | CSDN 下载自视觉中国

来源 | sowhat1412(ID:sowhat9094)

本文主要讲解:对象创建方式、Java中的深拷贝和浅拷贝。

创建对象的5种方式

1.1 通过 new 关键字

这是最常用的一种方式,通过 new 关键字调用类的有参或无参构造方法来创建对象。比如 Object obj = new Object。

1.2 通过 Class 类的 newInstance 方法

这种默认是调用类的无参构造方法创建对象。比如:

Person p2 = (Person) Class. forName("com.ys.test. Person"). newInstance;

1.3 通过 Constructor 类的 newInstance 方法

这和第二种方法类时,都是通过反射来实现。通过 java.lang.relect.Constructor类的 newInstance 方法指定某个构造器来创建对象。实际上第二种方法利用 Class 的 newInstance 方法创建对象,其内部调用还是 Constructor 的 newInstance 方法。

Person p3 = (Person) Person.class.getConstructors[0].newInstance;

1.4 利用 Clone 方法

Clone 是 Object 类中的一个方法,通过对象A.clone 方法会创建一个内容和对象 A 一模一样的对象 B,clone 克隆,顾名思义就是创建一个一模一样的对象出来。

Person p4 = (Person) p3.clone;

1.5 序列化

序列化是把堆内存中的 Java 对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。而反序列化则是把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。

Java 基本复制方法

java赋值是复制对象引用,如果我们想要得到一个对象的==副本==,使用赋值操作是无法达到目的的:修改新对象的值会同时修改旧对象的值。

public class Client{ public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(15, "sowhat", new Address("河北", "建华南大街")); Person p1 = person; p1.setAge(45); System.out.println(p1.hashCode); System.out.println(person.hashCode); System.out.println("================"); System.out.println(p1.display); System.out.println(person.display); }}

Clone 方法

如果创建一个对象的新的副本,也就是说他们的初始状态完全一样,但以后可以改变各自的状态,而互不影响,就需要用到java中对象的复制,如原生的clone方法。本次讲解的是 Java 的深拷贝和浅拷贝,其实现方式正是通过调用 Object 类的 clone 方法来完成。在 Object.class 类中,源码为:

/** * ... * performs a "shallow copy" of this object, not a "deep copy" operation. * 上面这里已经说明了,clone方法是浅拷贝,而不是深拷贝 * @see java.lang.Cloneable */ protected native Object clone throws CloneNotSupportedException;

这是一个用 native 关键字修饰的方法。关于native关键字有一篇博客专门有介绍。不理解也没关系,只需要知道用 native 修饰的方法就是告诉操作系统。这个方法我不实现了,让操作系统去实现(参考JNI)。具体怎么实现我们不需要了解,只需要知道 clone方法的作用就是复制对象,产生一个新的对象。那么这个新的对象和原对象是==什么关系呢==?

基本类型和引用类型

这里再给大家普及一个概念,在 Java 中基本类型和引用类型的区别。在 Java 中数据类型可以分为两大类:基本类型和引用类型。

基本类型也称为值类型,分别是字符类型 char,布尔类型 boolean以及数值类型 byte、short、int、long、float、double。

引用类型则包括类、接口、数组、枚举等。

Java 将内存空间分为堆和栈。基本类型直接在栈 stack中存储数值,而引用类型是将引用放在栈中,实际存储的值是放在堆 heap中,通过栈中的引用指向堆中存放的数据。

上图定义的 a 和 b 都是基本类型,其值是直接存放在栈中的;而 c 和 d 是 String 声明的,这是一个引用类型,引用地址是存放在栈中,然后指向堆的内存空间。下面 d = c,这条语句表示将 c 的引用赋值给 d,那么 c 和 d 将指向同一块堆内存空间。

浅拷贝

接下来用代码看看浅拷贝的效果。

package mytest;@Data//lombok注解class Person implements Cloneable{ private int age; private String name; private Address address;

public Person(int age, String name, Address address) { this.age = age; this.name = name; this.address = address; }

@Override protected Object clone throws CloneNotSupportedException { return super.clone; }

public String display { return "Person [age=" + age + ", name=" + name + ", address=" + address + "]"; }}

@Data//lombok注解class Address{ private String province; private String street;

public Address(String province, String street) { this.province = province; this.street = street; } @Override public String toString { return "Address [province=" + province + ", street=" + street + "]"; }}

public class Client{ public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(15, "sowhat", new Address("河北", "建华南大街")); Person clonePerson = (Person) person.clone; System.out.println(person); System.out.println(clonePerson);

// 信息完全一样 System.out.println(person.display); System.out.println(clonePerson.display); System.out.println("信息完全一致");

System.out.println("原始年龄:" + person.getAge); System.out.println("克隆后原始年龄:" + clonePerson.getAge); System.out.println("年龄完全一样");

System.out.println("原始名字哈希值:" + person.getName.hashCode); System.out.println("克隆后名字哈希值:" + clonePerson.getName.hashCode); System.out.println("字符串哈希值完全一样");

clonePerson.setName("xiaomai"); clonePerson.setAge(20); clonePerson.getAddress.setStreet("中山路"); System.out.println(clonePerson.display); System.out.println(person.display); System.out.println("年龄跟姓名 是完全的深拷贝 副本跟原值无关的!"); System.out.println("地址信息的修改是浅拷贝 ");

}}

结果如下:

mytest.Person@15f550a

mytest.Person@6b2d4a

Person [age=15, name=sowhat, address=Address [province=河北, street=建华南大街]]

Person [age=15, name=sowhat, address=Address [province=河北, street=建华南大街]]

信息完全一致

原始年龄:15

克隆后原始年龄:15

年龄完全一样

原始名字哈希值:-1432601412

克隆后名字哈希值:-1432601412

字符串哈希值完全一样

Person [age=20, name=xiaomai, address=Address [province=河北, street=中山路]]

Person [age=15, name=sowhat, address=Address [province=河北, street=中山路]]

结论:

原对象与新对象是两个不同的对象。

拷贝出来的新对象与原对象内容一致

接着将新对象里面的信息进行了修改,然后输出发现原对象里面的部分信息也跟着变了。其中基本类型跟 String类型的改变不会影响到原始对象的改变。而其他的Ojbect 类型改变的时候会影响到原始数据。上面的结论称为浅拷贝。即创建一个新对象,然后将当前对象的非静态字段复制到该对象,如果字段类型是值类型(基本类型跟String)的,那么对该字段进行复制;如果字段是引用类型的,则只复制该字段的引用而不复制引用指向的对象(也就是只复制对象的地址)。此时新对象里面的引用类型字段相当于是原始对象里面引用类型字段的一个副本,原始对象与新对象里面的引用字段指向的是同一个对象。因此,修改clonePerson里面的address内容时,原person里面的address内容会跟着改变。

深拷贝

了解了浅拷贝,那么深拷贝是什么也就很清楚了。那么该如何实现深拷贝呢?Object 类提供的 clone 是只能实现浅拷贝的,即将引用类型的属性内容也拷贝一份新的。

那么,实现深拷贝我这里收集到两种方式:第一种是给需要拷贝的引用类型也实现Cloneable接口并覆写clone方法;第二种则是利用序列化。

接下来分别对两种方式进行演示:

深拷贝-clone方式

对于以上演示代码,利用clone方式进行深拷贝无非就是将Address类也实现Cloneable,然后对Person的clone方法进行调整。让每个引用类型属性内部都重写clone 方法,既然引用类型不能实现深拷贝,那么我们将每个引用类型都拆分为基本类型,分别进行浅拷贝。比如上面的例子,Person 类有一个引用类型 Address(其实String 也是引用类型,但是String类型有点特殊,后面会详细讲解),我们在 Address 类内部也重写 clone 方法。如下:

package mytest;@Data//lombok注解class Person implements Cloneable{ private int age; private String name; private Address address; protected int abc = 12;

public Person(int age, String name, Address address) { this.age = age; this.name = name; this.address = address; }

@Override // clone 重载 protected Object clone throws CloneNotSupportedException { Person person = (Person) super.clone; //手动对address属性进行clone,并赋值给新的person对象 person.address = (Address) address.clone; return person; }

public String display { return "Person [age=" + age + ", name=" + name + ", address=" + address + "]"; }}

@Data//lombok注解class Address implements Cloneable{ private String province; private String street;

public Address(String province, String street) { this.province = province; this.street = street; } // 深拷贝时添加 @Override protected Object clone throws CloneNotSupportedException { return super.clone; }

@Override public String toString { return "Address [province=" + province + ", street=" + street + "]"; }}

public class Client{ public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(15, "sowhat", new Address("河北", "建华南大街")); Person p1 = person; p1.setAge(45); System.out.println(p1.hashCode); System.out.println(person.hashCode);

System.out.println(p1.display); System.out.println(person.display); System.out.println("-----------");

Person clonePerson = (Person) person.clone; System.out.println(person); System.out.println(clonePerson);

// 信息完全一样 System.out.println(person.display); System.out.println(clonePerson.display); System.out.println("信息完全一致");

System.out.println("原始年龄:" + person.getAge); System.out.println("克隆后原始年龄:" + clonePerson.getAge); System.out.println("年龄完全一样");

System.out.println("原始名字哈希值:" + person.getName.hashCode); System.out.println("克隆后名字哈希值:" + clonePerson.getName.hashCode); System.out.println("字符串哈希值完全一样");

clonePerson.setName("sowhat1412"); clonePerson.setAge(20); clonePerson.getAddress.setStreet("中山路"); System.out.println(clonePerson.display); System.out.println(person.display); System.out.println("年龄跟姓名 是完全的深拷贝 副本跟原值无关的!"); System.out.println("地址信息的修改是浅拷贝 ");

}}

但是这种做法有个弊端,这里我们Person 类只有一个 Address 引用类型,而 Address 类没有,所以我们只用重写 Address 类的clone 方法。但是如果 Address 类也存在一个引用类型,那么我们也要重写其clone 方法,这样下去,有多少个引用类型,我们就要重写多少次,如果存在很多引用类型,那么代码量显然会很大,所以这种方法不太合适。

利用序列化

序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。这里写到流中的对象则是原始对象的一个拷贝,因为原始对象还存在 JVM 中,所以我们可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。注意每个需要序列化的类都要实现 Serializable 接口,如果有某个属性不需要序列化,可以将其声明为 transient,即将其排除在克隆属性之外。

package mytest;

import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;

/** * 利用序列化和反序列化进行对象的深拷贝 * @author ljj */class DeepClone implements Serializable{ private static final long serialVersionUID = 1412L; public Object deepClone throws Exception{ //序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream; ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); //反序列化 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject; }}@Dataclass Person extends DeepClone{ private static final long serialVersionUID = 1L; private int age; private String name; private Address address;

public Person(int age, String name, Address address){ this.age = age; this.name = name; this.address = address; }

public String display{ return "Person [age=" + age + ", name=" + name + ", address=" + address + "]"; }}

@Dataclass Address extends DeepClone{ private static final long serialVersionUID = 1412L; private String province; private String street;

public Address(String province, String street){ this.province = province; this.street = street; }

@Override public String toString{ return "Address [province=" + province + ", street=" + street + "]"; }

public void setStreet(String street){ this.street = street; }}

public class Client{ public static void main(String[] args) throws Exception{ Person person = new Person(15, "sowhat", new Address("河北", "建华南大街"));

Person clonePerson = (Person) person.deepClone;

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

System.out.println(person.display); System.out.println(clonePerson.display);

clonePerson.setName("sowhat1412"); clonePerson.setAge(20); Address address = clonePerson.getAddress; address.setStreet("中山路"); System.out.println(clonePerson.display); System.out.println(person.display);

}}

标签: #java深拷贝和浅拷贝