一篇文章带你彻底理解Java中的克隆和拷贝

简介: 一篇文章带你彻底理解Java中的克隆和拷贝

0b3fa03ad8d54f4f821d516911b55e58.gif

新的一天,让我们一起学习,一起冲冲冲😎

引子

🌰下面由一个栗子引出我们今天的话题

1.class Student {
    public long id = 1024;
    @Override // 重写父类Object的toString方法,以便在主函数里println()可以直接打印出对象的各个属性,原因我们上篇博客已经讲过了
    public String toString() {
        return "Student{" +
                "id=" + id +
                '}';
    }
}
public class test3 {
    public static void main(String[] args) {
        Student student1 = new Student();
        System.out.println(student1); // 因为我们重写了父类Object的toString方法,所以可以直接打印出对象的值来
    }
}

😁如图我们自定义了一个学生类并且实例化了一个对象student1,在内存中就是这样:

791921f942cd4f8cb15d55f19489f6fd.png

那么如果我们想在堆内存中对student1对象拷贝一份怎么办呢???

🍑可能有同学会直接写下这样的代码

Student student2 = student1;

📝但这只是我们在栈上重新定义了一个引用变量student2,并指向了堆上的student1对象,并没有对我们的student1实现拷贝😂

3e200eac2c9c4f31b5a1e740ac8c992b.png

🏀不信的话,你可以试试,看看当我们改变student2中的id属性时,student1的属性会不会变

1.class Student {
    public long id = 1024;
    @Override 
    public String toString() {
        return "Student{" +
                "id=" + id +
                '}';
    }
}
public class test3 {
    public static void main(String[] args) {
        Student student1 = new Student();
        Student student2 = student1;
        System.out.print("改变前的:");
        System.out.print(student1); 
        System.out.println(student2);
        student2.id = 777;
        System.out.print("改变后的:");
        System.out.print(student1); 
        System.out.println(student2);
    }
}


0dc73188d57d485f8d0d5cafcaf87726.png

克隆方法的第一次尝试

🍑那么我们该怎样拷贝students1这个对象呢?我们可以运用克隆方法clone()来进行拷贝

d166a9a85cdf46c6b4b5c95a2b31caba.png

🍑你会发现clone标红报错了,为啥呢?首先只有一个对象能够被克隆的时候,他才能调用clone。而怎样的对象才能够被克隆呢?


📝那就是该对象所对应的类必须实现Cloneable这个接口才能够被克隆。那行,我们实现Cloneable接口后再调用clone方法试试。


080c644994624b69b90d45ef6968ec00.png

08df9f6910cd4b5a8e0f3d9c14fdd943.gif

别着急😂,我们先看看Cloneable这个接口的源码再说


7b0f1d2d7ab3471180428e70c781aef4.png

📝那Clone存在的意义是什么呀?——》他是一个标记接口,说明了该类的对象是可以被克隆的。


那好吧!我们怎样才能用上clone方法呢,我们需要在子类Student中重写父类Object的clone方法😎。至于为啥要重写而不能直接调用父类的clone方法,这个以我们现在先不用深究,先记住就行😂


那好我们就重写父类Object的clone方法,至于怎么重写:在IDEA里就有相应的快捷键来直接生成我们的重写方法——》ctrl+O


52519a76954f4f6d929ff1fc218f21d2.png


4dfdf49e568841909c0680a3d670e448.png


🌰在内存中就是这样:

3ba5886719e44b9d84d6eb2f7b739036.png

😁来看看此时的代码


a923d9d6cd244596bdd8b2fc5d7c8cc3.png

🍑这时你会发现你的student1可以student1.clone了,那么这就好了吗?


7d0a882fdcee457f90011ef8bd3681ac.png

bdb323a87b8d4381b91e7bb4f04ca9e2.png

🍑这时我们再按下回车键,编译器自动就给我们处理了这个异常

c33b0ad883a347079a7fd9f3ee3f0a67.png

😁好了,经过一番挫折,我们终于成功的把student1这个对象拷贝了一份。总结一下就是:

  • 要克隆的这个对象的类必须实现 Cloneable 接口
  • 类中重写 Objectclone() 方法
  • 处理重写clone方法时的异常情况
  • clone方法需要进行强转(比较特殊,先记住就好)

