安卓/Java對象拷貝(淺/深拷貝、兩種序列化、Beans等工具)

文/阿敏其人
本文出自阿敏其人簡書博客,轉(zhuǎn)載請與本人聯(lián)系。


為什么要拷貝對象

我們干嘛要去拷貝一個對象呢?

對于前端來說,這種情況多發(fā)于接收了后端的數(shù)據(jù),但是在界面展示上數(shù)據(jù)不夠完整,后端不改數(shù)據(jù),這時候就要你自己來動手拷貝對象了。

對應(yīng)后端來說,多發(fā)于造數(shù)據(jù)給前端,為了配合前端。

干巴巴的文字不好看,我來努力找個栗子吧。

假設(shè)你是個前端,做的是一個電商項目。每一個商品都有一個 名稱 ,價格 ,商品id。
然后,根據(jù)后端的返回,你知道需要如下一個bean。

public class Phone {
    public String name;
    public double price;
    public int goodsId ;
}

問題來了,現(xiàn)在你店里賣iPhone X,價格6666,商品id為8001。
關(guān)于8001這個商品后端只會給你返回這個信息。

可是這個時候老板說,我們要增加一件商品,名字叫做 蘋果10 , 但實際上就是iPhone X。

前端展示兩個商品,但是實際上就是同一個,因為goodsId只能有一個。這個時候,你就需要手動copy一個對象,然后修改他的商品名了。
(例子嘛,只是例子,莫認真,大概說明情況即可)

一、關(guān)于 = 的賦值,引用數(shù)據(jù)類型是地址傳遞

我們知道,Java的數(shù)據(jù)分為

  • 基本數(shù)據(jù)類型
  • 引用數(shù)據(jù)類型。

通常,我們會用 = 做賦值操作。

在基本數(shù)據(jù)類型類型中,我們使用 = 做賦值操作,實際上就是做拷貝操作,兩個變量對應(yīng)兩個地址。

在引用類型中,我們使用 = 號做賦值,只是執(zhí)行值傳遞,兩個對象對應(yīng)同一個地址。

public class AClass {
    public static void main(String[] args) {
        int i1 = 3;
        int  i2 = 5;
        i2 = 6;
        System.out.println("i1:"+i1);
        System.out.println("i2:"+i2);
        
        System.out.println("========");
        
        Phone p1 = new Phone();
        p1.size = 5;
        
        Phone p2 = new Phone();
        p2 = p1;
        p2.size = 6;
        
        System.out.println("p1:"+p1.size);
        System.out.println("p2:"+p2.size);
        
    }
}
public class Phone {
    public int size;
}

.
.
Console:

i1:3
i2:6
========
p1:6
p2:6

可見,在p2=p1這個過程中,執(zhí)行了是地址傳遞,兩個對象指向同一個地址,導(dǎo)致修改了p2的屬性值也同時影響p1的屬性值。

關(guān)于這點,大家都非常熟悉了。

顯然,= 操作無法滿足我們的需求,我們要的是對象拷貝。
在很多語言中,對象拷貝都是分為 淺拷貝深拷貝 的。

二、淺拷貝和深拷貝

大體區(qū)分

淺拷貝:
對基本數(shù)據(jù)類型進行值傳遞,對引用數(shù)據(jù)類型進行引用傳遞般的拷貝,此為淺拷貝。

深拷貝:
對基本數(shù)據(jù)類型進行值傳遞,對引用數(shù)據(jù)類型,創(chuàng)建一個新的對象,并復(fù)制其內(nèi)容,此為深拷貝。

實現(xiàn)拷貝方式

  • 1、通過set方法,逐一賦值
    (當(dāng)對象內(nèi)部復(fù)雜時,這種要是很要命,特別是每次修改屬性還要聯(lián)動修改)
  • 2、通過重寫java.lang.Object類中的方法clone()
  • 3、通過序列化的方式實現(xiàn)對象的拷貝。
  • 4、通過org.apache.commons中的工具類BeanUtils和PropertyUtils等進行對象復(fù)制
    (類似PropertyUtils的工具有很多,但是他們幾乎只在在基于JDK的環(huán)境中用,安卓用不了 )

