對象拷貝性能對比分析

對象拷貝可分爲淺拷貝和深拷貝,在開發過程中深拷貝不是隨處可見,大部分是引用的賦值,也即是內存地址的引用。如果簡單的類似Student studen1 = student0這樣便認爲複製了一份,這就大錯特錯了,有些時候你會莫名的發現studen1沒有任何操作裏面的屬性卻發生變化了,不用說一定是student0在某個時候被修改了,因爲這兩個對象引用的是一個地址的內容。開發過程中,因爲嵌套過深,對象中轉過多,着實需要小心。

真正的拷貝是完全複製一份,從而與原對象隔離開,保證了原對象的定格,從而在操作過程中不用擔心原對象被修改,必要時可“一鍵還原”。

什麼是淺拷貝?什麼是深拷貝?

先了解一個基礎知識:堆內存和棧內存。

針對變量,棧內存上分配一些基本類型的變量與對象的引用,而堆內存分配給真正的對象本身以及數組等,堆內存上的數據由棧內存上的相應變量引用,相當於棧中存儲着堆內存中實際對象或數組的標記或別名(實際上是堆內存變量首地址)。這裏包含兩部分:一個是基本數據類型,一個是引用數據類型,淺拷貝和深拷貝就是在這個基礎之上做的區分。

假如在拷貝這個對象的時候,只對基本數據類型進行了拷貝,而對引用數據類型只是進行了引用的傳遞,而沒有真實的創建一個新的對象,則認爲是淺拷貝。反之,在對引用數據類型進行拷貝的時候,創建了一個新的對象,並且複製其內的成員變量,則認爲是深拷貝。

簡單的說:不能拷貝完全的就是淺拷貝,能夠完完全全複製一份的叫深拷貝

淺拷貝
淺拷貝
對基本數據類型進行值傳遞,對引用數據類型進行引用傳遞般的拷貝,此爲淺拷貝。

深拷貝
深拷貝
對基本數據類型進行值傳遞,對引用數據類型,創建一個新的對象,並複製其內容,此爲深拷貝。

Android常見的拷貝方式

Android開發常見的拷貝方式總結大概有以下六種:

  1. 對象屬性相互賦值
  2. Serializable
  3. Parcelable
  4. Gson
  5. Cloneable
  6. Mapstruct

對象的相互賦值

對象屬性相互賦值這個是最普通也是操作最笨重的,就是兩個對象所有屬性相互賦值,從而實現對象的拷貝;注意前邊說的是操作最笨重的,但是如果從性能和執行效率上考慮應該是最快的,因爲免去了其他額外的開銷直接從最基本入手。

    Student student = new Student();
    student.setAge(11);
    student.setName("小明");
    Classmate classmate = new Classmate();
    classmate.setAge(12);
    classmate.setName("張三");
    student.setClassmates(classmate);
    
    Student studen1 = new Student();
    studen1.setAge(student.getAge());
    studen1.setName(student.getName());
    studen1.setClassmates(student.getClassmate());

細心的同學看到上邊有一個引用類型classmate,前邊已經說過如果對引用數據類型只是進行了引用的傳遞,而沒有真實的創建一個新的對象,則認爲是淺拷貝。那麼深拷貝應該如何操作呢?

    student1 = new Student();
    student1.setName(student.getName());
    student1.setAge(student.getAge());
    Classmate classmate1 = new Classmate();
    classmate1.setName(student.getClassmates().getName());
    classmate1.setAge(student.getClassmates().getAge());
    student1.setClassmates(classmate1);

如上把studen的Classmate各個屬性值重新拼裝成一個對象然後存入stuent1中,充分將student中的各個屬性做一個拷貝過程,算是一個深拷貝。

Serializable

