Java 深拷贝和浅拷贝

尾调薄雾
• 阅读 2542

前言

在讲解什么是深拷贝和浅拷贝之前,我们先来了解一下什么是基本类型和引用类型。

基本类型和引用类型

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

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

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

Java 深拷贝和浅拷贝

上图定义的 a 和 b 都是基本类型,其值是直接存放在栈中的;而 c 和 d 是 String 声明的,这是一个引用类型,引用地址是存放在 栈中,然后指向堆的内存空间。

下面 d = c;这条语句表示将 c 的引用赋值给 d,那么 c 和 d 将指向同一块堆内存空间。

Clone方法

本篇博客我们讲解的是 Java 的深拷贝和浅拷贝,其实现方式正是通过调用 Object 类的 clone() 方法来完成。在 Object.class 类中,源码为:

protected native Object clone() throws CloneNotSupportedException; 

这是一个用 native 关键字修饰的方法,只需要知道用 native 修饰的方法就是告诉操作系统,这个方法我不实现了,让操作系统去实现。具体怎么实现我们不需要了解,只需要知道 clone方法的作用就是复制对象,产生一个新的对象。

浅拷贝

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

实现对象拷贝的类,需要实现 Cloneable 接口,并覆写 clone() 方法。

public class Address {

    private String province;
    private String city;

    public void setAddress(String province, String city) {
        this.province = province;
        this.city = city;
    }

    @Override
    public String toString() {
        return "Address [province=" + province + ", city=" + city + "]";
    }
}
public class Student implements Cloneable {

    private String name;
    private int age;
    private Address address;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setAddress(String province, String city) {
        address.setAddress(province, city);
    }

    public void display(String name) {
        System.out.println(name + ":" + "name=" + name + ", age=" + age + "," + address);
    }

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

这是一个我们要进行赋值的原始类 Student。下面我们产生一个 Student对象,并调用其 clone 方法复制一个新的对象。

注意:调用对象的 clone 方法,必须要让类实现 Cloneable 接口,并且覆写 clone 方法。

测试:

    public static void main(String[] args) throws CloneNotSupportedException {
        Student s1 = new Student("小明",20);
        s1.setAddress("安徽","合肥");
        Student s2 = (Student) s1.clone();
        System.out.println("S1:"+s1);
        System.out.println("s1.getName:"+s1.getName().hashCode());
        System.out.println("S2:"+s2);
        System.out.println("s2.getName:"+s2.getName().hashCode());

        s1.display("s1");
        s2.display("s2");
        s2.setAddress("安徽","安庆");
        s1.display("s1");
        s2.display("s2");
    }

输出结果:

S1:org.example.jvm.Student@2a84aee7
s1.getName:756703
S2:org.example.jvm.Student@a09ee92
s2.getName:756703
s1:name=s1, age=20,Address [province=安徽, city=合肥]
s2:name=s2, age=20,Address [province=安徽, city=合肥]
s1:name=s1, age=20,Address [province=安徽, city=安庆]
s2:name=s2, age=20,Address [province=安徽, city=安庆]

首先我们创建一个Student类的对象 s1,其name 为小明,age为20,地址类 Address 两个属性为 安徽和合肥。接着我们调用 clone() 方法复制另一个对象 s2,接着打印这两个对象的内容。

分析结果:

  • 从第 1 行和第 3 行打印结果来看,这是两个不同的对象。
  • 从第 5 行和第 6 行打印的对象内容看,原对象 s1 和克隆出来的对象 s2 内容完全相同。
  • 我们更改一下克隆对象 s2 的属性 Address 为安徽安庆(原对象 s1 是安徽合肥),但是从第 7 行和第 8 行打印结果来看,原对象 s1 和克隆对象 s2 的 Address 属性都被修改了。
  • 对象 Student 的属性 Address,经过 clone 之后,其实只是复制了其引用,他们指向的还是同一块堆内存空间,当修改其中一个对象的属性 Address,另一个也会跟着变化。

Java 深拷贝和浅拷贝

浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。

深拷贝

深拷贝,在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。

Java 深拷贝和浅拷贝

对于 Student 的引用类型的成员变量 Address ,需要实现 Cloneable 并重写 clone() 方法。

public class Address implements Cloneable{

