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

weixin_33816300發表於2018-08-30

文/阿敏其人
本文出自阿敏其人簡書部落格,轉載請與本人聯絡。


為什麼要拷貝物件

我們幹嘛要去拷貝一個物件呢?

對於前端來說,這種情況多發於接收了後端的資料,但是在介面展示上資料不夠完整,後端不改資料,這時候就要你自己來動手拷貝物件了。

對應後端來說,多發於造資料給前端,為了配合前端。

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

假設你是個前端,做的是一個電商專案。每一個商品都有一個 名稱 ,價格 ,商品id。
然後,根據後端的返回,你知道需要如下一個bean。

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

問題來了,現在你店裡賣iPhone X,價格6666,商品id為8001。
關於8001這個商品後端只會給你返回這個資訊。

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

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

一、關於 = 的賦值,引用資料型別是地址傳遞

我們知道,Java的資料分為

  • 基本資料型別
  • 引用資料型別。

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

在基本資料型別型別中,我們使用 = 做賦值操作,實際上就是做拷貝操作,兩個變數對應兩個地址。

在引用型別中,我們使用 = 號做賦值,只是執行值傳遞,兩個物件對應同一個地址

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這個過程中,執行了是地址傳遞,兩個物件指向同一個地址,導致修改了p2的屬性值也同時影響p1的屬性值。

關於這點,大家都非常熟悉了。

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

二、淺拷貝和深拷貝

大體區分

淺拷貝:
對基本資料型別進行值傳遞,對引用資料型別進行引用傳遞般的拷貝,此為淺拷貝。

深拷貝:
對基本資料型別進行值傳遞,對引用資料型別,建立一個新的物件,並複製其內容,此為深拷貝。

實現拷貝方式

  • 1、通過set方法,逐一賦值
    (當物件內部複雜時,這種要是很要命,特別是每次修改屬性還要聯動修改)
  • 2、通過重寫java.lang.Object類中的方法clone()
  • 3、通過序列化的方式實現物件的拷貝。
  • 4、通過org.apache.commons中的工具類BeanUtils和PropertyUtils等進行物件複製
    (類似PropertyUtils的工具有很多,但是他們幾乎只在在基於JDK的環境中用,安卓用不了 )

clone方法實現淺拷貝

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

淺拷貝

對基本資料型別進行值傳遞,對引用資料型別進行引用傳遞般的拷貝,此為淺拷貝。

實現淺拷貝的步驟

1、實現Cloneable介面
2、複寫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
1083096-3d68c37cc0bb0f87.png
image.png

可見,淺拷貝中:

  • 如果原型物件的成員變數是值型別,將複製一份給克隆物件。
  • 如果原型物件的成員變數是引用型別,只是進行值地址的傳遞,原型物件和克隆物件的成員變數指向相同的記憶體地址,所以克隆物件修改引用型別的資料,原型物件會也會跟著改變。

clone方法實現深拷貝

深拷貝

對基本資料型別進行值傳遞,對引用資料型別,建立一個新的物件,並複製其內容,此為深拷貝。

實現深拷貝的步驟

1、實現Cloneable介面
2、原型物件的值型別內部也實現Cloneable介面和對應複寫clone()
3、複寫clone方法
4、把引用的物件也進行可控並進行返回

其實微調一下程式碼,就實現了 深拷貝。
(需要改動的只有這一份)


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的方式實現的深度拷貝,實在太麻煩。
比如我們Bean裡面各種巢狀,原型物件的引用型別裡面還有引用型別,巢狀四五層。
那麼寫這些clone也是夠嗆的。

三、利用Serializable和Parcelable實現深拷貝

用序列化的方式實現深拷貝

實現Serializable介面,通過物件的序列化和反序列化實現克隆,可以實現深度克隆。

如果是Android開發,自然還可以用Parcelable序列化的方式實現實現深拷貝

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 {
        // 說明:呼叫ByteArrayOutputStream或ByteArrayInputStream物件的close方法沒有任何意義
        // 這兩個基於記憶體的流只要垃圾回收器清理物件就能夠釋放資源,這一點不同於對外資源(如檔案流)的釋放
        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的方式,依然可實現深拷貝。

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

除了clone和序列化介面。
我們還可以利用一些強大工具類庫來實現深度拷貝。

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

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

(文中的全部Bean沒有按照物件導向的封裝的思想進行get和set,基本都是public,見諒)

關於工具類的,就不演示了。
本文完。

相關文章