Java深入理解深拷贝和浅拷贝区别

15 篇文章 0 订阅

目录

1 什么是深拷贝和浅拷贝

2 拷贝的内存变化

2.1 基本类型和引用类型在赋值时内存的变化

2.2 基本类型和引用类型作为函数参数的区别​

2.3 深拷贝和浅拷贝

3 深浅拷贝实例

3.1 浅拷贝

3.2 深拷贝:

3.3 Arrays.copyOf()


1 什么是深拷贝和浅拷贝

浅拷贝和深拷贝都是对一个已有对象的操作,编译系统会自动调用一种构造函数——拷贝构造函数,如果用户未定义拷贝构造函数,则会调用默认拷贝构造函。
在java中,一般使用=好做赋值操作的时候,对于基本数据类型,实际上是拷贝他的值,但是对于对象而言,其实赋值的指示这个对象的引用,将原对象的引用传递过去,他们实际上还是指向的用一个对象。
而浅拷贝和深拷贝就是在这个基础之上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,而没有创建一个新的对象,那就是浅拷贝。反之,在对引用数据类型进行拷贝的时候,新创建了一个新的对象,并完全复制了其内的成员变量,那么这就是深拷贝。实际浅拷贝和深拷贝只是相对的,如果一个对象内部只有基本数据类型,那用 clone() 方法获取到的就是这个对象的深拷贝,而如果其内部还有引用数据类型,那用 clone() 方法就是一次浅拷贝的操作。
clone在平时项目的开发中可能用的不是很频繁,但是区分深拷贝和浅拷贝会让我们对java内存结构和运行方式有更深的了解。深拷贝和彻底深拷贝,在创建不可变对象时,可能对程序有着微妙的影响,可能会决定我们创建的不可变对象是不是真的不可变。

简单总结

1.1 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
1.2 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

2 拷贝的内存变化

数据分为基本数据类型(int, boolean, double, byte, char等)和对象数据类型。
基本数据类型的特点:直接存储在栈(stack)中的数据.
引用数据类型的特点:在栈内存存储对象引用,真实的数据存放在堆内存里
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

首先需要明白,深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。那先来看看浅拷贝和深拷贝的概念。

在 Java 中,除了基本数据类型(元类型)之外,还存在 类的实例对象 这个引用数据类型。而一般使用 =号做赋值操作的时候。对于基本数据类型,实际上是拷贝的它的值,

但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际上还是指向的同一个对象。

浅拷贝:如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,而没有真实的创建一个新的对象。

深拷贝:在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量。

深拷贝和浅拷贝的示意图大致如下:

 

2.1 基本类型和引用类型在赋值时内存的变化

你可以认为,赋值就是在拷贝。

1、基本类型:

2、引用类型:

 

如果给arr[0]赋值的话,arr1[0]的值也会发生变化,因为,arr和arr1保存着相同的地址,它门两个引用的数据是共享的。就像你在很多地方(简历的那张纸,户口本上的那张纸)会写上你的家庭地址。这么多张纸都引用着你家。根据一张纸上找到你家,给你家放上一百万的现金(数据改变了,相当于arr[0]=10),再根据另外一张纸的地址也找到了你家,你发现你一百万在(不要给我说被人拿了)

如果在上面的基础上增加一句代码:arr[0]=10;那么内存将会有如下变化:

​​

2.2 基本类型和引用类型作为函数参数的区别

1 基本类型作为函数的参数

2、引用类型作为函数的参数:

2.3 深拷贝和浅拷贝

   终于说到了深拷贝和浅拷贝。

其实在第二点已经说到了拷贝,所谓拷贝,就是赋值。把一个变量赋给另外一个变量,就是把变量的内容进行拷贝。把一个对象的值赋给另外一个对象,就是把一个对象拷贝一份。

1、基本类没有问题,

因为,基本类型赋值时,赋的是数据(所以,不存在深拷贝和浅拷贝的问题)。

       如:

    Var x = 100;

    Var y = x; //此时x和y都是100;

   如果要改变y的值,x的值不会改变。

2、引用类型有问题

  因为,引用类型赋值时,赋的值地址(就是引用类型变量在内存中保存的内容),强烈建议把前面的第二点(基本类型和引用类型在赋值时内存的变化)多看几遍,以保证理解深刻。这样,一劳永逸,以后在碰到任何跟引用类型有关的话题(如:继承时,父类的属性是引用类型)都没有问题。

      如:

var arr1 = new Array(12,23,34)

Var arr2 = arr1;//这就是一个最简单的浅拷贝

如果要改变arr2所引用的数据:arr2[0]=100时,那么arr1[0]的值也是100。

        原因就是 arr1和arr2引用了同一块内存区域(以上的第二点中有体现)。

这是最简单的浅拷贝,因为,只是把arr1的地址拷贝的一份给了arr2,并没有把arr1的数据拷贝一份。所以,拷贝的深度不够

