原型模式

定义

原型模式是一种创建型设计模式,Prototype模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节,工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建。

通俗解释

比如有些人喜欢写文章,但是如果从头到尾原创的话太麻烦了,那么他可以上网去搜索,找一篇写得不错的文章,然后复制下来,做一些修改,最后发布就是自己的文章了。这其实就使用了原型模式的设计模式,创建一个对象过于麻烦的时候,我们只需要创建一次,后面再创建的话只需要对原对象进行克隆即可。

不使用原型模式的问题

假设我们有一个用户User的类,类里面有很多字段,当我们创建对象时,就会像这样子:

public class Main {
    public static void main(String[] args) throws Exception {
        //第一个用户
        User user = new User();
        user.setId(1);
        user.setName("张三");
        user.setAge(18);
        user.setJob("程序员");
        user.setSchool("家里蹲大学");
        user.setNation("汉族");
        //0-男 1-女
        user.setGender((byte)0);
        user.setPhone("110");
        user.setPoliticalFeatures("群众");
        user.setEducation("大学本科");
        //第二个用户,跟第一个用户只有id,name,phone这三个字段不同
        User user1 = new User();
        user1.setId(2);
        user1.setName("李四");
        user1.setAge(18);
        user1.setJob("程序员");
        user1.setSchool("家里蹲大学");
        user1.setNation("汉族");
        //0-男 1-女
        user1.setGender((byte)0);
        user1.setPhone("111");
        user1.setPoliticalFeatures("群众");
        user1.setEducation("大学本科");
    }
}

不难看出上面的代码有以下问题:

1.user对象有10个字段,明显在创建第二个user对象的时候有很多重复的设值的操作。在实际项目中,肯定还不止设置10个字段,那么就会显得很难看。

2.创建对象如果消耗资源很多的话,这样多次去创建设值肯定会造成资源浪费。

对于以上的问题,我们可以使用原型模式进行优化。

使用Cloneable接口优化

java提供了一个Cloneable接口,可以实现克隆对象的用途,怎么实现,请看以下代码:

//实现Cloneable接口
public class User implements Cloneable {
    //省略了字段
    //省略了字段的Get、Set方法

    //重写clone()方法
    @Override
    public User clone() throws CloneNotSupportedException {
        return (User) super.clone();
    }
}

然后就可以把main()方法的代码改成以下这样:

public static void main(String[] args) throws Exception {
        //第一个用户
        User user = new User();
        user.setId(1);
        user.setName("张三");
        user.setAge(18);
        user.setJob("程序员");
        user.setSchool("家里蹲大学");
        user.setNation("汉族");
        //0-男 1-女
        user.setGender((byte)0);
        user.setPhone("110");
        user.setPoliticalFeatures("群众");
        user.setEducation("大学本科");
        //调用克隆方法,复制第一个user对象
        User user1 = user.clone();
        user1.setId(2);
        user1.setName("李四");
        user1.setPhone("111");
        System.out.println(user1);
        //控制台打印结果
        //User{id=2, name='李四', phone='111', nation='汉族'...}
}

你是不是有疑问,这两个user对象内存地址是否一致呢?我们可以打印出来看看:

com.yehongzhi.httpclient.model.User@4c873330
com.yehongzhi.httpclient.model.User@119d7047

内存地址是不一样的,所以我们可以得出一个结论:克隆出来的对象是一个新的对象。

问题:克隆方法的底层是不是调用了构造器创建了一个对象的呢?

我们可以在构造器上面加一些打印语句来验证一下:

public class User implements Cloneable {
    //其他非重点代码省略

    //构造器,如果以下语句打印了两次,则证明clone调用了构造器创建对象
    public User() {
        System.out.println("调用了无参构造器");
    }

    //克隆方法
    @Override
    public User clone() throws CloneNotSupportedException {
        System.out.println("调用了clone()方法");
        return (User) super.clone();
    }
}

我们运行main()方法后,可以看到控制台打印信息如下:

// 调用了无参构造器
// 调用了clone()方法

只调用了一次构造器,我们可以得出结论:

clone()方法不是调用了构造器创建对象的。

如果你刨根究底,究竟clone()方法是怎么创建对象的,其实也很简单,打开源码:

public class Object {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    //最终会调用Object的克隆方法,是一个native修饰的方法
    protected native Object clone() throws CloneNotSupportedException;
}

native修饰的方法是什么意思呢?意思就是这个方法的实现不是用java,而是C/C++实现。这个native关键字我们可以单独写一篇文章细讲,这里就不深入展开。底层的实现逻辑就是拷贝一份数据,开辟一块新的内存。所以拷贝出来的对象,打印的内存地址和原来的对象不一样。

使用Cloneable接口的问题

使用Cloneable接口是不是就完美的呢,其实并不是,因为如果一个对象的字段也是一个对象,是一个引用数据类型时,那就会有问题。请看以下代码:

我们增加一个对象IdCard

public class IdCard {

    private String cardNo;

    private Integer validityPeriod;

    private Date createDate;

    //省略getter、setter方法
}
public class User implements Cloneable {
    //其他字段省略

    //身份证对象
    private IdCard idCard;

    //省略getter、setter方法
}

然后我们在main()方法赋值:

public class Main {

    public static void main(String[] args) throws Exception {
        //第一个用户
        User user = new User();
        //省略其他字段的赋值
        //创建一个IdCard对象
        IdCard idCard = new IdCard();
        //身份证号码
        idCard.setCardNo("111111");
        //创建日期
        idCard.setCreateDate(new Date());
        //身份证有效期
        idCard.setValidityPeriod(10);
        //user对象设置身份证对象
        user.setIdCard(idCard);
        //user克隆,得到user1
        User user1 = user.clone();
        //打印user、user1的IdCard对象的内存地址,内存地址一样!
        System.out.println(user.getIdCard());//IdCard@4c873330
        System.out.println(user1.getIdCard());//IdCard@4c873330
        //当改变克隆体user1的IdCard里面的字段值
        user1.getIdCard().setCardNo("222222");
        //源对象user的IdCard里面的字段值也跟着一起改变了
        System.out.println(user.getIdCard().getCardNo());//222222
    }
}

明显这样的克隆是有巨大的问题的,因为项目中不可能只有基本数据类型。那怎么解决呢?

//IdCard对象也要实现Cloneable接口
public class IdCard implements Cloneable{
    //也需要重写clone()方法
    @Override
    protected IdCard clone() throws CloneNotSupportedException {
        return (IdCard)super.clone();
    }
}
public class User implements Cloneable {

    @Override
    public User clone() throws CloneNotSupportedException {
        User user = (User) super.clone();
        //获取idCard源对象
        IdCard idCard = user.getIdCard();
        //克隆一个idCard对象。然后set值到user对象中
        user.setIdCard(idCard.clone());
        return user;
    }
}

最后我们再调用main()方法:

public class Main {
    public static void main(String[] args) throws Exception {
        //第一个用户
        User user = new User();
        //创建一个IdCard对象
        IdCard idCard = new IdCard();
        //身份证号码
        idCard.setCardNo("111111");
        //user对象设置身份证对象
        user.setIdCard(idCard);
        //克隆user对象,得到user1对象
        User user1 = user.clone();
        //打印user、user1的IdCard对象的内存地址,内存地址不一样了!
        System.out.println(user.getIdCard());//IdCard@4c873330
        System.out.println(user1.getIdCard());//IdCard@119d7047
        //当改变克隆体user1的IdCard里面的字段值
        user1.getIdCard().setCardNo("222222");
        //源对象user的IdCard里面的字段值也跟着一起改变了
        System.out.println(user.getIdCard().getCardNo());//111111
    }
}

使用序列化实现深克隆

上面使用Cloneable接口的方式,被称为浅克隆,如果你想要克隆的源对象里面又有对象时,里面的对象也要实现Cloneable接口,然后修改源对象的clone()方法,这样就非常麻烦,而且当扩展时会破坏开闭原则。

解决方法,我们可以采用序列化对象的方式,实现深克隆呢?请看以下代码:

User对象实现Serializable接口:

public class User implements Serializable {
    private static final long serialVersionUID = 8656071024384993135L;
}

IdCard对象实现Serializable接口:

public class IdCard implements Serializable {
    private static final long serialVersionUID = -422430076410272813L;
}

创建一个工具类CloneUtil实现深克隆:

public class CloneUtil {
    @SuppressWarnings("unchecked")
    public static <T> T depthClone(T t, Class<T> clazz) throws Exception {
        ByteArrayOutputStream baos = null;
        ObjectOutputStream ous = null;
        ByteArrayInputStream bais = null;
        ObjectInputStream ois = null;
        try {
            //将源对象序列化,写入流中,写入流的对象是一个拷贝的对象,原对象还在JVM中
            baos = new ByteArrayOutputStream();
            ous = new ObjectOutputStream(baos);
            ous.writeObject(t);
            //把流中的对象再读取到内存中,就获得了克隆后的对象
            bais = new ByteArrayInputStream(baos.toByteArray());
            ois = new ObjectInputStream(bais);
            return (T) ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("深克隆出现异常");
        } finally {
            if (ous != null) {ous.close();}
            if (baos != null) {baos.close();}
            if (bais != null) {bais.close();}
            if (ois != null) {ois.close();}
        }
    }
}

验证是否深克隆,在main()方法中打印内存地址查看即可:

public class Main {