clone方法實現(xiàn)淺拷貝

不管是淺拷貝還是深拷貝,我們都可以利用萬類之總 Object 里的 clone()方法來實現(xiàn)。

淺拷貝

對基本數(shù)據(jù)類型進行值傳遞,對引用數(shù)據(jù)類型進行引用傳遞般的拷貝,此為淺拷貝。

實現(xiàn)淺拷貝的步驟

1、實現(xiàn)Cloneable接口
2、復(fù)寫clone方法,并 return super.clone()

public class Phone{
    public String goodsName;
    public double price;
    public int goodsId ;
    
}
class Person  implements Cloneable{ // 淺拷貝 step1
    public String perName;
    public int age;
    public Phone phone;
    @Override
    public Object clone() throws CloneNotSupportedException
    {
        return super.clone();// 淺拷貝 step2
    }
    
    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return "info  :  goodsName:"+perName+"\n"+
        "age:"+age+"\n"+
        "phone.goodsName:"+phone.goodsName+"\n"+
        "phone.price:"+phone.price+"\n"+
        "phone.goodsId:"+phone.goodsId+"\n" 
        ;
    }
}

.
.

public class AClass {
    public static void main(String[] args) {
        Phone p1 = new Phone();
        p1.goodsName = "iPhone X";
        p1.goodsId = 8001;
        p1.price = 666;
        
        Person person = new Person();
        person.perName="張三";
        person.age = 18;
        person.phone = p1;
        
        Person person2 = null;
        try {
            person2 = (Person) person.clone();
            // 淺拷貝后修改值
            person2.perName= "李四";
            person2.age= 20;
        
            person2.phone.goodsId= 9001;
            person2.phone.goodsName= "MIX 3";
            person2.phone.price= 3299;
        } catch (CloneNotSupportedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("person:"+person.toString());
        System.out.println("person2:"+person2.toString());  
    }
}

.
.
console

person:info  :  goodsName:張三
age:18
phone.goodsName:MIX 3
phone.price:3299.0
phone.goodsId:9001

person2:info  :  goodsName:李四
age:20
phone.goodsName:MIX 3
phone.price:3299.0
phone.goodsId:9001
image.png

可見,淺拷貝中:

  • 如果原型對象的成員變量是值類型,將復(fù)制一份給克隆對象。
  • 如果原型對象的成員變量是引用類型,只是進行值地址的傳遞,原型對象和克隆對象的成員變量指向相同的內(nèi)存地址,所以克隆對象修改引用類型的數(shù)據(jù),原型對象會也會跟著改變。

clone方法實現(xiàn)深拷貝

深拷貝

對基本數(shù)據(jù)類型進行值傳遞,對引用數(shù)據(jù)類型,創(chuàng)建一個新的對象,并復(fù)制其內(nèi)容,此為深拷貝。

實現(xiàn)深拷貝的步驟

1、實現(xiàn)Cloneable接口
2、原型對象的值類型內(nèi)部也實現(xiàn)Cloneable接口和對應(yīng)復(fù)寫clone()
3、復(fù)寫clone方法
4、把引用的對象也進行可控并進行返回

其實微調(diào)一下代碼,就實現(xiàn)了 深拷貝。
(需要改動的只有這一份)


public class Phone implements Cloneable{ // 深拷貝 step2 
    public String goodsName;
    public double price;
    public int goodsId ;
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // TODO Auto-generated method stub
        return super.clone();
    }
    
}
class Person  implements Cloneable{ // 深拷貝 step1
    public String perName;
    public int age;
    public Phone phone;
    @Override
    public Object clone() throws CloneNotSupportedException
    {
        //return super.clone();
        
         // 深拷貝 step3
        Person person = (Person) super.clone();
         // 深拷貝 step4 把 值類型 的成員變量也進行拷貝
        person.phone = ((Phone) (person.phone.clone()));
        return person;

    }
    
    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return "info  :  goodsName:"+perName+"\n"+
        "age:"+age+"\n"+
        "phone.goodsName:"+phone.goodsName+"\n"+
        "phone.price:"+phone.price+"\n"+
        "phone.goodsId:"+phone.goodsId+"\n" 
        ;
    }
}


