首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >原型模式以及克隆技术

原型模式以及克隆技术

作者头像
java技术爱好者
发布2020-09-22 16:22:21
3890
发布2020-09-22 16:22:21
举报
文章被收录于专栏:java技术爱好者java技术爱好者

定义

原型模式是一种创建型设计模式,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技术爱好者吧!

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-05-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 java技术爱好者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 定义
  • 通俗解释
  • 不使用原型模式的问题
  • 使用Cloneable接口优化
  • 使用Cloneable接口的问题
  • 使用序列化实现深克隆
  • 使用反射实现克隆
  • Spring的copyProperties()
  • 总结
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档