    public static void main(String[] args) throws Exception {
        User user = new User();
        user.setIdCard(new IdCard());
        User user1 = CloneUtil.depthClone(user, User.class);
        //内存地址都不相同,达到了深克隆的效果
        System.out.println(user);//User@3d075dc0
        System.out.println(user1);//User@2ef1e4fa
        System.out.println(user.getIdCard());//IdCard@214c265e
        System.out.println(user1.getIdCard());//IdCard@306a30c7
    }
}

用序列化实现深克隆的优点就是,对源代码侵入性很低,只需要实现Serializable接口,不需要一层一层去实现Cloneable接口,还有重写clone()方法。

问题:如果这个实体类是在jar包中的呢,我们没法去修改实体类的代码,那怎么实现克隆呢?

使用反射实现克隆

利用反射,实际上我们可以拿到源对象的任何值,所以就可以实现克隆,请看以下代码:

我们创建一个copyProperties()方法,具体实现看以下代码:

public class CloneUtil {
    public static void copyProperties(Object source, Object target) throws Exception {
        //获取源对象的属性描述器
        PropertyDescriptor[] sourceDescriptors = Introspector
                .getBeanInfo(source.getClass())
                .getPropertyDescriptors();
        //获取目标对象的字段名称集合
        List<String> targetFieldNames = Arrays
                .stream(target.getClass().getDeclaredFields())
                .map(Field::getName)
                .collect(Collectors.toList());
        for (PropertyDescriptor sourceProperty : sourceDescriptors) {
            //获取源对象的属性名称
            String name = sourceProperty.getName();
            //源对象的getter方法
            Method readMethod = sourceProperty.getReadMethod();
            if (!readMethod.isAccessible()) {
                //设置方法的可访问权限
                readMethod.setAccessible(true);
            }
            //调用源对象的getter方法,获取到里面的每一个值
            Object value = readMethod.invoke(source);
            //如果源对象的属性名称包含在目标对象的字段名称集合中
            if (targetFieldNames.contains(name)) {
                //则通过源对象的属性名称获取目标对象属性的属性描述器
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(name, target.getClass());
                //获取目标对象的setter方法
                Method writeMethod = propertyDescriptor.getWriteMethod();
                writeMethod.setAccessible(true);
                //执行setter方法,参数是从源对象getter方法获取到的值
                writeMethod.invoke(target, value);
            }
        }
    }
}

测试:

public static void main(String[] args) throws Exception {
        User user = new User();
        user.setId(1);
        user.setName("张三");
        user.setIdCard(new IdCard());
        User user1 = new User();
        CloneUtil.copyProperties(user, user1);
        System.out.println(user.getName());//张三
        System.out.println(user1.getName());//张三
        System.out.println(user.getIdCard());//IdCard@b81eda8
        System.out.println(user1.getIdCard());//IdCard@b81eda8
    }

这样实现的不是深克隆的效果,所以IdCard对象的内存地址还是一样的。为了避免这样的结果,我们可以在使用时注意一下:

public static void main(String[] args) throws Exception {
        User user = new User();
        user.setName("张三");
        IdCard idCard = new IdCard();
        user.setIdCard(idCard);
        User user1 = new User();
        //创建一个新的IdCard对象
        IdCard idCard1 = new IdCard();
        CloneUtil.copyProperties(user, user1);
        //复制idCard的值到idCard1里
        CloneUtil.copyProperties(idCard,idCard1);
        //再设置idCard1到user1中
        user1.setIdCard(idCard1);
        System.out.println(user.getName());//张三
        System.out.println(user1.getName());//张三
        System.out.println(user.getIdCard());//IdCard@68de145
        //内存地址不同
        System.out.println(user1.getIdCard());//IdCard@27fa135a
    }

这样就避免产生内存地址一样的情况了。

Spring的copyProperties()

实际上在Spring框架中,已经提供了copyProperties()方法:

public static void main(String[] args) throws Exception {
        User user = new User();
        user.setName("张三");
        User user1 = new User();
        //Spring的copyProperties()方法
        BeanUtils.copyProperties(user,user1);
        System.out.println(user.getName());//张三
        System.out.println(user1.getName());//张三
    }

一般在实际项目中,我们采用copyProperties()方法实现原型模式会更好,因为这样不会破坏开闭原则,即使是jar包中定义的实体类,也可以使用。缺点就是如果对象层级比较多的话,会比较麻烦。

总结

实现原型模式的三种方式:实现Cloneable接口、序列化对象、反射机制。

原型模式的优点:

  1. 提高了创建对象的性能,避免了调用构造器创建对象。
  2. 对于创建一个对象需要很多资源的情况,可以减少资源的浪费。

原型模式的缺点:

  1. 如果使用Cloneable接口的方式,需要实现Cloneable接口,对代码有一定的侵入性。
  2. 如果使用序列化方式,则需要实现Serializable接口,对代码也有一定的侵入性。
  3. 如果使用反射机制,层级较多时会比较难维护。

以上就是原型模式的学习,更多的java技术分享,就关注java技术爱好者吧!

100

能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!



java 设计模式

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!