如何是實現對象之間的拷貝,複製========深拷貝,淺拷貝

一:概念
淺拷貝:創建了一個對象,但是這個對象的某些內容(比如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(特定場景下使用)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章