3 深浅拷贝实例

3.1 浅拷贝

浅拷贝(同一个引用):被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。

Person

public class Person {
    public String name;
    public Integer age;
    public String sex;
    /**
     * 提供get和set方法和全参构造函数
     */
}

Test

    public static void main(String[] args) throws Exception {
        Person person = new Person("小小",3,"女");
        //将person值赋值给person1
        Person person1 = person;
        System.out.println(person);
        System.out.println(person1);
        person1.setName("小小她爸");
        System.out.println("person 中 name为:"+person.getName());
        System.out.println("person1 中 name为:"+person.getName());
    }

查看运行结果

从图片中我们可以很明显看出,它们指向的内存地址是一致的,同样我改变person1的属性值时发现person的属性值也改变了。

说明:对于对象用"=" 赋值 其实只是引用指针的复制,这两个引用还是指向同一个对象。

3.2 深拷贝:

  实现方式:

  1. 被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常) 该接口为标记接口(不含任何方法)

  2. 覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象,(native为本地方法)

  Java的clone()方法

  ⑴clone方法将对象复制了一份并返回给调用者。一般而言,clone()方法满足:
    ①对任何的对象x,都有x.clone() !=x//克隆对象与原对象不是同一个对象
    ②对任何的对象x,都有x.clone().getClass()= =x.getClass()//克隆对象与原对象的类型一样
    ③如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。

  ⑵Java中对象的克隆
    ①为了获取对象的一份拷贝,我们可以利用Object类的clone()方法。 
    ②在派生类中覆盖基类的clone()方法,并声明为public。 
    ③在派生类的clone()方法中,调用super.clone()。 
    ④在派生类中实现Cloneable接口。

Student

/**
 * 如果对象要实现深拷贝 那么实体需要做两步
 * 1、实体实现Cloneable接口
 * 2、重写 clone()方法
 */
public class Student implements Cloneable {

    public String name;
    public Integer age;
    public String sex;
    //这也是个实体
    public Address address;
    /**
     * 提供get和set方法和全参构造函数
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

Test

    public static void main(String[] args) throws Exception {
        Student student = new Student("小小", 3, "女", null);
        //将person值赋值给person1
        Student student1 = (Student) student.clone();
        System.out.println(student);
        System.out.println(student1);
        student1.setName("小小她爸");
        System.out.println("person 中 name为:" + student.getName());
        System.out.println("person1 中 name为:" + student1.getName());
        }

这里可以已经是两个不同的对象了。但是这里需要注意的是,如果对象中含有对象,这个对象还是浅拷贝。

Address

public class Address  {
    public String  city;
    public  int phone;
    /**
     * 提供get和set方法和全参构造函数
     */
    }

Test

    public static void main(String[] args) throws Exception {
        Address address = new Address("杭州", 1888888888);
        Student student2 = new Student("小小", 3, "女", address);
        //将person值赋值给person1
        Student student3 = (Student) student2.clone();
        address.setCity("北京天安门");
        System.out.println("person2 中 city为:" + student2.getAddress().getCity());
        System.out.println("person3 中 city为:" + student3.getAddress().getCity());

    }

我们发现虽然Student是实现了深拷贝,但Address却还是浅拷贝,那如何让Adress也实现深拷贝呢。
Address修改

public class Address implements Cloneable {
    public String  city;
    public  int phone;
   /**
     * 提供get和set方法和全参构造函数
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

Student修改

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

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

序列化实现深拷贝

//序列化实现深拷贝
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();
}
 //因为序列化产生的是两个完全独立的对象,所有无论嵌套多少个引用类型,序列化都是能实现深拷贝的。

3.3 Arrays.copyOf()

之前我误以为Arrays.copyOf()为深拷贝,那只是因为我用的是基本数据类型作为数组,而基本数据类型上面已经说过它没有深浅拷贝这个概念,可以把他理解成只有深拷贝。

 public static void main(String[] args) {

        //1、基本数据类型
        int[] a = {0, 1, 2, 3};
        // Arrays.copyOf拷贝
        int[] copy = Arrays.copyOf(a, a.length);
        a[0] = 1;
        System.out.println(Arrays.toString(copy));
        System.out.println(Arrays.toString(a));

        //2、对象数组
        Student[]  stuArr = {new Student("小小", 3, "女"),new Student("小小爸", 29, "男"),new Student("小小妈", 27, "女")};
        // Arrays.copyOf拷贝
        Student[] copyStuArr = Arrays.copyOf(stuArr, stuArr.length);
        copyStuArr[0].setName("小小爷爷");
        System.out.println(Arrays.toString(stuArr));
        System.out.println(Arrays.toString(copyStuArr));
        

    }

运行结果:

可以明显看出,对于基本数据类型只有深拷贝,而对于数组对象而言,明显存在深浅拷贝,而且可以看出Arrays.copyOf()为浅拷贝

 

 

 

 

 

 

 

 

 

 

 

 

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值