如何是实现对象之间的拷贝,复制========深拷贝,浅拷贝

一:概念
浅拷贝:创建了一个对象,但是这个对象的某些内容(比如A)依然是被拷贝对象的,即通过这两个对象中任意一个修改A,两个对象的A都会受到影响
深拷贝:相当于创建了一个新的对象,只是这个对象的所有内容,都和被拷贝的对象一模一样而已,但是两者是相互独立的,是不同的地址值,其修改是隔离的,相互之间没有影响
在这里插入图片描述
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

二:数据类型的比较
数据类型分为:基本数据类型 与 引用数据类型
基本数据类型:四类八种(byte ,short,int,long double,float,char,boolean)
基本数据类型
引用数据类型:引用数据类型有:类、接口类型、数组类型、枚举类型、注解类型。

三:在内存中的位置:
1:基本数据类型:直接存储在栈(stack)中的数据
2:引用数据类型:栈中存储的是该对象的引用(是地址值),真实的数据存放在堆内存里
3:赋值与浅拷贝的区别:
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
虚拟机
四:实现对象克隆的几个方式
(1)将A对象的值分别通过set方法加入B对象中;
(2)通过重写java.lang.Object类中的方法clone();
(4)通过序列化实现对象的复制。
代码举例:
1>set赋值方式:

Student stu1 = new Student();  
stu1.setNumber(12345);  
Student stu2 = new Student();  
stu2.setNumber(stu1.getNumber());

2>重写克隆方法clone 需要实现cloneable接口
浅克隆:不支持引用数据类型的复制

被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常),
该接口为标记接口(不含任何方法)
覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象。(native为本地方法)

class Student implements Cloneable{  
    private int number;  
  
    public int getNumber() {  
        return number;  
    }  
  
    public void setNumber(int number) {  
        this.number = number;  
    }  
      
    @Override  
    public Object clone() {  
        Student stu = null;  
        try{  
            stu = (Student)super.clone();  
        }catch(CloneNotSupportedException e) {  
            e.printStackTrace();  
        }  
        return stu;  
    }  
}  
public class Test {  
    public static void main(String args[]) {  
        Student stu1 = new Student();  
        stu1.setNumber(12345);  
        Student stu2 = (Student)stu1.clone();  
          
        System.out.println("学生1:" + stu1.getNumber());  
        System.out.println("学生2:" + stu2.getNumber());  
          
        stu2.setNumber(54321);  
      
        System.out.println("学生1:" + stu1.getNumber());  
        System.out.println("学生2:" + stu2.getNumber());  
    }  
}  

在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。
简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。

3>深克隆代码对比举例(先浅克隆后深克隆)

class Address {  
    private String add;  
  
    public String getAdd() {  
        return add;  
    }  
  
    public void setAdd(String add) {  
        this.add = add;  
    }   
}  
  
class Student implements Cloneable{  
    private int number;  
  
    private Address addr;  
      
    public Address getAddr() {  
        return addr;  
    }  
  
    public void setAddr(Address addr) {  
        this.addr = addr;  
    }  
  
    public int getNumber() {  
        return number;  
    }  
  
    public void setNumber(int number) {  
        this.number = number;  
    }  
      
    @Override  
    public Object clone() {  
        Student stu = null;  
        try{  
            stu = (Student)super.clone();   //浅复制  
        }catch(CloneNotSupportedException e) {  
            e.printStackTrace();  
        }   
        return stu;  
    }  
}  
public class Test {  
      
    public static void main(String args[]) {  
          
        Address addr = new Address();  
        addr.setAdd("杭州市");  
        Student stu1 = new Student();  
        stu1.setNumber(123);  
        stu1.setAddr(addr);  
          
        Student stu2 = (Student)stu1.clone();  
          
        System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
        System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
          
        addr.setAdd("西湖区");  
          
        System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
        System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
    }  
}

结果:

学生1:123,地址:杭州市
学生2:123,地址:杭州市
学生1:123,地址:西湖区
学生2:123,地址:西湖区

深克隆

class Address implements Cloneable {  
    private String add;  
  
    public String getAdd() {  
        return add;  
    }  
  
    public void setAdd(String add) {  
        this.add = add;  
    }  
      
    @Override  
    public Object clone() {  
        Address addr = null;  
        try{  
            addr = (Address)super.clone();  
        }catch(CloneNotSupportedException e) {  
            e.printStackTrace();  
        }  
        return addr;  
    }  
}  
  
class Student implements Cloneable{  
    private int number;  
  
    private Address addr;  
      
    public Address getAddr() {  
        return addr;  
    }  
  
    public void setAddr(Address addr) {  
        this.addr = addr;  
    }  
  
    public int getNumber() {  
        return number;  
    }  
  
    public void setNumber(int number) {  
        this.number = number;  
    }  
      
    @Override  
    public Object clone() {  
        Student stu = null;  
        try{  
            stu = (Student)super.clone();   //浅复制  
        }catch(CloneNotSupportedException e) {  
            e.printStackTrace();  
        }  
        stu.addr = (Address)addr.clone();   //深度复制  
        return stu;  
    }  
}  
public class Test {  
      
    public static void main(String args[]) {  
          
        Address addr = new Address();  
        addr.setAdd("");  
        Student stu1 = new Student();  
        stu1.setNumber(123);  
        stu1.setAddr(addr);  
          
        Student stu2 = (Student)stu1.clone();  
          
        System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
        System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
          
        addr.setAdd("西湖区");  
          
        System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
        System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
    }  
}

结果:

学生1:123,地址:杭州市
学生2:123,地址:杭州市
学生1:123,地址:西湖区
学生2:123,地址:杭州市

在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。
简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。

4>:使用序列化和反序列化方式实现对象深度克隆
使用Serializable接口

利用序列化机制实现深克隆相比较重写clone()方法来说要安全、简单,但是效率不高。因为clone()方法实现的克隆是利用的本地方法,效率比基于Java虚拟机规范的序列化机制要高很多。

1.对象实现序列化,其和成员对象类都需要实现Serializable接口,标记接口,开启序列化,没有提供任何方法。
2.利用ObjectInputStream和ObjectOutputStream实现对象的反序列化与序列化
序列化与反序列化概念:
序列化:把对象转换为字节序列的过程称为对象的序列化
反序列化:把字节序列恢复为对象的过程称为对象的反序列化

什么时候会用到序列化呢?一般在以下的情况中会使用到序列化

===对象的持久化:把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中
在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

===远程调用:在网络上传送对象的字节序列
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

===序列化的基本实现
只要对象实现了Serializable接口,对象的序列化就会变得十分简单。要序列化一个对象首先要创建某些OutputStream对象,然后将其封装在一个ObejctOutputStream对象内,这时只需要调用writeObject()即可将对象序列化,并将其发送给OutputStream。

对象序列化是基于字节的,所以要使用InputStream和OutputStream继承层次结构

如果要反向上面的过程(即将一个序列还原为一个对象),需要将一个InputStream封装在ObjectInputStream内,然后调用readObject(),和往常一样,我们最后获得是一个引用,它指向了一个向上转型的Object,所以必须向下转型才能直接设置它们。

对象序列化不仅能够将实现了接口的那个类进行序列化,也能够将其引用的对象也实例化,以此类推。这种情况可以被称之为对象网。单个对象可与之建立连接。

我们举个例子可以看到在序列化和反序列过程中,对象网中的连接的对象信息都没有变。

