首頁 > 軟體

JAVA 物件建立與物件克隆

2022-02-17 19:01:13

一、物件的4種建立方式

  • new 建立
  • 反射
  • 克隆
  • 反序列化

二、通過new建立物件

一般情況下,物件通過new 關鍵字建立,首先會在堆上給物件分配空間,然後執行建構函式進行一系列的初始化,在分配的記憶體空間上為一眾屬性賦值;完成初始化後再將堆區物件的參照傳遞給棧區,最終參與程式的執行。

三、反射

呼叫Java.lang.Class或者java.lang.reflect.Constructor類的newInstance()實體方法。

四、克隆物件

它分為深拷貝和淺拷貝,通過呼叫物件的 clone方法,進行物件的克隆(拷貝)

我們可以看到 clone 方法的定義:

  • 首先它是一個 native 方法
  • 它被 protected 修飾,那麼,它的存取許可權就只有它的子類或者[同包:java.lang.*];
  • 雖然說類都繼承自Object,當物件A參照物件B,A沒法呼叫:B.clone() { B -> Object.clone() } 的,因為,Object上的clone()方法被protected修飾,
  • 若A一定要存取B的clone()方法:B重寫clone()為public修飾、或者通過public方法將 clone() 存取許可權傳遞出去。
  • 此外我們還需要實現:Cloneable介面,我們可以看它的定義,實際上空無一物,可以把Cloneable視為一個標記,若不實現它,當我們呼叫重寫的clone方法進行克隆時會丟擲異常:[java.lang.CloneNotSupportedException]

  • clone不會呼叫類的構造方法,它只是複製堆區上的物件資訊。

為了測試 clone 我定義了兩個類:

使用者資訊:UserBean

package com.bokerr.canaltask.po;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.Serializable;

@Data
@AllArgsConstructor
public class UserBean implements Cloneable, Serializable {

    private static final long serialVersionUID = 2022010901L;

    /** 基本型別、不可變型別 */
    private int age;
    private int sex;
    private String name;
    private String home;
    /** 參照型別 */
    private SubInfo subInfo;

    public UserBean(){}

    /***
     * 重寫 clone 為 public 讓任意物件都有 clone的存取許可權
     * */
    @Override
    public UserBean clone(){
        UserBean clone = null;
        try{
            clone = (UserBean) super.clone();
        } catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        return clone;
    }
}

附加資訊類:SubInfo

package com.bokerr.canaltask.po;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.Serializable;

@Data
@AllArgsConstructor
public class SubInfo implements Cloneable, Serializable {
    private static final long serialVersionUID = 2022010902L;
    /**
     * SubInfo 類的屬性都是基本型別、不可變物件(String)
     * */
    private String work;
    private Integer salary;
    private int idNum;

    public SubInfo(){}
    
    /**
     * 此處通過public 方法對外提供物件clone方法存取許可權
     * */
    public SubInfo selfClone(){
        try{
            return (SubInfo) clone();
        } catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        return null;
    }
    /*@Override
    public SubInfo clone(){
        try{
            return (SubInfo) super.clone();
        } catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        return null;
    }*/
}

淺拷貝

我們需要知道,單純呼叫一個物件的clone方法,它進行的是:"淺表複製",當物件的屬性都是基本型別或者不可變(final)型別時是沒有問題的;但是存在可變物件參照時,對它的拷貝並不是一個深層的拷貝,它只拷貝該物件的參照,這樣就會造成原物件和克隆物件的修改,都會反映到該參照物件上。

關於淺拷貝看如下測試程式碼:

package com.bokerr.canaltask.workerrun;

import com.bokerr.canaltask.po.SubInfo;
import com.bokerr.canaltask.po.UserBean;

public class ExecuteTest {

