设计模式——原型模式

579 阅读9分钟

一、概述

原型模式也是一种创建型模式,主要作用是对象克隆,是通过复制一个原型来创建新的对象,这个新对象具有和原型一样的内部属性,并且每一个克隆对象都是独立对象。如果一个对象引用有很多资源,并且需要创建一个新的此对象,那么通过克隆的模式进行创建就可以更加高效。原型模式中的对象克隆分为深克隆和浅克隆,深克隆也可以叫深拷贝,浅克隆也可以叫浅拷贝。

二、使用

1、Java实现

要进行对象克隆,必须要经过两个步骤:实现java.lang.Cloneable接口、重写java.lang.Object类中的clone()方法,这2者缺一不可。

Ⅰ.浅克隆(浅拷贝)

这个先新建一个可以用于创建克隆对象的类PersonForJava。在类中我们创建了姓名、年龄、体重这些基本数据类型的属性,以及兴趣爱好列表和家庭地址的引用类型属性。

public class PersonForJava implements Cloneable {

    private String name;
    private int age;
    private double weight;
    //兴趣列表
    private ArrayList<String> hobbyList = new ArrayList<>();
    //住址
    private Address address;

    public PersonForJava(){
        Log.e("XXX", "调用了PersonForJava类的构造方法");
    }

    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 double getWeight() {
        return weight;
    }

    public void setWeight(double weight) {
        this.weight = weight;
    }

    public void addHobby(String hobby) {
        hobbyList.add(hobby);
    }

