开场白
老铁 :今天我们“Java敲黑板系列”迎来了一位特殊嘉宾:他是Java家族顶级boss——Object的“左右护法”之一的“右护法” ——clone方法。他为什么会有这样的名望?江湖上给了他一个称号——“造物主”。那么“左护法”是谁了?——new方法。也就是说,所有的Java对象都是由“new方法”与“clone方法”所产生的;因此,他们是Java世界里面名副其实的“造物主”。
clone方法 :大家好,我是clone方法,很高心能来到“Java敲黑板系列”演播室和大家见面。
从一个例子讲起
//包包的颜色类
class Color{
private String name; //包包的颜色名称
public Color(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name){
this.name = name;
}
}
//包包类
public class Package implements Cloneable{
private String name; //包包的名字
private Color color; //包包的颜色
public Package(String name,Color color){
this.name = name;
this.color = color;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
public Package clone(){
Package pkg = null;
try {
pkg = (Package)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return pkg;
}
//输出包包的信息
public void print(){
System.out.println("Package "+getName()+" => Color is : "+getColor().getName());
}
//测试客户端
public static void main(String[] args) {
Package pkg1 = new Package("Dior",new Color("Red"));
pkg1.print();//打印Dior包的信息
//利用clone方法创建一个新对象,并更新新对象相关属性
Package pkg2 = pkg1.clone();
pkg2.setName("0007");
pkg2.getColor().setName("White");
pkg1.print();//再次打印Dior包的信息
pkg2.print();//打印0007包的信息
}
}
读到这里,老铁们都可以试着想想程序的输出信息。结果是让人大跌眼镜的:
Package Dior => Color is : Red
Package Dior => Color is : White
Package 0007 => Color is : White
对pkg1进行clone后,生成了新对象pkg2;接着对pkg2的名称与颜色等属性进行了修改。但是,让人意外的是:明明是只想修改pkg2的颜色属性,但是结果是把pkg1的颜色也修改了,并且还与pkg2颜色一模一样。这样的“撞衫”事件发生,让人多丢面子,这会让人觉得我一点品味都没有,买个包都和别人一模一样。
元芳,你怎么看?
遇到这样的问题,clone实在是很困惑,于是他把这个事情给他的老大Object报告了一下。
Object语重心长的对clone说:“所有的类都继承于我(Object),我提供了一个默认的对象拷贝实现,代码中的super.clone()就是在调用我的默认实现。但是,该方法并非十全十美,它只是提供了一个浅拷贝的方法,即该方法并不会把对象中的所有属性全部真正的clone一份,有一部分clone出来的属性与原来属性保持独立变化,而有一部分则不会。具体的处理规则如下:”,Object清了清嗓子,继续说道。
敲黑板:
- 如果属性是基本数据类型,Java基本类型包括了浮点型(float、double);整型(byte、short、int、long);字符型(char);布尔型(boolean)。那么就直接拷贝其值。
- 如果属性是对象类型,则拷贝地址引用,也就是说新拷贝的属性对象与原属性对象指向了同一块内存区域,其实是同一对象。
- 如果属性是String对象类型,该对象比较特殊,clone的也是一个地址引用;但是如果一旦修改该字符串的值,clone出来的对象就会重新生成一个新对象,原有的字符串对象保持不变。从实际的结果这一点来看,可以认为String类型与基本类型是一致的。
解决之道
知道原因以后,其实解决方法也就很简单了,那就是把“浅拷贝”转换为“深拷贝”,如下图所示。
public Package clone(){
Package pkg = null;
try {
pkg = (Package)super.clone();
//新增:新生成一个对象,切断与原对象关联,实现深拷贝
pkg.setColor(new Color(pkg.getColor().getName()));
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return pkg;
}
只需要对代码片段1中的clone方法进行修改(本例中只需要增加一句即可)就能实现Package类对象的深拷贝。通过深拷贝后,就能保证clone出来的对象与原对象互不影响。浅拷贝只是Java提供的一种简单拷贝机制,一般不能直接拿来就用。
思考题
请大家回顾一下“Java敲黑板系列”的NO.1 - NO.3, 内部有一条主线贯穿始终,欢迎大家把自己的感悟写在留言区进行讨论。
小结
敲黑板,画重点:
- Java提供的默认的clone方法实现了浅拷贝,其特性如下:如果属性是基本数值类型或String对象类型,则会拷贝其值;如果是对象类型,则只是拷贝对象的地址,实在是同一对象,会出现很多隐患。
- Java默认的clone方法只实现了简单的浅拷贝,在真实的业务应用中会存在很多的安全隐患,一般需要自定义实现深拷贝。
- clone方法和new方法是Java世界里生成对象的两种方法。但是,从性能上来看,一般用new方法生成对象的效率会比clone高。clone方法主要应用于构造函数比较复杂、成员属性比较繁多或者是用new方法创建对象比较耗时(可采用性能工具测试比较)的业务场景。
彩蛋
NO.2 equals与==的三生三世 | Java敲黑板系列中留了一个思考题,其解答答案如下:
String str1 = "敲黑板";
String str2 = "敲黑板";
String str3 = new String("敲黑板");
boolean ret;
ret = (str1 == str2);
//true:str1与str2均为直接量。对于直接量的字符串,Java从性能上有优化机制:
//Java为了避免在一个系统中产生大量的String对象;
//设计了一个字符串常量池容纳String对象。创建一个字符串
//时,首先检查池中是否有字符串内容相等的字符串;如果有则
//直接返回池中该对象的引用;否则创建并放入池中.
ret = (str1.equals(str2));
//true:String类的equals方法用于检查字符串内容相等
ret = (str1 == str3);
//false:直接量和对象之间的关系与两个直接量之间的关系不同
ret = (str1.equals(str3));
//true:String类的equals方法用于检查字符串内容相等
ret = (str1 == str1.trim());
//true:调用trim方法的字符串对象,返回值仍然为该对象的引用
转载自公众号:代码荣耀