写在前面:
没学习JAVA之前,想必大家都听过“克隆羊”这个词。克隆相当于复制的意思,但和复制的概念并不相同。克隆在百度上面的翻译如下:
Java中的克隆是克隆相对与类的实例来说的,克隆的是对象的一个副本。
采用的设计模式:原型模式(Prototype)
1. 什么时候使用克隆?
- 打个比方:你下班回家的路上,看的一家蛋糕店,恰巧你想吃蛋糕了,然后你进门看到模型的蛋糕。此时你并不知道每个蛋糕的名字,你指着你想要的蛋糕和销售员说要这个,然后,制作人员就克隆出一块一模一样的给你了。
- eg:方法需要返回引用,但是又不希望用户对该引用指向的对象进行修改。这时候直接返回clone()的结果
class Test {
private static int[] value;
static {
value = new int[]{1, 2, 3, 4, 5};
}
public static final int[] getValue() {
return value.clone();
}
}
2. 克隆的概念?
深浅克隆都会在堆中新分配一块区域,区别在于对象属性引用的对象是否需要进行克隆(详情看下面源码的注释部分)
- 浅克隆:
创建一个新对象,新对象的属性 和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。 - 深克隆:
创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。 - 源码分析:
1.Cloneable接口
源码翻译是不是看不懂,不用怕,源码翻译神器
/**
此处源码注释省略,直接上翻译。
1. 一个实现了Cloneable类意味着可以通过java.lang.Object的clone()合法的对该类的实例的属性逐一复制。
2. 对一个没有实现Cloneable接口的类的实例调用clone()会抛出
3. 按照惯例,实现了此接口的类应该重写clone(),重写时将该方法由受保护变为公开。
4. 需要注意的是,此接口并不包含clone()。因此,仅依靠实现此接口是不可能实现对象克隆的。即使clone()被成功调用,也不能保证克隆可以成功。
**/
public interface Cloneable {
}
2. Object类中clone方法
/**
此处源码注释省略,直接上翻译。
1.创建并返回此对象的一个副本。“副本”的准确含义可能依赖于对象的类。这样做的目的是,对于任何对象 x,表达式:x.clone() != x为 true,表达式:x.clone().getClass() == x.getClass()也为 true,但这些并非必须要满足的要求。一般情况下:x.clone().equals(x)为 true,但这并非必须要满足的要求。按照惯例,返回的对象应该通过调用 super.clone 获得。如果一个类及其所有的超类(Object 除外)都遵守此约定,则 x.clone().getClass() == x.getClass()。
2.按照惯例,此方法返回的对象应该独立于该对象(正被复制的对象)。要获得此独立性,在 super.clone 返回对象之前,有必要对该对象的一个或多个字段进行修改。这通常意味着要复制包含正在被复制对象的内部“深层结构”的所有可变对象,并使用对副本的引用替换对这些对象的引用。如果一个类只包含基本字段或对不变对象的引用,那么通常不需要修改 super.clone 返回的对象中的字段。Object 类的 clone 方法执行特定的复制操作。
4. 如果此对象的类不能实现接口 Cloneable,则会抛出CloneNotSupportedException。注意,所有的数组都被视为实现接口 Cloneable。否则,此方法会创建此对象的类的一个新实例,并像通过分配那样,严格使用此对象相应字段的内容初始化该对象的所有字段;这些字段的内容没有被自我复制。所以,此方法执行的是该对象的“浅表复制”,而不“深层复制”操作。
Object 类本身不实现接口 Cloneable,所以在类为 Object 的对象上调用 clone 方法将会导致在运行时抛出异常。
**/
protected native Object clone() throws CloneNotSupportedException;
克隆调用的是Object类的clone()方法,clone()是一个本地方法(native),默认的修饰符是protected;
3. 浅克隆(浅拷贝)
- 实现Cloneable接口,User类中重写Object类中的clone方法的【浅拷贝】
//User实体类
package cn.com;
import java.lang.Cloneable;
public class User implements Cloneable{
private int uid; //基本类型
private String uname; //引用类型
private Integer uage; //引用类型
private Integer money; //引用类型
private Quote quote; //引用类型
//此处省略Getter和Setter方法
@Override
public String toString() {
return "User{" +
"uid=" + uid +
", uname='" + uname + '\'' +
", uage=" + uage +
", money=" + money +
", quote=" + quote +
'}';
}
@Override
public Object clone() throws CloneNotSupportedException{
User user= (User) super.clone();
return user;
}
}
//自定义实体类用作演示引用类型
package cn.com;
public class Quote {
private int qid; //基本类型
private String qname;//引用类型
private Integer qage;//引用类型
//此处省略Getter和Setter方法
@Override
public String toString() {
return "Quote{" +
"qid=" + qid +
", qname='" + qname + '\'' +
", qage=" + qage +
'}';
}
}
//克隆测试类
package cn.com;
public class CloneTest {
public static void main(String[] args) {
User user=new User();
user.setUid(1001);
user.setUname("user");
user.setUage(18);
user.setMoney(128);
Quote quote=new Quote();
quote.setQid(2002);
quote.setQage(20);
quote.setQname("quote");
user.setQuote(quote);
try {
System.out.println("***************实现Cloneable接口,User类中重写Object类中的clone方法的【浅拷贝】***************");
User userClone= (User) user.clone();
System.out.println("原始对象"+user.toString());
System.out.println("克隆对象"+userClone.toString());
System.out.println("判断原始对象和克隆对象 "+(user==userClone));
System.out.println("判断两个对象中基本类型: "+(user.getUid()==userClone.getUid()));
System.out.println("判断两个对象中String类型: "+(user.getUname()==userClone.getUname()));
System.out.println("判断两个对象中Integer类型1: "+(user.getUage()==userClone.getUage()));
System.out.println("判断两个对象中Integer类型2: "+(user.getMoney()==userClone.getMoney()));
System.out.println("判断两个对象中引用类型: *************"+(user.getQuote()==userClone.getQuote()));
System.out.println();
// 修改浅拷贝对象的属性(基本类型和包装类型不变)
userClone.getQuote().setQage(40);
userClone.getQuote().setQname("QuoteCloneName");
userClone.getQuote().setQid(4004);
System.out.println("***************克隆后修改引用类型Quote的值打印比较***************");
System.out.println("原始对象"+user.toString());
System.out.println("克隆对象"+userClone.toString());
System.out.println("判断原始对象和修改后的克隆对象中基本类型: "+(user.getUid()==userClone.getUid()));
System.out.println("判断原始对象和修改后的克隆对象中String类型: "+(user.getUname()==userClone.getUname()));
System.out.println("判断原始对象和修改后的克隆对象中Integer类型1: "+(user.getUage()==userClone.getUage()));
System.out.println("判断原始对象和修改后的克隆对象中Integer类型2: "+(user.getMoney()==userClone.getMoney()));
System.out.println("判断原始对象和修改后的克隆对象中引用类型: *******"+(user.getQuote()==userClone.getQuote()));
System.out.println("***********************克隆后修改基本类型和包装类型的值打印比较**************************");
userClone.setUid(1002);
userClone.setUname("userName");
userClone.setUage(18);
userClone.setMoney(128); //Money如果是127的话,输出为true
System.out.println("原始对象"+user.toString());
System.out.println("克隆对象"+userClone.toString());
System.out.println("判断原始对象和修改后的克隆对象中基本类型: "+(user.getUid()==userClone.getUid()));
System.out.println("判断原始对象和修改后的克隆对象中String类型: "+(user.getUname()==userClone.getUname()));
System.out.println("判断原始对象和修改后的克隆对象中Integer类型1: "+(user.getUage()==userClone.getUage()));
System.out.println("判断原始对象和修改后的克隆对象中Integer类型2: "+(user.getMoney()==userClone.getMoney()));
System.out.println("判断原始对象和修改后的克隆对象中引用类型: *******"+(user.getQuote()==userClone.getQuote()));
System.out.println("*********************************验证String和Integer类型*************************************");
userClone.setUname("user"); //对于String类型,每次赋不同的值都会产生新的字符串对象放在常量池中,相同的值就会指向同一个引用地址
userClone.setUage(18);
userClone.setMoney(128); //Money如果是127的话,输出为true
System.out.println("原始对象"+user.toString());
System.out.println("克隆对象"+userClone.toString());
System.out.println("判断原始对象和修改后的克隆对象中String类型: "+(user.getUname()==userClone.getUname()));
System.out.println("判断原始对象和修改后的克隆对象中Integer类型1: "+(user.getUage()==userClone.getUage()));
System.out.println("判断原始对象和修改后的克隆对象中Integer类型2: "+(user.getMoney()==userClone.getMoney()));
System.out.println("判断原始对象和修改后的克隆对象中引用类型: *******"+(user.getQuote()==userClone.getQuote()));
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
//打印结果
***************实现Cloneable接口,User类中重写Object类中的clone方法的【浅拷贝】***************
原始对象User{uid=1001, uname='user', uage=18, money=128, quote=Quote{qid=2002, qname='quote', qage=20}}
克隆对象User{uid=1001, uname='user', uage=18, money=128, quote=Quote{qid=2002, qname='quote', qage=20}}
判断原始对象和克隆对象 false
判断两个对象中基本类型: true
判断两个对象中String类型: true
判断两个对象中Integer类型1: true
判断两个对象中Integer类型2: true
判断两个对象中引用类型: *************true
***************克隆后修改引用类型Quote的值打印比较***************
原始对象User{uid=1001, uname='user', uage=18, money=128, quote=Quote{qid=4004, qname='QuoteCloneName', qage=40}}
克隆对象User{uid=1001, uname='user', uage=18, money=128, quote=Quote{qid=4004, qname='QuoteCloneName', qage=40}}
判断原始对象和修改后的克隆对象中基本类型: true
判断原始对象和修改后的克隆对象中String类型: true
判断原始对象和修改后的克隆对象中Integer类型1: true
判断原始对象和修改后的克隆对象中Integer类型2: true
判断原始对象和修改后的克隆对象中引用类型: *******true
***********************克隆后修改基本类型和包装类型的值打印比较**************************
原始对象User{uid=1001, uname='user', uage=18, money=128, quote=Quote{qid=4004, qname='QuoteCloneName', qage=40}}
克隆对象User{uid=1002, uname='userName', uage=18, money=128, quote=Quote{qid=4004, qname='QuoteCloneName', qage=40}}
判断原始对象和修改后的克隆对象中基本类型: false
判断原始对象和修改后的克隆对象中String类型: false
判断原始对象和修改后的克隆对象中Integer类型1: true
判断原始对象和修改后的克隆对象中Integer类型2: false
判断原始对象和修改后的克隆对象中引用类型: *******true
*********************************验证String和Integer类型*************************************
原始对象User{uid=1001, uname='user', uage=18, money=128, quote=Quote{qid=4004, qname='QuoteCloneName', qage=40}}
克隆对象User{uid=1002, uname='user', uage=18, money=128, quote=Quote{qid=4004, qname='QuoteCloneName', qage=40}}
判断原始对象和修改后的克隆对象中String类型: true
判断原始对象和修改后的克隆对象中Integer类型1: true
判断原始对象和修改后的克隆对象中Integer类型2: false
判断原始对象和修改后的克隆对象中引用类型: *******true
总结:由打印输出可以看到,修改自己写的引用类型的参数,会影响克隆之前的值,因此这是浅拷贝的形式
我们可以观察到String类型和Integer类型克隆后修改的参数后,原始数据不会改变,但是,修改的结果为相同的时候,为什么原始数据不会改变,但是比较时地址相同呢?
首先,String类型和包装类克隆后的参数不会改变是因为:String类型和包装类型自身的一些特性,final关键字和没有为value提供Setter方法,导致参数传递时类似值传递的方式。
为什么比较地址时,结果相同呢?这是因为String类型和包装类型的缓存池问题,具体缓存池可查看,我的另一篇博客缓存池。
4. 深克隆(深拷贝)
-
每个引用类型属性内部都重写clone()方法
Quote实现Cloneable接口,并重写clone方法
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
User修改clone方法
@Override
public Object clone() throws CloneNotSupportedException{
User user=(User)super.clone();
Quote quo= (Quote) quote.clone();
user.setQuote(quo);
return user;
}
至此便可实现深拷贝,但是这样做的弊端是如果类中有多个引用类型的属性就需重写多个clone方法,这样做很麻烦,这是我们就可以使用序列化的方式实现对象的克隆。
-
实现Serializable接口,通过对象序列化和反序列化实现克隆
拓展
标识接口是没有任何方法和属性的接口
,它仅仅表明它的类属于一个特定的类型,供其他代码来测试允许做一些事情。使用标记接口的唯一目的是使得可以用 instanceof 进行类型查询,例如:
if(quote instanceof Cloneable){
Quote quo= (Quote) quote.clone();
user.setQuote(quo);
}
1. java.io.Serializable:未实现此接口的类将无法使其任何状态序列化或反序列化。为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID 值。
2.java.lang.Cloneable:表明 Object.clone() 方法可以合法地对该类实例进行按字段复制.实现此接口的类应该使用公共方法重写 Object.clone(它是受保护的)。如果在没有实现 Cloneable 接口的实例上调用 Object 的 clone 方法,则会导致抛出 CloneNotSupportedException 异常。
3.java.util.RandomAccess:用来表明其支持快速(通常是固定时间)随机访问。此接口的主要目的是允许一般的算法更改其行为,从而在将其应用到随机或连续访问列表时能提供良好的性能。
4.java.rmi.Remote:Remote 接口用于标识其方法可以从非本地虚拟机上调用的接口。任何远程对象都必须直接或间接实现此接口。只有在“远程接口”(扩展 java.rmi.Remote 的接口)中指定的这些方法才可远程使用。
//序列化与反序列静态方法
public static <T extends Serializable> T clone(T user){
try {
//序列化
ByteArrayOutputStream bout=new ByteArrayOutputStream();
ObjectOutputStream ooStream=new ObjectOutputStream(bout);
ooStream.writeObject(user);
//反序列化
ByteArrayInputStream bint=new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream ooStream2=new ObjectInputStream(bint);
return (User)ooStream2.readObject();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
//User和Quote实体类实现Seriablizable接口,其它不变
打印信息如下
原始对象User{uid=1001, uname='user', uage=18, money=128, quote=Quote{qid=2002, qname='quote', qage=20}}
克隆对象User{uid=1002, uname='user', uage=18, money=128, quote=Quote{qid=4004, qname='QuoteCloneName', qage=40}}
可以看到修改quote引用类型后,原始对象的值依然保持不变。说明这两个对象在内存中时相互独立的,对象间值的修改互不影响。
5. 总结
实现Serializable接口,通过对象序列化和反序列化实现深拷贝的形式是通过泛型
进行限定的,在程序编译期就可以判断出要克隆对象是否支持序列化。