谈谈java里的深拷贝和浅拷贝

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

java中主要有8种基本数据类型如下:
整型:byte,short,int,long
浮点型:float,double
字符型:char
布尔型:boolean

基本数据类型特点

  1. 基本数据类型不是对象
  2. 可移植性高,Java的基本类型所占空间大小是固定的,不会随着机器硬件架构改变
  3. 都有其对应的包装类,基本类型和包装类之间可以自动转换
  4. 基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象

引用类型特点

在Java中,引用类型的变量非常类似于C/C++的指针。引用类型指向一个对象,指向对象的变量是引用变量。这些变量在声明时被指定为一个特定的类型,比如 User、Shop 等。变量一旦声明后,类型就不能被改变了。
对象、数组都是引用数据类型。
所有引用类型的默认值都是null。
一个引用变量可以用来引用任何与之兼容的类型。
例子:User user = new User(“lily”)。

对应上述类型的不同,Java中的对象拷贝主要分为:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)。

浅拷贝

对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。
对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
如下图所示: :在这里插入图片描述
代码运行如下:

  public static void main(String[] args) {
       int x=7;
       fun(x);
        System.out.println("x="+x);
    }

    public static int fun(int z){
        z=z+10;
        System.out.println("z="+z);
        return  z;
    }

输出结果如下:
z=17
x=7

举例浅拷贝:

1通过构造函数进行浅拷贝,直接通过一个现有的对象创建出与该对象属性相同的新的对象。

代码参考如下:

@Data
public class User {
    int id;
    Age age;
    public User() {
    }
    public User(Age age,int id) {
        this.age = age;
        this.id=id;
    }
    public User(User user) {
        this.id=user.id;
        this.age=user.age;
	}
}
@Data
public class Age {
    int num;
    public Age(){}
    public Age(int num){
        this.num=num;
    }
}

从上述代码中可以看到,创建User对象的时候,包含了Age对象。如下
在这里插入图片描述
通过构造拷贝方法,拷贝一个对象如下:

   public static void main(String[] args) {
        Age age=new Age(10);
        User userOld=new User(age,1000);
        System.out.println("before clone userOld age:"+userOld.getAge().getNum());
        User userNew=new User(userOld);
        userNew.setId(1001);
        userNew.getAge().setNum(12);
        System.out.println("after clone userNew age:"+userNew.getAge().getNum());
        System.out.println("after clone userOld age:"+userOld.getAge().getNum());
        
    }

分析上述 构造构造拷贝的过程
User userNew=new User(userOld);
可以得出如下结论:

1.userOld和userNew指向了不同的内存地址
2. this.id=user.id;
this.age=user.age;这两句在拷贝的过程中,第一行数据值传递,第二行数据引用传递
将上述画成对象图如下:
在这里插入图片描述
于是运行上述代码,运行出来的结果如下:
before clone userOld age:10
after clone userNew age:12
after clone userOld age:12
结果分析:这里对User类选择了两个具有代表性的属性值:一个是引用传递类型;另一个是基本类型值传递。

通过拷贝构造方法进行了浅拷贝,各属性值成功复制。其中,userOld值传递部分的属性值发生变化时,userNew不会随之改变;而引用传递部分属性值发生变化时,userNew也随之改变。

浅拷贝带来的问题
1)如果类的成员变量包含或者全部是基本数据类型,拷贝时直接传递值,那么没有任何问题;
2)如果类的成员变量引用类型的数据,如上述中的Age对象,那么对这个类的对象使用时就要注意了,浅拷贝只会拷贝引用本身,而不会拷贝引用所指向的对象,这样就会导致多个对象同时持有指向某个对象的指引用,容易引发数据修改的混乱。如上述程序

有哪些容易混淆的问题
1)容易忽略的集合的浅拷贝
将上述Age对象装进一个List,并且对该List进行拷贝如下

        public static void main(String[] args) {
        Age age1=new Age(10);
        Age age2=new Age(12);
        List<Age> ageListOld= Lists.newArrayList(age1,age2);
        List<Age> ageListNew=new ArrayList<Age>(ageListOld);
        System.out.println("before ageListOld.age1:"+ageListOld.get(0).getNum());
        ageListNew.get(0).setNum(13);
        System.out.println("after ageListOld.age1:"+ageListOld.get(0).getNum());
        System.out.println("after ageListNew.age1:"+ageListNew.get(0).getNum());

    }

表面上将ageList传给ageListNew,ageListNew指向了一个新的地址,但是ageListNew里面的对象中引用都是指向原来的对象,很可能造成修改新的ageListNew,把原来的对象也修改了。把上述拷贝过程,画图如下:
在这里插入图片描述
于是,如果一个集合这样拷贝就是一个浅拷贝,实现的结果如下:

before ageListOld.age1:10
after ageListOld.age1:13
after ageListNew.age1:13

2)String拷贝是浅拷贝吗
首先要弄清楚String类型的内存存储模型,java中的内存有栈内存和堆内存。栈内存中存放变量名这些东西,存放的是对对象的引用,而堆内存中存放的是实际的对象。同时除了这两个以外,java中还有一个叫做常量池的东西。一些常量,将会放在这里面
常量池:JVM为每个已加载的类型维护一个常量池,常量池就是这个类型用到的常量的一个有序集合。包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用。池中的数据和数组一样通过索引访问。由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用。常量池存在于堆中。

于是当发生String类型的变量赋值时,就会出现如下的情况

String str0 = "hello";
String str1 = "hello";
String str2 = new String("hello");

上述三行代码解释如下:
从JDK1.7版本之后,java String池设置在堆中;

String str0 = “hello”;可能创建一个或者不创建对象。
如果”hello”这个字符串在java String池里不存在,会在java String池里创建一个String对象(“hello”),然后str0指向这个内存地址。
String str0 = “hello”;如果"hello"这个字符串在java string池中存在,则不创建对象, str0指向字符串池中的"hello"内存的地址。所以
String str1 = “hello”;str1不创建新的对象, str1指向字符串池中的"hello"内存的地址。也就是str0==str1
String str2 = new String(“hello”);至少创建一个对象,也可能两个。因为用到new关键字,肯定会在heap(堆)中创建一个str2的String对象,它的value是“hello”。同时如果这个字符串在java String池里不存在,会在java池里创建这个String对象“hello”。本例中因为上述已经在常量池中创建了hello对象,所以str2只会创建一个对象。内存图如下:
在这里插入图片描述
根据上述分析,如果发生拷贝事件时,例如:

        1)String str0 = "hello";
        2)String str1 = "hello";
        3)String str2 = new String("hello");
        4)String str3=str1;
        5)String str3="hel";
        6)String str4=new String(str2);

1)str0指向常量池里的一个内存地址
2)str1指向常量池的内存地址并且是str0指向的那个地址
3) str2指向堆里的某一个内地址且不等于str0,str1
4)str3指向str1,str0指向的内存地址
5)str3重新指向常量池里的另一个地址,跟str0,str1都没关系,所以str3的值发生改变不会影响str0,str1
6)str4的值来自str2,但是在堆内存中重新申请了一块地址,与str2没关系,str4的改动与str2没关系
所以无论是哪种形式的String类型的变量的拷贝,最终都不会影响原来的变量值。String类型变量的拷贝是深拷贝。

深拷贝:

首先介绍对象图的概念。设想一下,一个类有一个对象,其成员变量中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象,直到一个确定的实例。这就形成了对象图。那么,对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝!
简单地说,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。

目前常用的深拷贝方式:

1)序列化(serialization)这个对象,再反序列化回来,就可以得到这个新的对象,无非就是序列化的规则需要我们自己来写。
2)继续利用 clone() 方法,既然 clone() 方法,是我们来重写的,实际上我们可以对其内的引用类型的变量,再进行一次 clone()。
继续改写上面的 Demo ,让 ChildClass 也实现 Cloneable 接口。

public class CloneUtils {
    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T clone(T obj){
        T cloneObj = null;
        try {
            //写入字节流
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ObjectOutputStream obs = new ObjectOutputStream(out);
            obs.writeObject(obj);
            obs.close();
            
            //分配内存,写入原始对象,生成新对象
            ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(ios);
            //返回生成的新对象
            cloneObj = (T) ois.readObject();
            ois.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cloneObj;
    }
}

同时相应的对象必须实现Serializable接口

public class User implements Serializable {
   int id;
   Age age;


   public User() {
   }

   public User(Age age,int id) {
       this.age = age;
       this.id=id;
   }

   public User(User user) {
       this.id=user.id;
       this.age=user.age;

   }
}
@Data
public class Age implements Serializable{
   int num;
   public Age(){}
   public Age(int num){
       this.num=num;
   }
}
public static void main(String[] args) {
       Age age=new Age(10);
       User userOld=new User(age,1000);
       System.out.println("before clone userOld age:"+userOld.getAge().getNum());
       User userNew=CloneUtils.clone(userOld);
       userNew.getAge().setNum(12);

       System.out.println("after clone userNew age:"+userOld.getAge().getNum());
       System.out.println("after clone userOld age:"+userNew.getAge().getNum()/**/);
       
   }

深拷贝之后运行结果如下:

before clone userOld age:10
after clone userNew age:10
after clone userOld age:12

参考问下:
[1]: https://www.cnblogs.com/qlky/p/7348353.html
[2]: https://www.cnblogs.com/xingzc/p/9646923.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值