浅拷贝的实现

🏀但如果我们在Student类中再定义一个引用类型呢?

a7d261e4a6054600b2990988eabdfeaa.png

🏀此时在内存中就变成了这样:


355e152609b54dfcad1dbcd57012d6a2.png

🏀我们可以用代码验证一下:

class Money {
    int money = 17;
 }
class Student extends test3 implements Cloneable{
    public long id = 1024;
    Money m = new Money();
    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                '}';
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class test3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Student student1 = new Student();
        Student student2 = (Student) student1.clone();
        System.out.print("改变前的:");
        System.out.print(student1.m.money + "  ");
        System.out.println(student2.m.money);
        student1.m.money = 98;
        System.out.print("改变后的:");
        System.out.print(student1.m.money + "  ");
        System.out.println(student2.m.money);
    }
}

72330380c33c40779db535e0b0d63709.png

📝这种就称作浅拷贝 ,然后我们就可以引出浅拷贝的概念了:

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。


深拷贝的实现

那怎样把我们的Money类对象也在堆上上拷贝一份呢?

我们刚才通过实现Comparable接口、重写clone方法对Student类实现了拷贝,那么同理我们也可以用这样的办法对Money类对象进行拷贝,嘻嘻!

🌰于是代码就变成了这样

class Money implements Cloneable{  // 对Money类实现Cloneable接口
    int money = 17;
     @Override
     protected Object clone() throws CloneNotSupportedException {
         return super.clone(); // 重写clone方法
     }
 }
class Student extends test3 implements Cloneable{
    public long id = 1024;
    Money m = new Money();
    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                '}';
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // 此时我们在进行 “(Student) student1.clone();” 操作,我们在堆上对student1克隆拷贝出来一个新对象,并让引用变量tmp指向新对象
        // 之前我们是return super.clone(),然后再student1.clone()克隆student1对象,这和直接super.clone是一样的,都是重写了父类Object的clone方法后进行克隆
        Student tmp = (Student) super.clone();
        // 这里的this就是我们要克隆的student1对象,他的一个引用变量m也指向了一个Money类对象,
        // 我们刚才对Money类进行了处理所以可以用this.m.clone对引用变量m所指向的Money类对象进行克隆
        tmp.m = (Money) this.m.clone(); // 把把克隆出来的tmp中的money也给克隆一份,因为tmp.m是Money类型的所以要强制类型转换
        //以前只是把把student1对象的各个值克隆一份,引用变量m所对应的Money类对象并没有克隆,现在是把已经克隆好 Money类对象 的 tmp来返回
        return tmp;
    }
}
public class test3 {
    public static void main(String[] args) throws CloneNotSupportedException { // 处理克隆方法clone的异常
        Student student1 = new Student();
        Student student2 = (Student) student1.clone(); // 此时的student.clone返回Student类对象的引用tmp
        // 这样的话,student2 就指向了原来tmp所指向的对象
        System.out.print("改变前的:");
        System.out.print(student1.m.money + "  ");
        System.out.println(student2.m.money);
        student1.m.money = 98;
        System.out.print("改变后的:");
        System.out.print(student1.m.money + "  ");
        System.out.println(student2.m.money);
    }
}

📝在这个代码中有几个关键点需要解释一下:


e3df5f952ae14f4cb5460abdd418674a.png

🌰第一步分析:


49959f9b4ae54a0db253070e46478b6b.png

🌰第二步分析

80af9dc3870f46f892aa25ba18ba5096.png


🌰第三步分析

2ab76079a6044c75839228c972040fd5.png

🌰第四步分析


438c4bcb884c48a3a88010d7699b0277.png

上面的拷贝就把引用变量m所指向的Money类的对象也在堆中拷贝了一份,这就是深拷贝,

深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

 


📝总结一下就是


浅拷贝:创建一个新对象,然后将当前对象的各种成员属性复制到该新对象,如果成员属性是值类型的,那么对该属性执行复制;如果该字段是引用类型的话,则只是复制引用但不复制引用所指向的对象。