    public static void main(String[] args){
        UserBean userBean1 = new UserBean();
        userBean1.setAge(25);
        userBean1.setSex(1);
        userBean1.setName("bokerr");
        userBean1.setHome("貴州銅仁");

        SubInfo subInfo1 = new SubInfo();
        subInfo1.setIdNum(3423);
        subInfo1.setSalary(Integer.valueOf(15000));
        subInfo1.setWork("coder");
        userBean1.setSubInfo(subInfo1);

        System.out.println("userBean-orign:" + userBean1);
        UserBean userBean2 = userBean1.clone();
        userBean2.setHome("貴州貴陽");
        userBean2.setAge(26);
        SubInfo subInfo2 = userBean2.getSubInfo();
        subInfo2.setSalary(Integer.valueOf(25000));
        subInfo2.setWork("manager");
        subInfo2.setIdNum(100002);
        System.out.println("######################");
        System.out.println("userBean-orign:" + userBean1);
        System.out.println("userBean-clone:" + userBean2);
    }
}

UserBeanclone 方法定義如下,我們可以看見它只呼叫了super.clone 而對 super.clone 的返回值沒做任何修改:

@Override
    public UserBean clone(){
        UserBean clone = null;
        try{
            clone = (UserBean) super.clone();
        } catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        return clone;
    }

輸出如下,結合測試code,我們可以發現,克隆得到的物件對 SubInfo 的修改同樣體現到了原物件參照的 SubInfo 物件上,因為呼叫 super.clone 只是一個 "淺表複製"

userBean-orign:UserBean(age=25, sex=1, name=bokerr, home=貴州銅仁, subInfo=SubInfo(work=coder, salary=15000, idNum=3423))
######################
userBean-orign:UserBean(age=25, sex=1, name=bokerr, home=貴州銅仁, subInfo=SubInfo(work=manager, salary=25000, idNum=100002))
userBean-clone:UserBean(age=26, sex=1, name=bokerr, home=貴州貴陽, subInfo=SubInfo(work=manager, salary=25000, idNum=100002))

深拷貝

深拷貝生成的物件,必須擁有完全獨立的物件記憶體空間,拷貝物件和原物件上的修改,都不會影響對方;

前邊提到通過super.clone 呼叫 Object上的clone方法實際上進行的只是一個淺拷貝

為了實現深拷貝我們則必須修改 UserBean 的clone 方法:

/***
     * 重寫 clone 為 public 讓任意物件都有 clone的存取許可權
     * */
    @Override
    public UserBean clone(){
        UserBean clone = null;
        try{
            clone = (UserBean) super.clone();
            /** SubInfo.selfClone() 提供SubInfo物件clone()方法許可權 */
            /** 克隆可變參照物件 SubInfo,並賦值給 super.clone() 返回的:UserBean 完成深拷貝 */
            SubInfo cloneSub = this.subInfo.selfClone();
            clone.setSubInfo(cloneSub);
        } catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        return clone;
    }

實際上除此之外,測試程式碼一成不變,然後我們來看現在的輸出,可以發現對克隆物件的參照物件:SubInfo 的修改,並未使原物件的SubInfo變化

userBean-orign:UserBean(age=25, sex=1, name=bokerr, home=貴州銅仁, subInfo=SubInfo(work=coder, salary=15000, idNum=3423))
######################
userBean-orign:UserBean(age=25, sex=1, name=bokerr, home=貴州銅仁, subInfo=SubInfo(work=coder, salary=15000, idNum=3423))
userBean-clone:UserBean(age=26, sex=1, name=bokerr, home=貴州貴陽, subInfo=SubInfo(work=manager, salary=25000, idNum=100002))

此時問題來了:你可能會說假如我的物件進行了多層參照呢,且參照了多個物件該怎麼辦呢?那我只能一個一個去重寫 clone 方法麼?

是的你如果使用 clone 方法可能,你確實需要那樣去處理。

假如,存在如下以物件A為根節點的參照關係:

A  ->  B
       C -> E -> F
            G -> G
       D -> H 
            I -> J -> K