.
.
console:

person:info  :  goodsName:張三
age:18
phone.goodsName:iPhone X
phone.price:666.0
phone.goodsId:8001

person2:info  :  goodsName:李四
age:20
phone.goodsName:MIX 3
phone.price:3299.0
phone.goodsId:9001

可見,改為深拷貝之后。
楚河漢界,各不相犯。你我各自獨立。

可是利用clone的方式實現(xiàn)的深度拷貝,實在太麻煩。
比如我們Bean里面各種嵌套,原型對象的引用類型里面還有引用類型,嵌套四五層。
那么寫這些clone也是夠嗆的。

三、利用Serializable和Parcelable實現(xiàn)深拷貝

用序列化的方式實現(xiàn)深拷貝

實現(xiàn)Serializable接口,通過對象的序列化和反序列化實現(xiàn)克隆,可以實現(xiàn)深度克隆。

如果是Android開發(fā),自然還可以用Parcelable序列化的方式實現(xiàn)實現(xiàn)深拷貝

Serializable深拷貝

.
.

public class Phone implements Serializable{ 
    private static final long serialVersionUID = -6844928160614375642L;
    public String goodsName;
    public double price;
    public int goodsId ;

}
class Person  implements Serializable{ 
    private static final long serialVersionUID = 2254270518697430558L;
    public String perName;
    public int age;
    public Phone phone;
    
    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return "info  :  goodsName:"+perName+"\n"+
        "age:"+age+"\n"+
        "phone.goodsName:"+phone.goodsName+"\n"+
        "phone.price:"+phone.price+"\n"+
        "phone.goodsId:"+phone.goodsId+"\n" 
        ;
    }
}

.
.


public class AClass {
    public static void main(String[] args) {
        Phone p1 = new Phone();
        p1.goodsName = "iPhone X";
        p1.goodsId = 8001;
        p1.price = 666;
        
        Person person = new Person();
        person.perName="張三";
        person.age = 18;
        person.phone = p1;
        
        Person person2 = null;
        
        try {
            person2 = CloneUtil.clone(person);
            
            // 淺拷貝后修改值
            person2.perName= "李四";
            person2.age= 20;
        
            person2.phone.goodsId= 9001;
            person2.phone.goodsName= "MIX 3";
            person2.phone.price= 3299;
            
        } catch (ClassNotFoundException | IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        System.out.println("person:"+person.toString());
        System.out.println("person2:"+person2.toString());  
    }
}

.
.
CloneUtil

public class CloneUtil {

    private CloneUtil() {
        throw new AssertionError();
    }
    public static <T extends Serializable> T clone(T object) throws IOException, 
            ClassNotFoundException {
        // 說明:調(diào)用ByteArrayOutputStream或ByteArrayInputStream對象的close方法沒有任何意義
        // 這兩個基于內(nèi)存的流只要垃圾回收器清理對象就能夠釋放資源,這一點不同于對外資源(如文件流)的釋放
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(object);

        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        return (T) ois.readObject();
    }
}

.
.
console

person:info  :  goodsName:張三
age:18
phone.goodsName:iPhone X
phone.price:666.0
phone.goodsId:8001

person2:info  :  goodsName:李四
age:20
phone.goodsName:MIX 3
phone.price:3299.0
phone.goodsId:9001

可見,依然深拷貝。

Parcelable 深拷貝

利用安卓特有的Parcelable序列化方式,也可以進行深拷貝。

示例

public  class Person  implements Parcelable {