    private String province;
    private String city;

    public void setAddress(String province, String city) {
        this.province = province;
        this.city = city;
    }

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

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

Studentclone() 方法中,需要拿到拷贝自己后产生的新的对象,然后对新的对象的引用类型再调用拷贝操作,实现对引用类型成员变量的深拷贝。

public class Student implements Cloneable {

    private String name;
    private int age;
    private Address address;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setAddress(String province, String city) {
        address.setAddress(province, city);
    }

    public void display(String name) {
        System.out.println(name + ":" + "name=" + name + ", age=" + age + "," + address);
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student s = (Student) super.clone();
        s.address = (Address) address.clone();
        return s;
    }
}

测试:

    public static void main(String[] args) throws CloneNotSupportedException {
        Student s1 = new Student("小明",20);
        s1.setAddress("安徽","合肥");
        Student s2 = (Student) s1.clone();
        System.out.println("S1:"+s1);
        System.out.println("s1.getName:"+s1.getName().hashCode());
        System.out.println("S2:"+s2);
        System.out.println("s2.getName:"+s2.getName().hashCode());

        s1.display("s1");
        s2.display("s2");
        s2.setAddress("安徽","安庆");
        s1.display("s1");
        s2.display("s2");
    }

输出结果:

S1:org.example.jvm.Student@2a84aee7
s1.getName:756703
S2:org.example.jvm.Student@a09ee92
s2.getName:756703
s1:name=s1, age=20,Address [province=安徽, city=合肥]
s2:name=s2, age=20,Address [province=安徽, city=合肥]
s1:name=s1, age=20,Address [province=安徽, city=合肥]
s2:name=s2, age=20,Address [province=安徽, city=安庆]

由输出结果可知,深拷贝后,不管是基础数据类型还是引用类型的成员变量,修改其值都不会相互造成影响。

注意:

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

还有一种方式可以实现深拷贝:利用序列化

序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。

这里写到流中的对象则是原始对象的一个拷贝,因为原始对象还存在 JVM 中,所以我们可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。

注意每个需要序列化的类都要实现 Serializable 接口,如果有某个属性不需要序列化,可以将其声明为 transient,即将其排除在克隆属性之外。

public class Address implements Serializable {

    private String province;
    private String city;

    public void setAddress(String province, String city) {
        this.province = province;
        this.city = city;
    }

    @Override
    public String toString() {
        return "Address [province=" + province + ", city=" + city + "]";
    }
}
public class Student implements Serializable {

    private String name;
    private int age;
    private Address address;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setAddress(String province, String city) {
        address.setAddress(province, city);
    }

    public void display(String name) {
        System.out.println(name + ":" + "name=" + name + ", age=" + age + "," + address);
    }

    //深度拷贝
    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();
    }

}

测试:

    public static void main(String[] args) throws Exception {
        Student s1 = new Student("小明",20);
        s1.setAddress("安徽","合肥");
        Student s2 = (Student) s1.deepClone();
        System.out.println("S1:"+s1);
        System.out.println("s1.getName:"+s1.getName().hashCode());
        System.out.println("S2:"+s2);
        System.out.println("s2.getName:"+s2.getName().hashCode());

        s1.display("s1");
        s2.display("s2");
        s2.setAddress("安徽","安庆");
        s1.display("s1");
        s2.display("s2");
    }

输出结果:

S1:org.example.jvm.Student@3f99bd52
s1.getName:756703
S2:org.example.jvm.Student@1f17ae12
s2.getName:756703
s1:name=s1, age=20,Address [province=安徽, city=合肥]
s2:name=s2, age=20,Address [province=安徽, city=合肥]
s1:name=s1, age=20,Address [province=安徽, city=合肥]
s2:name=s2, age=20,Address [province=安徽, city=安庆]

因为序列化产生的是两个完全独立的对象,所有无论嵌套多少个引用类型,序列化都是能实现深拷贝的。

总结

至此,相信什么是深拷贝,什么是浅拷贝,相信你一定明白了。

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
java 复制Map对象(深拷贝与浅拷贝)
java复制Map对象(深拷贝与浅拷贝)CreationTime2018年6月4日10点00分Author:Marydon1.深拷贝与浅拷贝  浅拷贝:只复制对象的引用,两个引用仍然指向同一个对象
仔细看看,会有收获。js深浅拷贝
好好理解深浅拷贝和赋值(针对引用类型)赋值:两个对象指向同一内存地址。结果,无论是修改基本类型还是引用类型,两个对象的值都会改变。浅拷贝:两个对象指向不同的内存地址,但是他们中的引用类型数据指向同一内存地址。结果,修改引用类型,两个对象的值都会改变;修改基本类型,互不影响。深拷贝:两个对象指向不同的内存地址,他们中的引用类型也指向不同的内存地址。结果,均互不
晴空闲云 晴空闲云
3年前
也谈JavaScript浅拷贝和深拷贝
网上关于这个话题,讨论有很多了,根据各路情况我自己整理了一下,最后还是能接近完美的实现深拷贝,欢迎大家讨论。javascript中的对象是引用类型,在复制对象的时候就要考虑是用浅拷贝还是用深拷贝。直接赋值对象是引用类型,如果直接赋值给另外一个对象,那么只是赋值一个引用,实际上两个变量指向的同一个数据对象,如果其中一个对象的属性变更,那么另外一个也会变更。示
Wesley13 Wesley13
3年前
Java对象的浅拷贝和深拷贝&&String类型的赋值
Java中的数据类型分为基本数据类型和引用数据类型。对于这两种数据类型,在进行赋值操作、方法传参或返回值时,会有值传递和引用(地址)传递的差别。浅拷贝(ShallowCopy):①对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,
Wesley13 Wesley13
3年前
Java中的基本数据类型和引用数据类型的区别
一、前言众所周知Java是一种强类型语言,在Java语言中,Java的数据类型一共分为两大类,分别为基本数据类型和引用数据类型,其中基本数据类型细分小类可分为整数类型、浮点类型、字符类型、布尔类型这四小类。二、基本数据类型和引用数据类型1\.基本数据类型只有
Stella981 Stella981
3年前
JVM调优总结一
数据类型   Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。基本类型的变量保存原始值,即:他代表的值就是数值本身;而引用类型的变量保存引用值。“引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置。基本类型包括:byte,short,int,long,cha
Stella981 Stella981
3年前
JavaScript基础心法——深拷贝和浅拷贝
!(https://oscimg.oschina.net/oscnet/c131215a5aaaeb7909d7398688df6ea6dcd.png)浅拷贝和深拷贝都是对于JS中的引用类型而言的,浅拷贝就只是复制对象的引用,如果拷贝后的对象发生变化,原对象也会发生变化。只有深拷贝才是真正地对对象的拷贝。前言说到深浅拷贝,必须先
Wesley13 Wesley13
3年前
JAVA基本类型和引用类型
一、基本数据类型java中一共分为8种基本数据类型:byte、short、int、long、float、double、char、boolean,其中byte、short、int、long是整型。float、double是浮点型,char是字符型,boolean是布尔型。二、引用类型j
Wesley13 Wesley13
3年前
Java基础(二)数据类型
  数据类型主要分为基本类型和引用类型两大类。  一、基本类型  1.基本类型又分为数值类型和boolean类型,  (1)数值类型包括浮点数类型、整数类型和字符类型  整型                                          浮点型(初始化时需要加f或d)  字符类型  byte    
Stella981 Stella981
3年前
JVM调优总结(一)基本概念
数据类型Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。    基本类型:保存原始值,即:他代表的值就是数值本身;    引用类型:保存引用值。“引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引
小万哥 小万哥
1年前
Kotlin 数据类型详解:数字、字符、布尔值与类型转换指南
Kotlin中变量类型由值决定,如Int、Double、Char、Boolean、String。通常可省略类型声明,但有时需指定。数字类型分整数(Byte,Short,Int,Long)和浮点(Float,Double),默认整数为Int,浮点为Double。布尔值是true或false,Char用单引号,字符串用双引号。数组和类型转换将在后续讨论,转换需用特定函数。