我相信處理深拷貝的人會瘋掉的。。。。

那麼有更省事的方法麼? 當然有,那就是下一節提到的反序列化。

五、反序列化

  • java中的序列化是將物件轉化成一個二進位制位元組序列,它可以持久化到磁碟檔案,也可通過網路進行傳輸;
  • 而反序列化是指將該二進位制位元組序列,重新還原成一個物件並載入到記憶體中的過程。
  • 物件的反序列化過程中,沒有呼叫任何建構函式,整個物件都是通過將,檔案流中取得的資料恢復,從而得來。
  • 序列化只儲存物件的屬性狀態,而不會儲存物件的方法。
  • 只有實現了Serializable介面的類才能被序列化,官方建議自定義一個SerialversionUID,若使用者沒有自定義SerialversionUID那麼會生成預設值;序列化和反序列化就是通過對比其SerialversionUID來進行的,一旦SerialversionUID不匹配,反序列化就無法成功
  • 如果一個類的屬性包含物件參照,那麼被參照的物件也將被序列化,[被參照的物件也必須實現Serializable介面,否則會丟擲異常:java.io.NotSerializableException]
  • statictransient修飾的變數不會被序列化,可以理解為:static是類屬性存在於方法區而不在堆區;transient常用於修飾涉及安全的資訊,它只能和Serializable介面一起使用,比如:使用者密碼,不應該讓它序列化之後在網路上傳輸。

參考程式碼:

package com.bokerr.canaltask.workerrun;

import com.bokerr.canaltask.po.NoCloneInfo;
import com.bokerr.canaltask.po.SubInfo;
import com.bokerr.canaltask.po.UserBean;
import org.apache.commons.lang3.SerializationUtils;

import java.util.Arrays;

public class ExecuteTest {

    public static void main(String[] args){
        UserBean userBean1 = new UserBean();
        userBean1.setAge(25);
        userBean1.setSex(1);
        userBean1.setName("bokerr");
        userBean1.setHome("貴州銅仁");
        SubInfo subInfo1 = new SubInfo();
        subInfo1.setIdNum(3423);
        subInfo1.setSalary(Integer.valueOf(15000));
        subInfo1.setWork("coder");
        userBean1.setSubInfo(subInfo1);

        System.out.println("序列化前" + userBean1);
        /** 物件序列化為二進位制位元組序列 */
        byte[] serializeBytes = SerializationUtils.serialize(userBean1);
        /** 反序列化還原為Java物件 */
        UserBean userBeanSer = SerializationUtils.deserialize(serializeBytes);
        userBeanSer.getSubInfo().setSalary(800000);
        userBeanSer.getSubInfo().setWork("CTO");
        System.out.println("反序列化" + userBeanSer);
        System.out.println(userBean1 == userBeanSer);
    }
}

輸出:

序列化前UserBean(age=25, sex=1, name=bokerr, home=貴州銅仁, subInfo=SubInfo(work=coder, salary=15000, idNum=3423))
反序列化UserBean(age=25, sex=1, name=bokerr, home=貴州銅仁, subInfo=SubInfo(work=CTO, salary=800000, idNum=3423))
false

我們可以發現最終輸出了:subInfo.work=CTO subInfo.salary=800000,對反序列化得到的物件參照的SubInfo物件的修改,並未影響到原物件,所以可以通過反序列化進行物件的深拷貝。

六、補充

PS*有一說一:lombok 是真的好用,雖然 set、get 方法可以自動生成,但是用 lombok後明顯程式碼更簡潔了 ;

commons-lang3 它是apache的一個工具包,我用的序列化工具來自它。

可能有小夥伴不瞭解,我還是貼一下Maven依賴吧,雖然我知道大家都人均大佬了。

<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
            <scope>compile</scope>
        </dependency>


        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>

到此這篇關於JAVA 物件建立與物件克隆的文章就介紹到這了,更多相關JAVA 物件的建立與克隆內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com