文/阿敏其人
本文出自阿敏其人简书博客,转载请与本人联系。
为什么要拷贝对象
我们干嘛要去拷贝一个对象呢?
对于前端来说,这种情况多发于接收了后端的数据,但是在界面展示上数据不够完整,后端不改数据,这时候就要你自己来动手拷贝对象了。
对应后端来说,多发于造数据给前端,为了配合前端。
干巴巴的文字不好看,我来努力找个栗子吧。
假设你是个前端,做的是一个电商项目。每一个商品都有一个 名称 ,价格 ,商品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
可见,浅拷贝中:
- 如果原型对象的成员变量是值类型,将复制一份给克隆对象。
- 如果原型对象的成员变量是引用类型,只是进行值地址的传递,原型对象和克隆对象的成员变量指向相同的内存地址,所以克隆对象修改引用类型的数据,原型对象会也会跟着改变。
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,见谅)
关于工具类的,就不演示了。
本文完。