Java拷贝(克隆)原理

1. 基本类型和引用类型的区别

Java中的数据类型分为两大类,基本数据类型引用类型

1.1 基本数据类型

基本数据类型有八种类型,大致按照如下分类
①整数类型:long、int、short、byte
②浮点类型:float、double
③字符类型:char
④布尔类型:boolean

数据类型位数范围默认值
byte(字节型)8-128(-27)~127(27 - 1)0
short(短整型)16-32768(-215)~32767(215 - 1)0
int(整型)32-2147483648(-231)~2147483647(231 - 1)0
long(长整型)64-9223372036854775808(-263)~9223372036854775807(263 - 1)0L
float(单精度)32-3.4E38~3.4E380.0f
double(双精度)64-1.7E308~1.7E3080.0d
char(字符)16\u0000(十进制等效值为 0)~\uffff(即为 65535)‘\u0000’
boolean(布尔)1true或falsefalse

注:1字节等于8位,即1byte(字节)= 8bit(位)

1.2 引用类型

  • 变量一旦声明后,类型就不能被改变了。
  • 对象、数组都是引用数据类型。
  • 所有引用类型的默认值都是null。
  • 一个引用变量可以用来引用任何与之兼容的类型。
  • 例子:String str = new String(“hello world”)。

注:在Java中,基本类型都有对应的封装类:

数据类型封装类
byteByte
shortShort
longLong
intInteger
floatFloat
doubleDouble
charCharacter
booleanBoolean

这样用的原因跟泛型必须包容的是引用类型有关,然后咱们经常用的集合List、Set有些使用到了泛型,还有一个很重要的点就是像int默认值是0,Integer默认值是null,咱们可以通过这一点去判断是否有输入数据,因为在数据库里null是不会显示值的,当然你也可以做多一步逻辑判断。

1.3 区别

先看一段实例代码(建议用idea最后一行打断点看地址):

public class SpaceTest {
    public static void main(String[] args) {
        int a = 1;
        Integer b = 3;
        Integer[] integers = new Integer[3];
        int[] arr = new int[3];
        arr[0] = a;
        integers[0] = b;
    }
}

可以看到这里我定义了两个基本数据类型int->a,对应的(引用类型)封装类Integer->b,一个int数组arr,一个Integer数组integers,通过debug打断点可以看到具体的地址是什么。
debug结果

根据上图咱们看到

  • a是没有地址的只有数值,然后对象b、数组integers、arr是有地址分配的
  • 数组arr存放的是基本数据类型所以里面的值也是没有分配地址的,并且没赋值的下标是有初始化值0的
  • 数组integers里面存放的也是引用类型,所以下标0赋值b是有地址的,并且跟b地址一致,其余没赋值的下标均为null

main()方法体内的局部变量是存放在栈里,引用类型是存放在堆里,咱们画图分析一下:

在这里插入图片描述

2. 序列化原理

序列化相关可以看我另一篇:Java序列化和反序列化

3. 浅拷贝的原理和实现

为了方便理解,咱们这里先创建一个Student类:

public class Student {
    int id;
    String name;
    int age;
    Student(int id,String name,int age){
        this.id = id;
        this.name = name;
        this.age = age;
    }
    
	public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setId(int id) {
        this.id = id;
    }

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

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

3.1 浅拷贝的实现方式