Serializable實現拷貝過程原理是將對象序列化成二進制流到本地硬盤,然後在反序列化成對象,因此整個過程不存在某個引用類型共同指向問題,因此整個拷貝過程都屬於深拷貝。需要注意的是拷貝的對象包括其內部引用類型的屬性對象都要實現Serializable接口。

    /**
     * 對象拷貝,傳入和返回的T必須是serializable類型。
     * 將對象序列化成流,因爲寫在流裏的是對象的一個拷貝,而原對象仍然存在於JVM裏面。所以利用這個特性可以實現對象的深拷貝。
     */
    public static <T extends Serializable> T sCopy(T object) {
        try {
            //將對象寫到流裏
            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();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

Parcelable

Parcelable實現拷貝過程的原理和Serializable如出一轍,Parcelable和Serializable的區別是一個序列化時的狀態的保存,前者僅僅是保存在內存中,後者保存在硬盤中,因此前者性能更快,這也是android序列化所推薦的。因此Parcelable的拷貝過程也是深拷貝,所拷貝的對象以及引用類型的屬性對象都要實現Parcelable接口。

    /**
     * 對象拷貝,傳入的對象是Parcelable類型。
     */
    public static <T extends Parcelable> T pCopy(T object) {
        Parcel parcel = null;
        try {
            parcel = Parcel.obtain();
            parcel.writeParcelable(object, 0);
            parcel.setDataPosition(0);
            return parcel.readParcelable(object.getClass().getClassLoader());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (parcel != null) {
                parcel.recycle();
            }
        }
        return null;
    }

Gson

Gson拷貝的原理同樣是序列化和反序列的過程,和Serializable是類似的,只不過Gson僅僅是序列化成json格式,並且不保存對象的相關方法和狀態(反序列化時需要指定相應的對象類型),因此相比Serializable性能更優。使用時需要Gson相關依賴。

    /**
     * Gson copy
     */
    public static <T, S> T gCopy(S source, Class<T> targetType) {
        return sGson.fromJson(sGson.toJson(source), targetType);
    }

Cloneable

Cloneable拷貝是java對象固有的功能,每個實體類中父類Object中都有一個clone方法,專門用於本類的拷貝工作的。採用Cloneable拷貝需要實體類中實現Cloneable接口,並且要重寫Object中的clone方法。

public class Student implements Cloneable {
    private String name;
    private int age;
    private Classmate classmates;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Classmate getClassmates() {
        return classmates;
    }

    public void setClassmates(Classmate classmates) {
        this.classmates = classmates;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student student = (Student) super.clone();
        student.classmates = (Classmate) this.classmates.clone();
        return student;
    }
}

注意觀察Student中引用型屬性對象classmates,重寫的clone方法中如果僅僅是對Studen對象進行clone,而不對classmates進行clone的話則屬於淺拷貝。這裏我們需要深拷貝,所以需要針對Student中的所有引用型屬性對象做一個clone(這裏指classmates)。依次類推Classmate同樣實現Cloneable,同樣需要重寫clone方法。

public class Classmate implements Cloneable {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

Classmate實體類中沒有引用型屬性對象了,所以重寫的clone方法裏面僅僅返回父類的clone()方法即可。

Mapstruct

MapStruct是一種類型安全的bean映射類生成java註釋處理器。
我們要做的就是定義一個映射器接口,聲明必需的映射方法。在編譯的過程中,MapStruct會生成此接口的實現。該實現使用純java方法調用的源和目標對象之間的映射,MapStruct節省了時間,通過生成代碼完成繁瑣和容易出錯的代碼邏輯。這裏巧妙藉助一下其映射功能實現類的拷貝,其拷貝原理和對象屬性相互賦值相同。

android中使用Mapstruct需要在build.gradle中引入Mapstruct的相關依賴:

//模型映射
implementation 'org.mapstruct:mapstruct:1.2.0.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.2.0.Final'

使用也很簡單,只需要自定義一個Mapstruct接口,在編譯時會根絕註解自動生成相關代碼方法。

@Mapper
public interface MapStructCopy {
    MapStructCopy INSTANCE = Mappers.getMapper(MapStructCopy.class);
    
    Student copy(Student t);
    Classmate copy(Classmate c);
    
}

這裏需要注意的是,如果僅僅定義Student的映射方法,則同樣屬於淺拷貝,因爲Classmate屬於Student中的一個引用型對象屬性,所以需要在自定義接口中定義一個屬於Classmate的映射方法;來覆蓋Student中屬性映射。接下來就可以對Student對象拷貝了。

Student student = MapStructCopy.INSTANCE.copy(student);

性能對比

下邊通過一個例子來對比以上各個拷貝的性能做一個分析對比。

假設這裏有一個Student類,包含名字、年齡、同學Classmate(名字,年齡),這裏做兩個功能:

  1. 分別對上邊的各個方法拷貝一萬次,觀察各個耗時。
  2. 修改拷貝對象屬性值,查看原對象屬性值是否變化,來驗證是深拷貝還是淺拷貝。
public class Student implements Serializable, Parcelable, Cloneable {
    private String name;
    private int age;
    private Classmate classmates;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Classmate getClassmates() {
        return classmates;
    }

    public void setClassmates(Classmate classmates) {
        this.classmates = classmates;
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(this.name);
        dest.writeInt(this.age);
        dest.writeParcelable(this.classmates, flags);
    }

    public Student() {
    }

    protected Student(Parcel in) {
        this.name = in.readString();
        this.age = in.readInt();
        this.classmates = in.readParcelable(Classmate.class.getClassLoader());
    }

    public static final Parcelable.Creator<Student> CREATOR = new Parcelable.Creator<Student>() {
        @Override
        public Student createFromParcel(Parcel source) {
            return new Student(source);
        }

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student student = (Student) super.clone();
        student.classmates = (Classmate) this.classmates.clone();
        return student;
    }
}
public class Classmate implements Serializable, Parcelable, Cloneable {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(this.name);
        dest.writeInt(this.age);
    }

    public Classmate() {
    }

    protected Classmate(Parcel in) {
        this.name = in.readString();
        this.age = in.readInt();
    }

    public static final Parcelable.Creator<Classmate> CREATOR = new Parcelable.Creator<Classmate>() {
        @Override
        public Classmate createFromParcel(Parcel source) {
            return new Classmate(source);
        }

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        test();
    }

    private void test() {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                Student student = new Student();
                student.setAge(11);
                student.setName("小明");
                Classmate classmate = new Classmate();
                classmate.setAge(12);
                classmate.setName("張三");
                student.setClassmates(classmate);

                Student student1 = null;
                long last;
                int count = 10000;

                Log.e("TAG","+++++++++++++++++開始");

                try {
                    last = System.currentTimeMillis();
                    for (int i = 0; i < count; i++) {
                        student1 = new Student();
                        student1.setName(student.getName());
                        student1.setAge(student.getAge());
                        Classmate classmate1 = new Classmate();
                        classmate1.setName(student.getClassmates().getName());
                        classmate1.setAge(student.getClassmates().getAge());
                        student1.setClassmates(classmate1);
                    }
                    log("Normal", last, student1, student);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                try {
                    last = System.currentTimeMillis();
                    for (int i = 0; i < count; i++) {
                        student1 = CopyUtil.sCopy(student);
                    }
                    log("Serializable", last, student1, student);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                try {
                    last = System.currentTimeMillis();
                    for (int i = 0; i < count; i++) {
                        student1 = CopyUtil.pCopy(student);
                    }
                    log("Parcelable", last, student1, student);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                try {
                    last = System.currentTimeMillis();
                    for (int i = 0; i < count; i++) {
                        student1 = CopyUtil.gCopy(student, Student.class);
                    }
                    log("Gson", last, student1, student);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                try {
                    last = System.currentTimeMillis();
                    for (int i = 0; i < count; i++) {
                        student1 = (Student) student.clone();
                    }
                    log("Cloneable", last, student1, student);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                try {
                    last = System.currentTimeMillis();
                    for (int i = 0; i < count; i++) {
                        student1 = MapStructCopy.INSTANCE.copy(student);
                    }
                    log("MapStruct", last, student1, student);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    private void log(String tag, long lastTime, Student targetBean, Student resouceBean) {
        Log.e("TAG", "+++++" + tag+"+++++");
        Log.e("TAG", "耗時:" + (System.currentTimeMillis() - lastTime)
                + "  student hash:" + (targetBean.hashCode() == resouceBean.hashCode())
                + "  classmate hash:" + (targetBean.getClassmates().hashCode() == resouceBean.getClassmates().hashCode()));

        Log.e("TAG", "student1 name:" + targetBean.getName()+"  classmate1 name:" + targetBean.getClassmates().getName());
        targetBean.setName("王五");
        targetBean.getClassmates().setName("趙六");
        Log.e("TAG", "student name:" + resouceBean.getName()+"  classmate name:" + resouceBean.getClassmates().getName());
    }
}

打印結果如下:
對比結果
根據打印結果可以得出一個性能對比隊列:

Cloneable > Normal = MapStruct > parcelable > Serializable = Gson

這個結果也是合乎常理的,前面已經着重針對每種拷貝原理做了分析,不用說凡是內存上操作的肯定優於硬盤上操作的。

如果根據操作麻煩程度可以得到一個對比隊列:

Gson > Serializable > MapStruct > Cloneable > Parcelable > Normal

根據以上對比,項目開發過程中我個人認爲,如果僅僅是簡單的拷貝不必在意於性能多消耗那麼一點點可以採用Gson,如果項目中的Model都實現了Serializable,也可以採用Serializable序列化形式;如果要兼顧性能和易用性,推薦Cloneable。

知識擴展

文中提到MapStruct是一種類型安全的bean映射類生成java註釋處理器,而文中僅僅是利用很簡單的映射來實現拷貝功能,其實MapStruct功能很強大,下邊就舉幾個簡單實用場景:

兩個對象的映射

@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface OrderInfoMapper {
    OrderInfoMapper INSTANCE = Mappers.getMapper(OrderInfoMapper.class);

    @Mappings({
        @Mapping(source = "feeDesc", target = "feeTypeDesc"),
        @Mapping(source = "origAmount", target = "originAmount"),
        @Mapping(source = "confirmationNo", target = "orderNo")
    })
    PriceInfo dbToOrderPriceInfo(OrderPriceOffineDb orderOffineDb);
}

這裏將OrderPriceOffineDb中的字段’feeDesc’、‘origAmount’、‘confirmationNo’字段賦值給PriceInfo的’feeTypeDesc’、‘originAmount’、'orderNo’這三個字段,其他這兩個對象中字段相同的相互賦值,不同的根據MapStruct處理器策略做相應處理。

多個對象的映射

@Mapper
public interface AddressMapper {
 
    @Mappings({
        @Mapping(source = "person.description", target = "description"),
        @Mapping(source = "address.houseNo", target = "houseNumber")
    })
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}

這裏映射來源有兩個分別是Person、Address,分別將Person對象中description字段和Address對象中houseNo字段的值分別賦值給DeliveryAddressDto中description、houseNumber。

如果多個源對象定義具有相同名稱的屬性,則必須使用@Mapping註解指定從中檢索屬性的source參數,如示例中的description屬性所示。如果不解決這種歧義,將會引發錯誤。對於在給定源對象中僅存在一次的屬性,可以選擇指定源參數的名稱,因爲它可以自動確定。

MapStruct映射策略

正如上邊兩個對象的映射一樣,所採用的映射策略是unmappedTargetPolicy = ReportingPolicy.IGNORE,假如目標映射對象有字段未被映射將被忽略,如果沒有這個策略指定則會給出響應的提示警告。

下邊給出MapStruct常用的映射策略:
MapStruct常用的映射策略

其他

  • MapStruct GitHub 訪問地址 : https://github.com/mapstruct/mapstruct/
  • 使用例子 : https://github.com/mapstruct/mapstruct-examples
  • MapStrcut與其它工具對比以及使用說明! http://www.tuicool.com/articles/uiIRjai
  • MapStruct 1.1.0.Final中文參考指南 https://blog.csdn.net/YoshinoNanjo/article/details/81363285
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章