public class TestSerializable {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        String fileName = "/Users/doc/test.sql";
        Worm w = new Worm(6,'a');
        System.out.println("w:"+w);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(fileName));
        out.writeObject("Worm Storage\n");
        out.writeObject(w);
        out.close();
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(fileName));
        String s = (String) in.readObject();
        Worm w2 = (Worm) in.readObject();
        System.out.println(s+"w2:"+w2);
    }
}
class Data implements Serializable{
    private Integer i ;
    public Data(Integer i ){
        this.i = i;
    }
    @Override
    public String toString() {
        return i.toString();
    }
}
class Worm implements Serializable{
    private static final long serialVersionUID = 8033549288339500180L;
    private static Random random = new Random(47);
    private Data [] d = {
            new Data(random.nextInt(10)),
            new Data(random.nextInt(10)),
            new Data(random.nextInt(10))
    };
    private Worm next;
    private char c;
    public Worm(int i ,char x){
        System.out.println("Worm Constructor:"+i);
        c = x;
        if (--i>0){
            next = new Worm(i,(char)(x+1));
        }
    }
    public Worm(){
        System.out.println("Default Constructor");
    }
    @Override
    public String toString() {
        StringBuffer result = new StringBuffer(":");
        result.append(c);
        result.append("(");
        for (Data data: d){
            result.append(data);
        }
        result.append(")");
        if (next!=null){
            result.append(next);
        }
        return result.toString();
    }
}

可以看到打印信息如下

Worm Constructor:6
Worm Constructor:5
Worm Constructor:4
Worm Constructor:3
Worm Constructor:2
Worm Constructor:1

w::a(853):b(119):c(802):d(788):e(199):f(881)
Worm Storage
w2::a(853):b(119):c(802):d(788):e(199):f(881)
在生成Data对象时是用随机数初始化的,从输出中可以看出,被还原后的对象确实包含了原对象中的所有链接。

上面我们举了个如何进行序列化的例子,其中或许看到了serialVersionUID 这个字段,如果不加的话,那么系统会自动的生成一个,而如果修改了类的话,哪怕加一个空格那么这个serialVersionUID 也会改变,那么在反序列化的时候就会报错,因为在反序列化的时候会将serialVersionUID 和之前的serialVersionUID 进行对比,只有相同的时候才会反序列化成功。所以还是建议显视的定义一个serialVersionUID 。

transient(瞬时)关键字

当我们在对序列化进行控制的时候,可能需要某个字段不想让Java进行序列化机制进行保存其信息与恢复。如果一个对象的字段保存了我们不希望将其序列化的敏感信息(例如密码)。尽管我们使用private关键字但是如果经过序列化,那么在进行反序列化的时候也是能将信息给恢复过来的。我们举个例子如下:

我们定义个Student类

class Student implements Serializable{
    private static final long serialVersionUID = 1734284264262085307L;
    private String password;
------get set 方法
}

然后将其序列化到文件中然后再从文件中反序列化

public static void main(String[] args) throws IOException, ClassNotFoundException {
    String fileName="/Users/doc/test.sql";
    Student student = new Student();
    student.setPassword("123456");
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(fileName));
    objectOutputStream.writeObject(student);
    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(fileName));
    Student readStudent = (Student) objectInputStream.readObject();
    System.out.println(readStudent.getPassword());
}

然后发现输出为

readStudent的password=123456
此时我们如果想password参数在序列化的时候存储其值,那么可以加上transient关键字,就像下面一样

private transient String password;

然后输出如下

readStudent的password=null

发现在序列化的时候参数就已经没被保存进去了
五:实现对象克隆的几个工具类:

在做业务的时候,为了隔离变化,我们会将DAO查询出来的DO和对前端提供的DTO隔离开来。大概90%的时候,它们的结构都是类似的;但是我们很不喜欢写很多冗长的b.setF1(a.getF1())这样的代码,于是我们需要简化对象拷贝方式。

  1. BeanUtils(简单,易用,Spring 包下的BeanUtil包更加稳定)
  2. BeanCopier(加入缓存后和手工set的性能接近)
  3. Dozer(深拷贝)
  4. fastjson(特定场景下使用)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值