    public ArrayList<String> getHobbyList() {
        return hobbyList;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public String showAll(){
        return "name: " + name + ",age: " + age + ",weight: " + weight + ",hobbyList: " + hobbyList + ",address: " + address.myAddress;
    }

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

Address类很简单,只有一个地址。

public class Address{
    public String myAddress;
}

接下来先自然创建一个PersonForJava的对象,设置相关属性的值:

PersonForJava personForJava1 = new PersonForJava();
personForJava1.setName("张三");
personForJava1.setAge(26);
personForJava1.setWeight(141.12);
personForJava1.addHobby("篮球");
personForJava1.addHobby("足球");
personForJava1.addHobby("排球");
Address address = new Address();
address.myAddress = "四川省成都市";
personForJava1.setAddress(address);

然后通过克隆方法,进行对象的克隆操作,得到personForJava2:

try {
    PersonForJava personForJava2 = (PersonForJava) personForJava1.clone();
    Log.e(TAG, "personForJava1:" + personForJava1);
    Log.e(TAG, "personForJava2:" + personForJava2);
    Log.e(TAG, "personForJava1 ---> " + personForJava1.showAll());
    Log.e(TAG, "personForJava2 ---> " + personForJava2.showAll());
    Log.e(TAG, "personForJava1.getHobbyList() == personForJava2.getHobbyList():" + (personForJava1.getHobbyList() == personForJava2.getHobbyList()));
    Log.e(TAG, "personForJava1.getAddress() == personForJava2.getAddress():" + (personForJava1.getAddress() == personForJava2.getAddress()));
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
    Log.e(TAG, "对象克隆失败---1");
}

首先打印出personForJava1和personForJava2的对象地址,再调用showAll方法展示所有的属性,得到以下输出。可以看到两个对象并不是同一个,但是具有相同的属性。并且只调用了一次构造方法,说明对象的克隆并没有通过构造方法进行对象的创建。最后比较了hobbyList和address属性是具有相同的地址,这也就说明了在浅克隆中对象对引用类型具有相同的指向。

接下来修改personForJava2对象的所有属性值,再调用showAll方法展示所有的属性。重新运行程序,得到以下输出。从图中我们可以看到,基本数据类型的属性值并没有改变,但是引用类型的值发生了变化,从上一点中我们就可以得知,因为克隆出来的不同对象,是共同指向了相同的引用类型,所以当personForJava2改变了hobbyList以及address属性内容时,personForJava1中的值也会发生变化。为什么姓名、年龄没有发生变化,是因为它们都是值类型,不需要clone,在副本对象克隆好之后,直接返回给客户端对象即可。

personForJava2.setName("李四");
personForJava2.setAge(28);
personForJava2.setWeight(136.78);
personForJava2.addHobby("乒乓球");
address.myAddress = "四川省绵阳市";
personForJava2.setAddress(address);
Log.e(TAG, "personForJava1 ---> " + personForJava1.showAll());
Log.e(TAG, "personForJava2 ---> " + personForJava2.showAll());

小结,从上面我们可以得出,当对象具有引用类型的属性时,当克隆对象对引用类型的值做改变时,是会直接影响到原型对象的值,这个对我们来说是不可靠的、有隐患的,所以我们需要进行深克隆。以下是全部测试代码:

/**
 * 浅克隆
 */
 private void shallowClone(){
    PersonForJava personForJava1 = new PersonForJava();
    personForJava1.setName("张三");
    personForJava1.setAge(26);
    personForJava1.setWeight(141.12);
    personForJava1.addHobby("篮球");
    personForJava1.addHobby("足球");
    personForJava1.addHobby("排球");
    Address address = new Address();
    address.myAddress = "四川省成都市";
    personForJava1.setAddress(address);

    try {
        PersonForJava personForJava2 = (PersonForJava) personForJava1.clone();
        Log.e(TAG, "personForJava1:" + personForJava1);
        Log.e(TAG, "personForJava2:" + personForJava2);
        Log.e(TAG, "personForJava1 ---> " + personForJava1.showAll());
        Log.e(TAG, "personForJava2 ---> " + personForJava2.showAll());
        Log.e(TAG, "personForJava1.getHobbyList() == personForJava2.getHobbyList():" + (personForJava1.getHobbyList() == personForJava2.getHobbyList()));
        Log.e(TAG, "personForJava1.getAddress() == personForJava2.getAddress():" + (personForJava1.getAddress() == personForJava2.getAddress()));

        Log.e(TAG, "-------------");

        personForJava2.setName("李四");
        personForJava2.setAge(28);
        personForJava2.setWeight(136.78);
        personForJava2.addHobby("乒乓球");
        address.myAddress = "四川省绵阳市";
        personForJava2.setAddress(address);
        Log.e(TAG, "personForJava1 ---> " + personForJava1.showAll());
        Log.e(TAG, "personForJava2 ---> " + personForJava2.showAll());
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
        Log.e(TAG, "对象克隆失败---1");
    }
}

Ⅱ.深克隆(深拷贝)

从浅克隆我们得知,基本类型的拷贝已经完成,但是对于引用类型来说,只是拷贝出了一个副本,指向原型对象的引用类型的地址。所以深克隆就是解决这个问题的。我们在浅克隆中只是单纯的重写了clone()方法,就直接返回调用父类的克隆方法,并没有在方法中做出自己具体的操作。

深克隆,在克隆引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的克隆。

引用类型如果是自定义类,需要在自定义类中也要实现java.lang.Cloneable接口,并重新clone()方法,比如Address类;数组与列表系统已经帮我们重写了clone方法,无需我们自己手动去重写。

public class Address implements Cloneable{

    public String myAddress;

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

最重要的一点,就是在原型对象的clone()方法中,需要对引用类型进行克隆,代码如下。

@Override
protected Object clone() throws CloneNotSupportedException {
    PersonForJava personForJava = (PersonForJava) super.clone();
    personForJava.hobbyList = (ArrayList<String>) personForJava.getHobbyList().clone();
    personForJava.address = (Address) personForJava.getAddress().clone();
    return personForJava;

//  return super.clone();
}

接下来还是和之前一样,先自然创建一个原型对象,并设置相关属性值。

PersonForJava personForJava3 = new PersonForJava();
personForJava3.setName("王五");
personForJava3.setAge(22);
personForJava3.setWeight(149.71);
personForJava3.addHobby("王者荣耀");
personForJava3.addHobby("穿越火线");
personForJava3.addHobby("斗地主");
Address address = new Address();
address.myAddress = "四川省达州市";
personForJava3.setAddress(address);

然后通过克隆方法,进行对象的克隆操作,得到personForJava4:

try {
    PersonForJava personForJava4 = (PersonForJava) personForJava3.clone();
    Log.e(TAG, "personForJava3:" + personForJava3);
    Log.e(TAG, "personForJava4:" + personForJava4);
    Log.e(TAG, "personForJava3 ---> " + personForJava3.showAll());
    Log.e(TAG, "personForJava4 ---> " + personForJava4.showAll());
    Log.e(TAG, "personForJava3.getHobbyList() == personForJava4.getHobbyList():" + (personForJava3.getHobbyList() == personForJava4.getHobbyList()));
    Log.e(TAG, "personForJava3.getAddress() == personForJava4.getAddress():" + (personForJava3.getAddress() == personForJava4.getAddress()));
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
    Log.e(TAG, "对象克隆失败---2");
}

首先打印出personForJava3和personForJava4的对象地址,再调用showAll方法展示所有的属性,得到以下输出。可以看到两个对象并不是同一个,但是具有相同的属性。并且只调用了一次构造方法,说明对象的克隆并没有通过构造方法进行对象的创建。最后比较了hobbyList和address属性是不同的地址,这也就说明了在深克隆中对象对引用类型已经不再指向同一个。

接下来修改personForJava4对象的所有属性值,再调用showAll方法展示所有的属性。重新运行程序,得到以下输出。从图中我们可以看到,不管是基本类型的属性,还是引用类型的属性,当personForJava4克隆对象改变属性值时,并没有影响到personForJava3中的属性的值。

personForJava4.setName("孙六");
personForJava4.setAge(24);
personForJava4.setWeight(121.13);
personForJava4.addHobby("地下城与勇士");
Address arrr = personForJava4.getAddress();
arrr.myAddress = "四川省宜宾市";
personForJava4.setAddress(arrr);
Log.e(TAG, "personForJava3 ---> " + personForJava3.showAll());
Log.e(TAG, "personForJava4 ---> " + personForJava4.showAll());

小结,从上面我们可以得出,进行深克隆的对象,对引用类型是独自开辟了内存空间,所以不会影响到原型对象。以下是全部测试代码:

/**
 * 深克隆
 */
private void deepClone(){
    PersonForJava personForJava3 = new PersonForJava();
    personForJava3.setName("王五");
    personForJava3.setAge(22);
    personForJava3.setWeight(149.71);
    personForJava3.addHobby("王者荣耀");
    personForJava3.addHobby("穿越火线");
    personForJava3.addHobby("斗地主");
    Address address = new Address();
    address.myAddress = "四川省达州市";
    personForJava3.setAddress(address);

    try {
        PersonForJava personForJava4 = (PersonForJava) personForJava3.clone();
        Log.e(TAG, "personForJava3:" + personForJava3);
        Log.e(TAG, "personForJava4:" + personForJava4);
        Log.e(TAG, "personForJava3 ---> " + personForJava3.showAll());
        Log.e(TAG, "personForJava4 ---> " + personForJava4.showAll());
        Log.e(TAG, "personForJava3.getHobbyList() == personForJava4.getHobbyList():" + (personForJava3.getHobbyList() == personForJava4.getHobbyList()));
        Log.e(TAG, "personForJava3.getAddress() == personForJava4.getAddress():" + (personForJava3.getAddress() == personForJava4.getAddress()));

        Log.e(TAG, "-------------");

        personForJava4.setName("孙六");
        personForJava4.setAge(24);
        personForJava4.setWeight(121.13);
        personForJava4.addHobby("地下城与勇士");
        Address arrr = personForJava4.getAddress();
        arrr.myAddress = "四川省宜宾市";
        personForJava4.setAddress(arrr);
        Log.e(TAG, "personForJava3 ---> " + personForJava3.showAll());
        Log.e(TAG, "personForJava4 ---> " + personForJava4.showAll());
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
        Log.e(TAG, "对象克隆失败---2");
    }
}

2、 kotlin实现

Ⅰ.浅克隆(浅拷贝)

  1. 实现Cloneable接口

相对应kotlin的原型模式,我们也可以用上面Java的方式写出这么一套代码,但是与java中的java.lang.Cloneable接口不同,而是要实现kotlin中的kotlin.Cloneable!接口,以及重写clone()方法,并且clone()方法必须手动使用public修饰符。代码贴出来,仅供参考。需要说明一下,由于所有的类都在同一个工程下的同一个包当中,为了使得java类和kotlin有区别,在类名上就可以看出来。

class PersonForKotlin : Cloneable {

    var name: String? = null
    var age: Int? = null
    var weight: Double? = null

    var address = AddressForKotlin()
    var hobbyList = arrayListOf<String>()

    fun showAll() : String{
        return "name: " + name + ",age: " + age + ",weight: " + weight + ",hobbyList: " + hobbyList + ",address: " + address.myAddress;
    }

    public override fun clone(): Any {
//        var tempPerson = PersonForKotlin()
//        try {
//            tempPerson = super.clone() as PersonForKotlin
//            tempPerson.address = address.clone() as AddressForKotlin
//            tempPerson.hobbyList = hobbyList.clone() as ArrayList<String>
//        } catch (e: Exception) {
//            e.printStackTrace()
//        }
//        return tempPerson

        return super.clone()
    }
}

然后是AddressForKotlin类:

class AddressForKotlin: Cloneable {

    var myAddress : String? = null

    override fun clone(): Any {
        return super.clone()
    }
}

下面是测试的方法。

/**
 * 浅克隆
 */
private fun shallowClone(){
    val p1 = PersonForKotlin()
    p1.name = "张三"
    p1.age = 24
    p1.weight = 133.12
    p1.hobbyList.add("跳水")
    p1.hobbyList.add("滑冰")
    p1.hobbyList.add("体操")
    val address1 = AddressForKotlin()
    address1.myAddress = "湖北省武汉市"
    p1.address = address1

    val p2 = p1.clone() as PersonForKotlin
    println("p1:$p1")
    println("p2:$p2")
    println("p1 ---> " + p1.showAll())
    println("p2 ---> " + p2.showAll())
    println("p1.hobbyList === p2.hobbyList:" + (p1.hobbyList === p2.hobbyList))
    println("p1.hobbyList == p2.hobbyList:" + (p1.hobbyList == p2.hobbyList))
    println("p1.address == p2.address:" + (p1.address == p2.address))
    println("p1.address === p2.address:" + (p1.address === p2.address))

    println("-------------")

    p2.name = "李四"
    p2.age = 28
    p2.weight = 136.78
    p2.hobbyList.add("游泳")
    address1.myAddress = "湖北省宜昌市"
    p2.address = address1
    println("p1 ---> " + p1.showAll())
    println("p2 ---> " + p2.showAll())
}

运行结果如下,可以看到和用java的方式是一样的。通过克隆方法,得到的不是同一个对象,但是具有相同的属性,并且引用属性都是指向的同一个,所以当p2修改了兴趣列表以及家庭住址的引用类型的属性时,p1也会同时改变。

  1. 使用data class

其实kotlin对于浅克隆早已经为我们做了封装,就是使用data class数据类的copy方法。而数据类其实也是对各种JavaBean对象的一种语法糖,我们就不用再像java那样写一堆get/set方法,如果不需要添加额外的方法,那么只需要一行代码就可以搞定。具体data class的使用不是本文的重点,不再过多说明。这里我们创建一个data class类MyDataClass:

data class MyDataClass(var name: String, var age: Int, var weight: Double, var hobbyList: ArrayList<String>, var address: AddressForKotlin){
    fun showAll() : String{
        return "name: $name,age: $age,weight: $weight,hobbyList :$hobbyList,address: ${address.myAddress}";
    }
}

在类中添加了一个showAll方法,用来打印所有的数据。接下来测试一下。

private fun useDataClass(){
    val hList = arrayListOf("射击","马术","击剑")
    val addr = AddressForKotlin()
    addr.myAddress = "蜀国"
    val myDataClass1 = MyDataClass("赵云",31,153.66, hList,addr)
    val myDataClass2 = myDataClass1.copy()

    println("myDataClass1:${myDataClass1.showAll()}")
    println("myDataClass2:${myDataClass2.showAll()}")
    println("myDataClass1.hobbyList === myDataClass2.hobbyList:" + (myDataClass1.hobbyList === myDataClass2.hobbyList))
    println("myDataClass1.hobbyList == myDataClass2.hobbyList:" + (myDataClass1.hobbyList == myDataClass2.hobbyList))
    println("myDataClass1.address == myDataClass2.address:" + (myDataClass1.address == myDataClass2.address))
    println("myDataClass1.address === myDataClass2.address:" + (myDataClass1.address === myDataClass2.address))

    println("-------------")

    myDataClass2.name = "陆逊"
    myDataClass2.age = 38
    myDataClass2.weight = 145.91
    myDataClass2.hobbyList.add("火攻")
    addr.myAddress = "吴国"
    myDataClass2.address = addr
    println("myDataClass1:${myDataClass1.showAll()}")
    println("myDataClass2:${myDataClass2.showAll()}")
}

运行程序,得到以下输出。通过myDataClass1对象调用copy()方法,返回一个myDataClass2对象。从打印输出中我们可以看到,通过比较myDataClass1和myDataClass2不是同一个对象,但具有相同的属性,并且引用类型指向是同一个,当myDataClass2修改引用类型属性的值后,myDataClass1也会引起改变。

Ⅱ.深克隆(深拷贝)

kotlin的深克隆和java是一样的。如果属性中有引用类型,就拿PersonForKotlin这个类来说,需要重写类中的clone()方法。里面有AddressForKotlin家庭地址属性和arrayListOf()列表属性这两个引用类型的属性。我们需要重写每个引用类型的clone方法,当然arrayListOf()系统已经帮我们重写了,我们需要重写AddressForKotlin类。

public override fun clone(): Any {
    var tempPerson = PersonForKotlin()
    try {
        tempPerson = super.clone() as PersonForKotlin
        tempPerson.address = address.clone() as AddressForKotlin
        tempPerson.hobbyList = hobbyList.clone() as ArrayList<String>
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return tempPerson

//  return super.clone()
}

深克隆的具体用法和浅克隆是完全一样的,就不单独贴代码了。

class PrototypeModeKotlinActivity : Activity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_prototype_mode)

//        shallowClone()
//        useDataClass()
        deepClone()
    }

    /**
     * 浅克隆
     */
    private fun shallowClone(){
        val p1 = PersonForKotlin()
        p1.name = "张三"
        p1.age = 24
        p1.weight = 133.12
        p1.hobbyList.add("跳水")
        p1.hobbyList.add("滑冰")
        p1.hobbyList.add("体操")
        val address1 = AddressForKotlin()
        address1.myAddress = "湖北省武汉市"
        p1.address = address1

        val p2 = p1.clone() as PersonForKotlin
        println("p1:$p1")
        println("p2:$p2")
        println("p1 ---> " + p1.showAll())
        println("p2 ---> " + p2.showAll())
        println("p1.hobbyList === p2.hobbyList:" + (p1.hobbyList === p2.hobbyList))
        println("p1.hobbyList == p2.hobbyList:" + (p1.hobbyList == p2.hobbyList))
        println("p1.address == p2.address:" + (p1.address == p2.address))
        println("p1.address === p2.address:" + (p1.address === p2.address))

        println("-------------")

        p2.name = "李四"
        p2.age = 28
        p2.weight = 136.78
        p2.hobbyList.add("游泳")
        address1.myAddress = "湖北省宜昌市"
        p2.address = address1
        println("p1 ---> " + p1.showAll())
        println("p2 ---> " + p2.showAll())
    }

    private fun useDataClass(){
        val hList = arrayListOf("射击","马术","击剑")
        val addr = AddressForKotlin()
        addr.myAddress = "蜀国"
        val myDataClass1 = MyDataClass("赵云",31,153.66, hList,addr)
        val myDataClass2 = myDataClass1.copy()

        println("myDataClass1 === myDataClass2:${myDataClass1 === myDataClass2}")
        println("myDataClass1:${myDataClass1.showAll()}")
        println("myDataClass2:${myDataClass2.showAll()}")
        println("myDataClass1.hobbyList === myDataClass2.hobbyList:" + (myDataClass1.hobbyList === myDataClass2.hobbyList))
        println("myDataClass1.hobbyList == myDataClass2.hobbyList:" + (myDataClass1.hobbyList == myDataClass2.hobbyList))
        println("myDataClass1.address == myDataClass2.address:" + (myDataClass1.address == myDataClass2.address))
        println("myDataClass1.address === myDataClass2.address:" + (myDataClass1.address === myDataClass2.address))

        println("-------------")

        myDataClass2.name = "陆逊"
        myDataClass2.age = 38
        myDataClass2.weight = 145.91
        myDataClass2.hobbyList.add("火攻")
        addr.myAddress = "吴国"
        myDataClass2.address = addr
        println("myDataClass1:${myDataClass1.showAll()}")
        println("myDataClass2:${myDataClass2.showAll()}")
    }

    /**
     * 深克隆
     */
    private fun deepClone(){
        val p3 = PersonForKotlin()
        p3.name = "张三"
        p3.age = 24
        p3.weight = 133.12
        p3.hobbyList.add("跳水")
        p3.hobbyList.add("滑冰")
        p3.hobbyList.add("体操")
        val address1 = AddressForKotlin()
        address1.myAddress = "湖北省武汉市"
        p3.address = address1

        val p4 = p3.clone() as PersonForKotlin
        println("p3:$p3")
        println("p4:$p4")
        println("p3 ---> " + p3.showAll())
        println("p4 ---> " + p4.showAll())
        println("p3.hobbyList == p4.hobbyList:" + (p3.hobbyList === p4.hobbyList))
        println("p3.address == p4.address:" + (p3.address === p4.address))

        println("-------------")

        p4.name = "李四"
        p4.age = 28
        p4.weight = 136.78
        p4.hobbyList.add("游泳")
        p4.address.myAddress = "湖北省宜昌市"
        println("p3 ---> " + p3.showAll())
        println("p4 ---> " + p4.showAll())
    }
}

三、总结

原型模式就是以一个对象作为原型,当需要创建一个新的对象时,通过克隆方法克隆出一个新对象。而克隆又分为浅克隆和深克隆。浅克隆只是传递地址指向,新的对象并没有对引用数据类型创建内存空间,所以当克隆对象对引用类型的值进行修改后,原型对象中的值也会随之变化;深克隆对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间,所以克隆对象对引用类型的属性进行修改后,不会影响到原型对象中的值。正是由于深克隆的这种特性,当有多层对象时,需要对每一层对象进行实现Cloneable接口并重写clone方法,进而实现对象的层层克隆,所以相比于浅克隆花销更大。

github代码:github.com/leewell5717…

四、参考

Java 浅拷贝和深拷贝

Android 原形模式(深浅拷贝)

Android Kotlin 设计模式之原型模式