原型模式是一种创建型设计模式,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.创建对象如果消耗资源很多的话,这样多次去创建并设值肯定会造成资源浪费。
对于以上的问题,我们可以使用原型模式进行优化。
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
接口是不是就完美的呢,其实并不是,因为如果一个对象的字段也是一个对象,是一个引用数据类型时,那就会有问题。请看以下代码:
我们增加一个对象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()
方法:
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
接口、序列化对象、反射机制。
原型模式的优点:
原型模式的缺点:
Cloneable
接口的方式,需要实现Cloneable
接口,对代码有一定的侵入性。Serializable
接口,对代码也有一定的侵入性。以上就是原型模式的学习,更多的java技术分享,就关注java技术爱好者吧!
能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!