對象拷貝可分爲淺拷貝和深拷貝,在開發過程中深拷貝不是隨處可見,大部分是引用的賦值,也即是內存地址的引用。如果簡單的類似Student studen1 = student0
這樣便認爲複製了一份,這就大錯特錯了,有些時候你會莫名的發現studen1沒有任何操作裏面的屬性卻發生變化了,不用說一定是student0在某個時候被修改了,因爲這兩個對象引用的是一個地址的內容。開發過程中,因爲嵌套過深,對象中轉過多,着實需要小心。
真正的拷貝是完全複製一份,從而與原對象隔離開,保證了原對象的定格,從而在操作過程中不用擔心原對象被修改,必要時可“一鍵還原”。
什麼是淺拷貝?什麼是深拷貝?
先了解一個基礎知識:堆內存和棧內存。
針對變量,棧內存上分配一些基本類型的變量與對象的引用,而堆內存分配給真正的對象本身以及數組等,堆內存上的數據由棧內存上的相應變量引用,相當於棧中存儲着堆內存中實際對象或數組的標記或別名(實際上是堆內存變量首地址)。這裏包含兩部分:一個是基本數據類型,一個是引用數據類型,淺拷貝和深拷貝就是在這個基礎之上做的區分。
假如在拷貝這個對象的時候,只對基本數據類型進行了拷貝,而對引用數據類型只是進行了引用的傳遞,而沒有真實的創建一個新的對象,則認爲是淺拷貝。反之,在對引用數據類型進行拷貝的時候,創建了一個新的對象,並且複製其內的成員變量,則認爲是深拷貝。
簡單的說:不能拷貝完全的就是淺拷貝,能夠完完全全複製一份的叫深拷貝。
淺拷貝:
對基本數據類型進行值傳遞,對引用數據類型進行引用傳遞般的拷貝,此爲淺拷貝。
深拷貝:
對基本數據類型進行值傳遞,對引用數據類型,創建一個新的對象,並複製其內容,此爲深拷貝。
Android常見的拷貝方式
Android開發常見的拷貝方式總結大概有以下六種:
- 對象屬性相互賦值
- Serializable
- Parcelable
- Gson
- Cloneable
- 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(名字,年齡),這裏做兩個功能:
- 分別對上邊的各個方法拷貝一萬次,觀察各個耗時。
- 修改拷貝對象屬性值,查看原對象屬性值是否變化,來驗證是深拷貝還是淺拷貝。
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 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