安卓/Java对象拷贝(浅/深拷贝、两种序列化、Beans等工具)

文/阿敏其人
本文出自阿敏其人简书博客,转载请与本人联系。


为什么要拷贝对象

我们干嘛要去拷贝一个对象呢?

对于前端来说,这种情况多发于接收了后端的数据,但是在界面展示上数据不够完整,后端不改数据,这时候就要你自己来动手拷贝对象了。

对应后端来说,多发于造数据给前端,为了配合前端。

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

假设你是个前端,做的是一个电商项目。每一个商品都有一个 名称 ,价格 ,商品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
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,见谅)

关于工具类的,就不演示了。
本文完。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269