  • 实现Cloneable接口,并重写clone()方法

这里对Student类进行改造,实现Cloneable接口并重写clone()方法

public class Student implements Cloneable{
    int id;
    String name;
    int age;
    Student(int id,String name,int age){
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setId(int id) {
        this.id = id;
    }

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

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

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

main方法

public class SpaceTest {
    public static void main(String[] args) throws CloneNotSupportedException{
        Student stu1 = new Student(1,"张三",18);
        Student stu2 = (Student) stu1.clone();
        System.out.println(stu1);
        System.out.println(stu2);
    }
}

运行结果

clone.test.Student@29453f44
clone.test.Student@5cad8086

可以看出两个对象的地址不同,这里就实现了对象的拷贝了。

3.2 原理

浅拷贝是对象拷贝,网上把基本类型和对象的直接赋值也称为浅拷贝

Student a = new Student(1,"张三",18);
Student b = a;

我认为拷贝你起码对象是创建了一个新的出来,而不是两个对象都指向同一个地址(本质上还是一个对象),浅拷贝的原理其实就是new一个新对象b(开辟新的内存空间),并把已存在的相同引用对象a的成员变量值赋予b,例如3.1

Student a = new Student(1,"张三",18);
Student b = (Student) a.clone();

4. 深拷贝的原理和实现

浅拷贝深拷贝都是对象拷贝,但是浅拷贝只能拷贝外层的成员变量,如果你的成员变量有一个引用类型你修改这个引用对象会发现浅拷贝是两个对象一起变相同的值

public class SpaceTest {
    public static void main(String[] args) throws CloneNotSupportedException{
        Teacher tea1 = new Teacher(10,"赵老师",25);
        Student stu1 = new Student(1,"张三",18,tea1);
        System.out.println(stu1.toString());
        Student stu2 = (Student) stu1.clone();
        stu2.setName("李四");
        stu2.setAge(20);
        stu2.getTeacher().setName("李老师");
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}

运行结果,会发现这里你改变stu2对象的Teacher,stu1也会随着更改。

Student{id=1, name='张三', age=18, teacher=Student{id=10, name='赵老师', age=25}}
Student{id=1, name='张三', age=18, teacher=Student{id=10, name='李老师', age=25}}
Student{id=1, name='李四', age=20, teacher=Student{id=10, name='李老师', age=25}}

深拷贝才能真正实现完全拷贝,先新增一个Teacher类。

public class Teacher {
    int id;
    String name;
    int age;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    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;
    }
}

改造一下Student类,新增引用类型Teacher的成员变量和getter、setter方法、重载构造器

public class Student implements Cloneable{
    int id;
    String name;
    int age;
    Teacher teacher;
    Student(int id,String name,int age){
        this.id = id;
        this.name = name;
        this.age = age;
    }
    Student(int id,String name,int age,Teacher teacher){
        this.id = id;
        this.name = name;
        this.age = age;
        this.teacher = teacher;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setId(int id) {
        this.id = id;
    }

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

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

    public Teacher getTeacher() {
        return teacher;
    }

    public void setTeacher(Teacher teacher) {
        this.teacher = teacher;
    }
    
	@Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", teacher=" + teacher +
                '}';
    }
	
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

4.1 深拷贝的两种实现方式

  1. 创建新的引用类型对象(成员变量)

这里直接new一个新的Teacher对象tea2,再给stu2对象setTeacher(tea2)

public class SpaceTest {
    public static void main(String[] args) throws CloneNotSupportedException{
        Teacher tea1 = new Teacher(10,"赵老师",25);
        Teacher tea2 = new Teacher(20,"李老师",30);
        Student stu1 = new Student(1,"张三",18,tea1);
        System.out.println(stu1.toString());
        Student stu2 = (Student) stu1.clone();
        stu2.setName("李四");
        stu2.setAge(20);
        stu2.setTeacher(tea2);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}

运行结果

Student{id=1, name='张三', age=18, teacher=Student{id=10, name='赵老师', age=25}}
Student{id=1, name='张三', age=18, teacher=Student{id=10, name='赵老师', age=25}}
Student{id=1, name='李四', age=20, teacher=Student{id=20, name='李老师', age=30}}

这里就真正实现了深拷贝,stu1对象并不会因为stu2的setTeacher(tea2)而改变自己的teacher引用,问题是你不可能因为引用类里面有几个引用类型的成员变量就去new多少个吧,这样太麻烦,咱们可以看2的实现方式。

  1. 直接调用工具类Hutool提供的方法(序列化和反序列化)

用过maven的可以直接pom文件编写依赖,导入hutool工具

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.5.1</version>
        </dependency>

Student类和Teacher都实现Serializable接口

public class Student implements Cloneable, Serializable
public class Teacher implements Serializable

改造main方法的拷贝方式

public class SpaceTest {
    public static void main(String[] args) throws CloneNotSupportedException{
        Teacher tea1 = new Teacher(10,"赵老师",25);
        Student stu1 = new Student(1,"张三",18,tea1);
        System.out.println(stu1.toString());
        Student stu2 = ObjectUtil.cloneByStream(stu1);
        stu2.setName("李四");
        stu2.setAge(20);
        stu2.getTeacher().setName("马老师");
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}

运行结果

Student{id=1, name='张三', age=18, teacher=Student{id=10, name='赵老师', age=25}}
Student{id=1, name='张三', age=18, teacher=Student{id=10, name='赵老师', age=25}}
Student{id=1, name='李四', age=20, teacher=Student{id=10, name='马老师', age=25}}

可以看到利用ObjectUtil.cloneByStream方法也能实现深拷贝,原理看4.2

4.2 原理

为什么浅拷贝和深拷贝的结果不一样,是因为他们的内存结构导致的,浅拷贝只是复制了当前对象,但是对象里面存在另一个对象的时候是不会去复制的(指向同一个地址),所以当你赋值给该对象stu2的teacher是会导致stu1的teacher跟着变化的

浅拷贝
在这里插入图片描述

深拷贝

在这里插入图片描述
可以看出如果teacher地址不同的话指向的是不同的数据,所以new一个或者利用序列化和序列化都是可以实现的,至于ObjectUtil.cloneByStream方法是如何实现深拷贝的,咱们这里可以看一下源码

    public static <T> T cloneByStream(T obj) {
        if (null != obj && obj instanceof Serializable) {//判断被拷贝对象是否实现了Serializable接口(序列化条件)
            FastByteArrayOutputStream byteOut = new FastByteArrayOutputStream();
            ObjectOutputStream out = null;

            Object var4;
            try {
                out = new ObjectOutputStream(byteOut);
                out.writeObject(obj);//将序列化后的对象写进out
                out.flush();
                ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(byteOut.toByteArray()));//通过byteOut转为字节数组实例化in(输入流)
                var4 = in.readObject(); //var4从输入流in读取对象
            } catch (Exception var8) {
                throw new UtilException(var8);
            } finally {
                IoUtil.close(out);
            }

            return var4;
        } else {
            return null;
        }
    }

为什么要序列化来进行深拷贝,我认为是序列化可以把你整个对象给重新开辟新的空间(包括成员变量)并赋值,这样就能实现深拷贝而且代码也简单,序列化是将对象变为二进制的形式存储,反序列化是把字节序列恢复为对象,这样它是从JVM虚拟机出来又进去,那地址肯定是有变化的。

总结:
浅拷贝只是复制对象(地址不同),而不复制成员属性(引用类型)。
深拷贝是完全复制对象,包括成员属性是引用类型。

有问题欢迎指正,这篇文章我查阅的资料说法不一,我是加了自己的理解去写的。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值