欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java中的深拷贝和浅拷贝

程序员文章站 2024-03-05 18:27:49
...

在之前做项目的时候,同组的人因为深拷贝和浅拷贝的问题产生过BUG,当时不是自己负责的部分,没对这个问题做过深入的了解,今天深入学习一下????

在介绍深拷贝和浅拷贝之前,先温习几个概念。

数据类型

Java中的数据类型有两种:

  1. 基本类型:粒度最小的数据类型。主要包括
  • 4种整数类型:int、short、long、byte
  • 2种浮点数类型:float、double
  • 1种字符类型:char
  • 1种布尔类型:boolean
  1. 引用类型:也叫做句柄,实际存放的应该是一个地址,地址指向实际内容。主要包括:
  • 接口
  • 数组

数据如何在内存中储存

JVM在程序运行的时候会将内存划分为四个区域:

  • 静态方法区
  • 常量区

针对第一节的数据类型,下面分为基本数据类型的局部变量、基本数据类型的成员变量、引用类型变量来讲解。

基本数据类型的局部变量

基本数据类型的局部变量的储存形式如下图所示:

Java中的深拷贝和浅拷贝

方法中的基本类型的局部变量,都是储存虚拟机的堆内存中的,数据本身的值就是储存在栈内存中。每次给一个新变量赋值的时候,JVM首先会检验内存中有无该数据,如果有,就直接将这个变量指向该数据,没有的话就开辟一个新的内存空间存放数据,然后将变量指向该数据。

int age = 50;
int weight = 50;
int height = 100;

看上面的代码段,三个变量都是某个方法中的局部变量,当执行int age = 50的时候,首先会定义变量,存入变量表中,然后去栈中找是否存在值为50的内容,并将age变量指向50。当执行int weight = 50时,会在栈中找到50,直接将weight指向50。当方法执行完毕以后,方法局部变量会被JVM的垃圾回收机制回收。

基本数据类型的成员变量和引用类型变量

前面提到,除了基本数据类型,还有一种是引用数据类型,对于引用数据类型在内存中的存储形式如下图所示。

Java中的深拷贝和浅拷贝

public class Student {
    private String name;
    private int age;
    private int grade;
}

上面是一个Student类,如果我们不重写toString方法,直接输出一个实例化的对象的话,会得到这样的输出:

aaa@qq.com,其实这就是Student类的地址。

所以,对于引用数据类型来说,引用数据类型只储存一个在堆内存中的地址,而成员变量和方法被储存在堆内存中。

深拷贝和浅拷贝

概念

前面说了这么多,现在步入正题,首先介绍深拷贝和浅拷贝的概念。

在Java中使用 “=” 做赋值操作时,对于基本数据类型,会直接将值直接赋值给变量,而对于引用数据类型来说,会将其引用赋值给另一个对象。

  • 浅拷贝

浅拷贝会创建一个新的对象,这个对象拥有被拷贝对象的一个精确的副本。在该类的成员变量中,对于基本数据类型,拷贝的就是其值;对于引用数据类型,拷贝的就是其内存地址。如果其中一个对象改变了这个引用数据类型的变量的数据,相应地,另一个对象地该引用数据类型地数据也会随之改变。

如下图,Student类有两个成员变量,一个Schoole类成员变量,一个int类型的成员变量,Student类有两两个实例,student1是对student0的浅拷贝的对象。对于引用类型的School来说,两个对象都储存的是School对象的地址,所以说,如果有其中一个对象的School对象的内容发生了改变,另一个对象也会发生改变;对于基本类型的成员变量来说,两个对象都是直接储存的age的值,双方互不干涉。

Java中的深拷贝和浅拷贝

  • 深拷贝

深拷贝不仅会为拷贝新建对象,也会对该对象中任何的引用类型变量建立新的对象,这样,拷贝对象和原对象就是两个完全相同的对象,两者各自的成员变量也互不受影响了。

Java中的深拷贝和浅拷贝