深拷贝:创建一个新对象,然后将当前对象的各种成员属性复制到该新对象,无论该成员属性是值类型的还是引用类型,都复制独立的一份,引用类型也会复制该引用类型所指向的对象。


🍑好了今天我们就学到这里,铁汁们咱们下篇聊聊Java中Comparable接口和Comparator 接口,看看这两个小家伙是怎么用的,嘻嘻😁

73e9664319814ecb8eacfccf56942bad.jpg

相关文章
|
3天前
|
Java
别再被 Java 类和对象绕晕了!一篇文章让你秒懂!
【6月更文挑战第15天】Java中的类是对象的模板,定义属性(如姓名、年龄)和行为(如走路、说话)。对象是类的实例,有自己的属性值。例如,`Person`类可创建`Person`对象,赋予属性值并调用方法。同样,`Car`类可创建不同品牌的汽车对象,每个对象能加速或刹车。理解类与对象的关系是Java面向对象编程的基础。
|
18天前
|
JavaScript Java 测试技术
Java项目基于ssm+vue.js的网上手机销售系统附带文章和源代码设计说明文档ppt
Java项目基于ssm+vue.js的网上手机销售系统附带文章和源代码设计说明文档ppt
23 0
|
23小时前
|
存储 安全 Java
从入门到精通:Java Map全攻略,一篇文章就够了!
【6月更文挑战第18天】Java Map是键值对集合,接口有HashMap、TreeMap、LinkedHashMap等实现。创建Map如`Map<String, Integer> map = new HashMap<>();`。访问修改值用`get()`和`put()`。遍历Map用`entrySet()`配合for-each。多线程下用ConcurrentHashMap。优化包括选对实现类、设置容量和负载因子、避免遍历时修改。本文助你精通Map使用。
|
1天前
|
存储 安全 Java
Java Queue:从入门到精通,一篇文章就够了!
【6月更文挑战第18天】Java集合框架中的队列Queue遵循FIFO原则,用于存储和管理元素。从创建队列(如LinkedList示例)到移除元素(remove和poll方法),再到不同实现类(如ArrayDeque和ConcurrentLinkedQueue),队列在多线程、任务调度等场景中广泛应用。自定义队列如LimitedQueue展示如何限制容量。了解并熟练使用队列能提升程序性能和可读性。队列,是高效编程的关键工具。
|
2天前
|
安全 Java 索引
Java List:从入门到精通,一篇文章就够了!
【6月更文挑战第17天】Java List是有序元素集合,支持索引访问、添加、删除和修改。从ArrayList、LinkedList到Vector,各种实现满足不同场景需求。使用add()添加元素,get()获取,set()修改,remove()删除。遍历可用for-each或Iterator,subList()创建子集。注意线程安全,可选synchronizedList()、Vector或CopyOnWriteArrayList。理解List的基本操作和特性,能提升编程效率。
|
13天前
|
存储 算法 Java
【经典算法】LeetCode133克隆图(Java/C/Python3实现含注释说明,中等)
【经典算法】LeetCode133克隆图(Java/C/Python3实现含注释说明,中等)
9 2
|
15天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的java语言的考试信息报名系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的java语言的考试信息报名系统附带文章源码部署视频讲解等
18 0
|
18天前
|
JavaScript Java 测试技术
Java项目基于ssm+vue.js的生鲜在线销售系统附带文章和源代码设计说明文档ppt
Java项目基于ssm+vue.js的生鲜在线销售系统附带文章和源代码设计说明文档ppt
14 0
|
18天前
|
JavaScript Java 测试技术
Java项目基于ssm+vue.js的经典电影推荐网站附带文章和源代码设计说明文档ppt
Java项目基于ssm+vue.js的经典电影推荐网站附带文章和源代码设计说明文档ppt
14 0
|
18天前
|
JavaScript Java 测试技术
Java项目基于ssm+vue.js的餐馆点餐系统附带文章和源代码设计说明文档ppt
Java项目基于ssm+vue.js的餐馆点餐系统附带文章和源代码设计说明文档ppt
17 0