深拷贝与浅拷贝

2020/09/23
共 1.5k 字
约 6 分钟
归档: 学习
标签: java基础

Java中深拷贝和浅拷贝的区别?网上有两句很简短的解释

浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。

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

字面意思似乎很好理解,而实际在java中怎么实现,不动手敲一下心里总会不舒服于是有了下面的尝试与研究


基本数据类型和引用数据类型

Java中的数据类型分为基本数据类型和引用数据类型。

基本数据类型有8种

整型 byte short int long
浮点型 float double
布尔型 boolean(true、false)
字符型 char

剩下的类、 接口类型、 数组类型、 枚举类型、 注解类型、 还有非常常见的Stirng类型都是引用类型。

值传递与引用传递

值传递:指在调用方法时将实际参数的值拷贝一份传递给方法,这样方法在修改参数的值时就不会影响到实际的值。

引用传递:指将实际参数的引用地址直接传递给方法中,这样在方法中如果通过该地址修改数据会影响到实际地址的值。

浅拷贝

有了上面两个解析,开头两句话的字面意思就很好理解了:浅拷贝就是一个引用,令b=a,修改b同时也会影响a,因为两者在内存中的地址一样;而深拷贝就是开辟一个新的内存空间,两个是独立的对象。

下面我们有一个User类

class User {
    private String name;
    private int age;

    public User() {
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
	//getter、setter省略
}

然后有一个浅拷贝方法

public class ShallowClone {
    public static void main(String[] args) {
        User user1 = new User();
        user1.setName("user1");
        user1.setAge(11);
        //浅拷贝
        User user2 = user1;

        System.out.println(user1);
        System.out.println(user2);
        //修改user2
        user2.setName("user2");
        user2.setAge(12);
        System.out.println("user1:"+user1.getName()+" "+user1.getAge());
        System.out.println("user2:"+user2.getName()+" "+user2.getAge());
    }
}

输出结果:

someTest.User@1540e19d
someTest.User@1540e19d
user1:user2 12
user2:user2 12

从上可见二者的引用是同一个对象。并且修改user2的类容时,user1的也会跟着改变

这个就是浅拷贝

深拷贝

实现深拷贝有多个方法,常见的有

  1. 实现Cloneable接口,并且重写Object类中的clone()方法

  2. 实现Serializable接口序列化

实现Cloneable接口

让User类实现Cloneable接口,并重写clone()方法

class User implements Cloneable {
    private String name;
    private int age;

    public User() {
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
	//getter、setter省略
	
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

主函数抛CloneNotSupported的异常,调用clone()方法实现深拷贝

public static void main(String[] args) throws CloneNotSupportedException {
    User user1 = new User();
    user1.setName("user1");
    user1.setAge(11);
    
    //深拷贝
    User user3 = (User) user1.clone();
    System.out.println(user1);
    System.out.println(user3);
    //修改user3的age
    user3.setAge(13);
    System.out.println("user1 age:"+user1.getAge());
    System.out.println("user3 age:"+user3.getAge());
}

输出结果:

someTest.User@1540e19d
someTest.User@677327b6
user1 age:12
user3 age:13

二者的对象地址不一样,修改user3也不会影响user1,实现了深拷贝

但是还是有个问题,java中String类型为引用类型,User类中有一个String类型的引用对象name
开头讲到,对引用数据类型,创建一个新的对象,我们看看事实上是怎样的

System.out.println(user1.getName().hashCode());
System.out.println(user3.getName().hashCode());

输出结果:

111578567
111578567

可见,二者的name属性依然是指向同一个对象理论上说,如果想要name也实现深拷贝,得重写String的clone() 方法,而String类又是final的,所以无法重写

但String又是比较特殊的:一个对象调用clone方法,克隆出一个新对象,这时候两个对象的同一个String类型的属性是指向同一片内存空间的,但是如果改变了其中一个,会产生一片新的内存空间,此时该对象的这个属性的引用将指向这片新的内存空间

我们尝试改变user3的name字段

user3.setName("user3");
System.out.println(user1.getName().hashCode());
System.out.println(user3.getName().hashCode());

输出结果:

111578567
111578568

可见,当我们改变user3的name的时候,会开辟新的内存空间,简单地说String在这里可以当做基本类型来使用。

实现Serializable接口序列化

上面这种做法有个弊端,有多少个引用类型,我们就要重写多少次,如果存在很多引用类型,代码量将会非常大

这次我们新建一个User2类,实现Serializable接口

class User2 implements Serializable {
    private String name;
    private int age;

    public User2() {
    }

    public User2(String name, int age) {
        this.name = name;
        this.age = age;
    }
    //省略getter/setter
    //省略toString
}

深拷贝的实现

public class DeepClone {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        User2 u1 = new User2("jack",18);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        //序列化
        oos.writeObject(u1);
        oos.flush();
        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        //深拷贝
        User2 u2 = (User2) ois.readObject();
        System.out.println("修改前");
        System.out.println(u1);
        System.out.println(u2);
        u2.setName("newName");
        u2.setAge(10);
        System.out.println("修改后");
        System.out.println(u1);
        System.out.println(u2);
    }
}

输出结果:

修改前
User2{name='jack', age=18}
User2{name='jack', age=18}
修改后
User2{name='jack', age=18}
User2{name='newName', age=10}

需要注意的是,如果User2类中还包含了其他非基本类,例如还有个Address类,则这个Address类也要实现Serializable接口,即源对象类型及其成员对象类型需要实现Serializable接口,这样才能实现两个不同的User2对象,各自引用着不同的Address对象。

参考自:

https://blog.csdn.net/zhouxuanwushini/article/details/90446668
https://www.cnblogs.com/yanayo/p/javaHome.html

留言

本站已运行
© 2024 Jack  由 Hexo 驱动
复制成功