    public String perName;
    public int age;
    public Phone phone;

    public Person() {
    }

    protected Person(Parcel in) {
        perName = in.readString();
        age = in.readInt();
        phone = in.readParcelable(Phone.class.getClassLoader());
    }

    public static final Creator<Person> CREATOR = new Creator<Person>() {
        @Override
        public Person createFromParcel(Parcel in) {
            return new Person(in);
        }

        @Override
        public Person[] newArray(int size) {
            return new Person[size];
        }
    };

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return "info  :  goodsName:"+perName+"\n"+
                "age:"+age+"\n"+
                "phone.goodsName:"+phone.goodsName+"\n"+
                "phone.price:"+phone.price+"\n"+
                "phone.goodsId:"+phone.goodsId+"\n"
                ;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeString(perName);
        parcel.writeInt(age);
        parcel.writeParcelable(phone, i);
    }
}

.
.

public class Phone implements Parcelable {

    public String goodsName;
    public double price;
    public int goodsId ;

    public Phone() {
    }

    public Phone(Parcel in) {
        goodsName = in.readString();
        price = in.readDouble();
        goodsId = in.readInt();
    }

    public static final Creator<Phone> CREATOR = new Creator<Phone>() {
        @Override
        public Phone createFromParcel(Parcel in) {
            return new Phone(in);
        }

        @Override
        public Phone[] newArray(int size) {
            return new Phone[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeString(goodsName);
        parcel.writeDouble(price);
        parcel.writeInt(goodsId);
    }
}

.
.

public class ParcelHelper {

    public static <T> T copy(Parcelable input) {
        Parcel parcel = null;

        try {
            parcel = Parcel.obtain();
            parcel.writeParcelable(input, 0);

            parcel.setDataPosition(0);
            return parcel.readParcelable(input.getClass().getClassLoader());
        } finally {
            parcel.recycle();
        }
    }
}

.
.

進行拷貝和修改

Phone p1 = new Phone();
p1.goodsName = "iPhone X";
p1.goodsId = 8001;
p1.price = 666;

Person person = new Person();
person.perName="張三";
person.age = 18;
person.phone = p1;

Person person2 = null;


person2 = ParcelHelper.copy(person);
// 淺拷貝后修改值
person2.perName= "李四";
person2.age= 20;

person2.phone.goodsId= 9001;
person2.phone.goodsName= "MIX 3";
person2.phone.price= 3299;

System.out.println("person:"+person.toString());
System.out.println("person2:"+person2.toString());

.
.
console:

person:info  :  goodsName:張三
age:18
phone.goodsName:iPhone X
phone.price:666.0
phone.goodsId:8001

person2:info  :  goodsName:李四
age:20
phone.goodsName:MIX 3
phone.price:3299.0
phone.goodsId:9001

可見,采用Parcelable的方式,依然可實現(xiàn)深拷貝。

四、 利用工具類庫進行深拷貝

除了clone和序列化接口。
我們還可以利用一些強大工具類庫來實現(xiàn)深度拷貝。

  • Apache BeanUtil.CopyProperties
  • apache PropertyUtils.CopyProperties
  • spring BeanUtils.CopyProperties
  • cglib BeanCopier
  • ezmorph BeanMorpher

其中,BeanUtil最為常見,BeanCopier效率相對較高。
然后,在Java的世界你隨便耍。
在Adnroid的世界還是算了吧。
這些類庫,基本都是基于完整的JDK,而安卓的SDK對JDK進行了精簡,基本拜拜。

(文中的全部Bean沒有按照面向?qū)ο蟮姆庋b的思想進行g(shù)et和set,基本都是public,見諒)

關(guān)于工具類的,就不演示了。
本文完。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容