实现浅拷贝

先看一个例子(不是浅拷贝)。

public class Tester {
    public static void main(String[] args) {
        int age = 10;
        int age1 = 20;
        String name = "aaa";
        String name1 = "bbb";

        Student student = new Student();
        student.setAge(age);
        student.setName(name);

        Student student1 = student;
        student.setAge(age1);
        student.setName(name1);

        System.out.println(student);
        System.out.println(student1);

    }
}

----Output----
Student{age=20, name='bbb'}
Student{age=20, name='bbb'}

上面代码直接使用了 “=” 对对象进行拷贝,发现,无论是基本类型还是引用类型的数据,只要其中一个对象的值被改变,另一个对象的数据也随之改变。

其实Student student1 = student;这一句代码,相当于只是将student的地址赋值给了student1,两个对象指向一个地址,所有数据都是一起共享的。

而实现浅拷贝之后,创建的新对象会单独开辟一个内存空间有自己的地址,这样对于基本类型的数据来说,每个对象都有自己的数据,而对于引用类型的数据来说,新对象是拷贝的引用类型的地址,所以两个对象是共享该成员变量的。

public class School implements Cloneable {
    String name;

    public School(String name) {
        this.name = name;
    }

	...
        
    @Override
    public String toString() {
        return "School{" +
                "name='" + name + '\'' +
                '}';
    }
}
public class Student implements Cloneable{
    private int age;
    private School school;

    public Student(int age, School school) {
        this.age = age;
        this.school = school;
    }

    ...

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", school=" + school +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class Tester {
    public static void main(String[] args) throws CloneNotSupportedException {
        Student student = new Student(10, new School("QUST"));
        Student student1 = (Student) student.clone();
        student.setAge(20);
        student.getSchool().setName("THU");
        System.out.println(student);
        System.out.println(student1);
    }
}

----Output----
Student{age=20, school=School{name='THU'}}
Student{age=10, school=School{name='THU'}}    

以上就是一个实现浅拷贝的例子,首先,需要拷贝的类要实现Cloneable接口(此接口是个标识性接口,接口内无任何方法),重写clone方法,在拷贝对象的时候,使用Student student = (Student) student.clone(),这样就实现了浅拷贝。从测试类的输出可以看出来,修改原对象的基本数据类型的成员变量时,拷贝对象并没有收到影响,而修改原对象引用类型成员变量School的属性时,拷贝类型也受到了影响。

实现深拷贝

public class School implements Cloneable{
    String name;

    public School(String name) {
        this.name = name;
    }
    
    @Override
    public String toString() {
        return "School{" +
                "name='" + name + '\'' +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class Student implements Cloneable{
    private int age;
    private School school;

    public Student(int age, School school) {
        this.age = age;
        this.school = school;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", school=" + school +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student student = (Student) super.clone();
        student.school = (School) this.school.clone();
        return student;
    }
}
public class Tester {
    public static void main(String[] args) throws CloneNotSupportedException {
        Student student = new Student(10, new School("QUST"));
        Student student1 = (Student) student.clone();
        student.setAge(20);
        student.getSchool().setName("THU");
        System.out.println(student);
        System.out.println(student1);
    }
}
----Output----
Student{age=20, school=School{name='THU'}}
Student{age=10, school=School{name='QUST'}}

实现深拷贝的方法就是将类中的引用类型变量也进行拷贝,引用类型所在的类实现Cloneable接口,重写clone方法。这样就可以实现深拷贝了。

还有一种实现深拷贝的方式,就是通过序列化实现可以查看之前的关于序列化的文章。

总结

  • 需要拷贝对象的时候,如果一个对象内部数据类型只有基本类型,那么就可以使用浅拷贝,如果一个对象内部还有引用数据类型,就需要使用深拷贝。
  • 虽然深拷贝和浅拷贝可能用到的不会很多,但是对于理解JVM内部内存还是很有帮助的????
相关标签: java