Java的浅拷贝与深拷贝详细解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: Java的浅拷贝与深拷贝详细解析

一、认识浅拷贝与深拷贝


对于=赋值,相对于基本数据类型实际上就是直接拷贝它的值,而对于引用数据类型则只是传递这个对象的引用,将原对象的引用实际上还是指向的同一个对象。


浅拷贝:拷贝一个对象时,只对基本数据类型进行拷贝,而对于引用数据类型只是进行了引用的传递,并没有正式的创建一个新的对象。


深拷贝:相对于浅拷贝不同的是,针对于引用数据类型的拷贝是创建了一个新的对象,并且复制其中的成员变量。


引用赋值:


class Person{
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
public class AnnotationTest {
    @Test
    public void test(){
        Person person = new Person("长路", 18);
        Person person2 = person;
        System.out.println(person);//xyz.changlu.Person@621be5d1
        System.out.println(person2);//xyz.changlu.Person@621be5d1
    }
}



这种方式既不属于浅拷贝也不属于深拷贝就是简单的引用传递。person与person2实例都指向堆中同一个引用地址。

接下来我们来通过例子探讨浅拷贝与深拷贝实现方式。



二、认识clone()方法


首先看一下Object类中的clone()方法:


public class Object {
    //native修饰符说明该方法是一个本地方法
    protected native Object clone() throws CloneNotSupportedException;
}


实现了Cloneable接口即可使用Object的clone()方法。

如何使用这个clone方法呢?需要搭配一个Cloneable接口。


public interface Cloneable {
}


是的该接口没有方法是不是很奇怪。

讲述这两者之间联系:自定义类实现该接口表名当前对象可以clone,实现了该接口后能够改变父类Object类中的clone方法,且能够调用Object的clone方法;若直接调用clone()方法否则会抛出CloneNotSupportedException异常。



三、实现浅拷贝


3.1、clone()方法


对下面的Person进行浅拷贝,利用clone()方法来拷贝:


class Wallet{
    private Integer money = 100;
}
class Person implements Cloneable{
    private String name;
    private int age;
    private Wallet wallet;
    public Person(String name, int age,Wallet wallet) {
        this.name = name;
        this.age = age;
        this.wallet = wallet;
    }
    public Wallet getWallet() {
        return wallet;
    }
    @Override
    public Object clone(){
        Person person = null;
        try {
            person = (Person) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return person;
    }
}
public class AnnotationTest {
    @Test
    public void test() throws CloneNotSupportedException {
        Person person = new Person("长路", 18,new Wallet());
        Person clonePerson = (Person) person.clone();
        //使用clone()获得的对象与原先的引用地址不一样,是在堆中新开辟的
        System.out.println(person == clonePerson);//false
        //查看下其中的引用类型是否直接拷贝了引用,true表示对引用数据类型直接赋引用地址
        System.out.println(person.getWallet() == clonePerson.getWallet());//true
    }
}


首先是Person类实现Cloneable接口。

接着重写了Object的clone()方法,有几个改变点(注意点):

将protected修饰符更改为public,为啥呢,因为使用protected修饰符在不同包中类无法使用,除非该类是Person的子类,方便其他人调用。

不抛出异常,而是直接在方法中catch异常。

关键浅拷贝部分,super.clone()实际上就是调用父类的clone方法(即Object类的),对本实例进行浅拷贝。

总结:使用上面方式的浅拷贝的实例会复制一份新的实例出来,但其中的成员属性内容对于引用类型都是直接拷贝引用而不是重新创建新的对象。(若是想要引用类型属性也重新创建新对象,那么其引用属性也要重写clone()方法,并且在clone好Person之后对其引用属性再重新调用clone()方法)


3.2、System.arraycopy()


该方法属于System类中静态本地方法,是浅拷贝:


public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

在jdk中调用该本地方法有:Arrays.copyOf([] original, int newLength, Class<? extends T[]> newType)、ArrayList.clone()


测试程序:通过使用ArrayList来进行测试


class Person{
    private String name = "changlu";
    private int age = 18;
    public void setName(String name) {
        this.name = name;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class Test {
    public static void main(String[] args) {
        ArrayList<Person> list = new ArrayList<>();
        list.add(new Person());
        //克隆的ArrayList集合
        ArrayList<Person> clone = (ArrayList<Person>) list.clone();
        clone.get(0).setName("liner");//对该集合中的元素name进行修改看是否改变了原来的对象实例
        //打印原来集合
        System.out.println(list);
    }
}



说明:可以看到对克隆集合中的元素进行修改,同时也改变了原有的集合元素,说明该方法是浅拷贝。



四、实现深拷贝


方式一:使用clone()方法

import org.junit.Test;


class Wallet implements Cloneable{
    private Integer money = 100;
    //1、实现cloneable接口,并重写clone()方法,其中调用Object的clone()方法
    @Override
    public Object clone() {
        Wallet wallet = null;
        try {
            wallet = (Wallet) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return wallet;
    }
    @Override
    public String toString() {
        return "Wallet{" +
                "money=" + money +
                '}';
    }
}
class Person implements Cloneable{
    private String name;
    private int age;
    private Wallet wallet;
    public Person(String name, int age,Wallet wallet) {
        this.name = name;
        this.age = age;
        this.wallet = wallet;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setWallet(Wallet wallet) {
        this.wallet = wallet;
    }
    public Wallet getWallet() {
        return wallet;
    }
    @Override
    public Object clone(){
        Person person = null;
        try {
            person = (Person) super.clone();
            //2、对person实例中的wallet进行重新拷贝
            person.setWallet((Wallet) person.getWallet().clone());
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return person;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", wallet=" + wallet +
                '}';
    }
}
public class AnnotationTest {
    @Test
    public void test() throws CloneNotSupportedException {
        Person person = new Person("changlu", 18,new Wallet());
        Person clonePerson = (Person) person.clone();
        //使用clone()获得的对象与原先的引用地址不一样,是在堆中新开辟的
        System.out.println(person == clonePerson);//false
        System.out.println(person);
        System.out.println(clonePerson);
        //在Person的clone()方法中对克隆之后的person的属性wallet再次进行clone()
        System.out.println(person.getWallet() == clonePerson.getWallet());//false
        System.out.println(person);
        System.out.println(clonePerson);
        System.out.println(person.getName() == clonePerson.getName());//true
    }
}


实现Person的深拷贝,关键点就是其中的引用数据类型Wallet,那么我们就在Wallet类中实现Cloneable接口,并重写clone()方法即可。

主要改动点就是上面例子中的1、2部分。



其中的String类型并没有进行深拷贝。

弊端说明:很明显我们就能感受到其中的问题所在,若是一个类中有多个自定义的引用类型,那么我们不得一个个类都要去实现Cloneable接口,并重写方法吗?那么我们可以使用反序列化来实现深拷贝;对于jdk中原本定义好的核心类例如String无法进行深拷贝。



方式二:使用反序列化方式

import org.junit.Test;

import java.io.*;


/**
 * @ClassName Test
 * @Author ChangLu
 * @Date 2021/2/21 20:48
 * @Description TODO
 */
class Wallet implements Serializable{
    private static final long serialVersionUID = -6849794470754688710L;
    private Integer money = 100;
    @Override
    public String toString() {
        return "Wallet{" +
                "money=" + money +
                '}';
    }
}
class Person implements Serializable {
    private static final long serialVersionUID = -6849794470754667722L;
    private String name;
    private int age;
    private Wallet wallet;
    public Person(String name, int age,Wallet wallet) {
        this.name = name;
        this.age = age;
        this.wallet = wallet;
    }
    public String getName() {
        return name;
    }
    public void setWallet(Wallet wallet) {
        this.wallet = wallet;
    }
    public Wallet getWallet() {
        return wallet;
    }
    //进行深拷贝
    public Person deepClone() {
        Person person = null;
        ObjectInputStream ois = null;
        ObjectOutputStream oops = null;
        //使用对象输出流进行写操作,写入本身实例
        try{
            //对象输入流将本实例写入
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            oops = new ObjectOutputStream(baos);
            oops.writeObject(this);
            //对象输出流将实例读出
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());//读入刚刚写入的baos
            ois = new ObjectInputStream(bais);
            //对象输出流读出实例
            person = (Person) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }finally {
            if(ois != null){
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(oops != null){
                try {
                    oops.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return person;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", wallet=" + wallet +
                '}';
    }
}
public class AnnotationTest {
    @Test
    public void test() {
        Person person = new Person("changlu", 18, new Wallet());
        Person clonePerson = person.deepClone();
        //判断克隆的实例是否指向一个引用
        System.out.println(person == clonePerson);//false
        System.out.println(person);
        System.out.println(clonePerson);
        //判断克隆的实例中的引用对象是否指向一个引用
        System.out.println(person.getWallet() == clonePerson.getWallet());//false
        System.out.println(person);
        System.out.println(clonePerson);
        System.out.println(person.getName() == clonePerson.getName());
    }
}


Person与Wallet类都实现了Serializable接口,并且各自类中添加UID。

接着在Person中实现deepClone()方法,该方法中对本身实例进行序列化与反序列化,从而达到深拷贝的效果。



Person类中的String引用类型也进行了深拷贝。

注意:使用反序列化进行深拷贝的对类实现Serializable接口,并添加UID。

相关文章
|
12天前
|
传感器 监控 Java
Java代码结构解析:类、方法、主函数(1分钟解剖室)
### Java代码结构简介 掌握Java代码结构如同拥有程序世界的建筑蓝图,类、方法和主函数构成“黄金三角”。类是独立的容器,承载成员变量和方法;方法实现特定功能,参数控制输入环境;主函数是程序入口。常见错误包括类名与文件名不匹配、忘记static修饰符和花括号未闭合。通过实战案例学习电商系统、游戏角色控制和物联网设备监控,理解类的作用、方法类型和主函数任务,避免典型错误,逐步提升编程能力。 **脑图速记法**:类如太空站,方法即舱段;main是发射台,static不能换;文件名对仗,括号要成双;参数是坐标,void不返航。
34 5
|
24天前
|
Java API 数据处理
深潜数据海洋:Java文件读写全面解析与实战指南
通过本文的详细解析与实战示例,您可以系统地掌握Java中各种文件读写操作,从基本的读写到高效的NIO操作,再到文件复制、移动和删除。希望这些内容能够帮助您在实际项目中处理文件数据,提高开发效率和代码质量。
29 4
|
1月前
|
XML JSON Java
Java中Log级别和解析
日志级别定义了日志信息的重要程度,从低到高依次为:TRACE(详细调试)、DEBUG(开发调试)、INFO(一般信息)、WARN(潜在问题)、ERROR(错误信息)和FATAL(严重错误)。开发人员可根据需要设置不同的日志级别,以控制日志输出量,避免影响性能或干扰问题排查。日志框架如Log4j 2由Logger、Appender和Layout组成,通过配置文件指定日志级别、输出目标和格式。
|
2月前
|
存储 Java 计算机视觉
Java二维数组的使用技巧与实例解析
本文详细介绍了Java中二维数组的使用方法
63 15
|
2月前
|
算法 搜索推荐 Java
【潜意识Java】深度解析黑马项目《苍穹外卖》与蓝桥杯算法的结合问题
本文探讨了如何将算法学习与实际项目相结合,以提升编程竞赛中的解题能力。通过《苍穹外卖》项目,介绍了订单配送路径规划(基于动态规划解决旅行商问题)和商品推荐系统(基于贪心算法)。这些实例不仅展示了算法在实际业务中的应用,还帮助读者更好地准备蓝桥杯等编程竞赛。结合具体代码实现和解析,文章详细说明了如何运用算法优化项目功能,提高解决问题的能力。
87 6
|
2月前
|
存储 算法 搜索推荐
【潜意识Java】期末考试可能考的高质量大题及答案解析
Java 期末考试大题整理:设计一个学生信息管理系统,涵盖面向对象编程、集合类、文件操作、异常处理和多线程等知识点。系统功能包括添加、查询、删除、显示所有学生信息、按成绩排序及文件存储。通过本题,考生可以巩固 Java 基础知识并掌握综合应用技能。代码解析详细,适合复习备考。
32 4
|
2月前
|
Java 编译器 程序员
【潜意识Java】期末考试可能考的简答题及答案解析
为了帮助同学们更好地准备 Java 期末考试,本文列举了一些常见的简答题,并附上详细的答案解析。内容包括类与对象的区别、多态的实现、异常处理、接口与抽象类的区别以及垃圾回收机制。通过这些题目,同学们可以深入理解 Java 的核心概念,从而在考试中更加得心应手。每道题都配有代码示例和详细解释,帮助大家巩固知识点。希望这些内容能助力大家顺利通过考试!
35 0
|
18天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
144 60
【Java并发】【线程池】带你从0-1入门线程池
|
7天前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
58 23
|
14天前
|
Java 调度
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
83 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码

热门文章

最新文章

推荐镜像

更多