克隆
浅克隆
浅克隆,也叫默认的克隆方法。在clone对象时,只会把基本数据类型的数据进行复制过去;如果是引用类型,只会把引用复制过去,也就是原对象和克隆对象共享了一些信息。原克隆数据一旦改变,克隆对象的信息会随之改变。
浅克隆的两个步骤:
1.实现 Cloneable 接口
2.重写 clone() 方法
Cloneable 接口是一个标记接口,里面没有任何方法,他的唯一作用就是允许在类型查询中是使用 instanceof :
if(obj instanceof Cloneable) . . .
clone() 方法是 Object 类的一个protect方法,返回一个 Object类的新的对象,而不是引用。
为什么要实现Cloneable接口?
《Java核心技术I》写道:对象对于克隆很“偏执”,如果一个对象请求克隆,但没有实现这个接口,就会产生一个受查异常
Cloneable接口是不包含任何方法的!其实这个接口仅仅是一个标志,而且这个标志也仅仅是针对 Object类中clone()方法的,如果clone类没有实现Cloneable接口,并调用了Object的clone()方法(也就是调用了 super.Clone()方法),那么Object的clone()方法就会抛出CloneNotSupportedException异常。
这是eclipse自动重写的clone()方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
应该修改成 public
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
为什么要把clone()方法改成public
不改写成 public ,就只能同一个包或者子类能访问这个方法,显然我们是希望别的类都能享用这个方法。
Object类是所有类的根类,为什么还要覆盖clone()方法
在Object中,声明如下protected native Object clone() throws CloneNotSupportedException;(由于使用native,无需实现方法体,该方法没有在object实现)。没有实现,自然没有继承这一说。即便是Object类的对象,也无法直接调用这个方法。
为什么要用clone()方法
clone方法是java中顶层父类Object中的一个方法,此方法在java中为实现,是一个native方法,也就是本地方法(可以调用底层操作系统的方法),在调用本地方法创建对象,比直接new创建对象效率高。
浅拷贝会有影响吗
要看具体情况。如果原对象和浅克隆对象共享的子对象是不可变的,那么这种共享是安全的。
如果一个子对象属于一个不变得类,这种情况也是安全的。
或者在子对象得生命周期中,子对象一直包含不变得常量,没有更改器方法会改变他,也没有方法会生成他的引用,这种情况也是安全的。
深克隆
克隆出一个不受原对象干扰的克隆对象
深克隆的方法:
重新定义clone方法来建立深拷贝,同时克隆对象中可变的实例域。
在浅克隆下,clone方法如下时
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
测试代码如下
Date date = new Date();
CloneClass v1 = new CloneClass();
v1.setId("007");
v1.setName("王小明");
v1.setDate(date);
System.out.println("v1: " + v1);
CloneClass v2 = (CloneClass) v1.clone();
System.out.println("v2: " + v2);
System.out.println("------------------");
v1.setId("009");
date.setTime(123456789);
System.out.println("v1: " + v1);
System.out.println("v2: " + v2);
String s1 = "java";
String s2 = s1;
System.out.println("s1 和 s2 是否同一对象:" + s1.equals(s2));
s2 += "spring";
System.out.println(s1 + " " + s2);
}
结果如下
v1: CloneClass [id=007, name=王小明, date=Thu Mar 12 01:37:20 CST 2020]
v2: CloneClass [id=007, name=王小明, date=Thu Mar 12 01:37:20 CST 2020]
------------------
v1: CloneClass [id=009, name=王小明, date=Fri Jan 02 18:17:36 CST 1970]
v2: CloneClass [id=007, name=王小明, date=Fri Jan 02 18:17:36 CST 1970]
s1 和 s2 是否同一对象:true
java javaspring
顺便验证了一下String这个不可变类:
我们可以发现 String 复制时两个引用同一个对象,但是一旦其中一个做出更改,便会自动创建新的对象,所以当 v1 改成009时,v2并没有随之改变,因此 String 并不是可变的实例。
这是v1初始化完成数据后,v2克隆之后的数据
执行 v1.setId(“009”);之后
执行 date.setTime(123456789);之后
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VJQndWqY-1584806060320)($resource/1583949462(1)].jpg)
最简单得改写成深克隆得方法:对可变的属性进行克隆。
@Override
public Object clone() throws CloneNotSupportedException {
Object cloned = super.clone();
CloneClass cl = (CloneClass)cloned;
//对可变的属性进行克隆
cl.date = (Date)this.date.clone();
return cloned;
}
或者可以改成不返回Object对象,而是返回自身的对象,感觉更加清晰简洁。
@Override
public CloneClass clone() throws CloneNotSupportedException {
CloneClass cloned = (CloneClass)super.clone();
//对可变得属性进行克隆
cloned.date = (Date)this.date.clone();
return cloned;
}
不可变类
不可变类(Immutable Objects):当类的实例一经创建,其内容便不可改变,即无法修改其成员变量。
可变类(Mutable Objects):类的实例创建后,可以修改其内容。
Java 中八个基本类型的包装类和 String 类都属于不可变类,而其他的大多数类都属于可变类。
与引用不可变的区别
需要特别注意的是,不可变类的不可变是指该类的实例不可变而非指向该实例的引用的不可变。
String s = "abc";
System.out.println("s:" + s); // 输出s:abc
s = "xyz";
System.out.println("s:" + s); // 输出s:xyz
以上代码显示,不可变类 String 貌似是可以改变值的,但实际上并不是。变量 s 只是一个指向 String 类的实例的引用,存储的是实例对象在内存中的地址。代码中第三行的 “改变” 实际上是新实例化了一个 String 对象,并将 s 的指向修改到新对象上,而原来的对象在内存中并未发生变化,只是少了一个指向它的引用,并且在未来被垃圾回收前它都将保持不变。
public class Immutable {
public static void main(String[] args) {
String str = new String("abc");
String str2 = str;
System.out.println(str == str2); // true
str2 = "cba";
System.out.println(str == str2); // false
System.out.println(str == row(str)); // true
System.out.println(str == other(str)); // false
}
static private String row(String s){
return s;
}
static private String other(String s){
s="xyz"; //此处形参 s 指向了新的String对象,引用的地址发生变化
return s;
}
}
如此我们看到,对于不可变类的对象,都是通过新创建一个对象并将引用指向新对象来实现变化 的。
通常,使用关键字 final 修饰的字段初始化后是不可变的,而这种不可变就是指引用的不可变。具体就是该引用所指对象的内存地址是不可变的,但并非该对象不可变。如果该对象也不可变,那么该对象就是不可变类的一个实例。
不可变类是如何实现的
immutable对象的状态在创建之后就不能发生改变,任何对它的改变都应该产生一个新的对象。
因此,一个不可变类的定义应当具备以下特征:
- 所有成员都是 private final 的
- 不提供对成员的改变方法,例如:setXXXX
- 确保所有的方法不会被重载。手段有两种:使用final Class(强不可变类),或者将所有类方法加上final(弱不可变类)。
- 如果某一个类成员不是基本类型(primitive type)或不可变类,必须通过在成员初始化(in)或者getter方法(out)时通过深度拷贝(即复制一个该类的新实例而非引用)方法,来确保类的不可变。
- 如果有必要,重写hashCode和equals方法,同时应保证两个用equals方法判断为相等的对象,其hashCode也应相等。
下面是一个示例:
public final class ImmutableDemo {
private final int[] myArray;
public ImmutableDemo(int[] array) {
// this.myArray = array; // 错误!
this.myArray = array.clone(); // 正确
}
public int[] get(){
return myArray.clone();
}
}
上例中错误的方法不能保证不可变性,myArray 和形参 array 指向同一块内存地址,用户可以在 ImmutableDemo 实例之外通过修改 array 对象的值来改变实例内部 myArray 的值。正确的做法是通过深拷贝将 array 的值传递给 myArray 。同样, getter 方法中不能直接返回对象本身,而应该是克隆对象并返回对象的拷贝,这种做法避免了对象外泄,防止通过 getter 获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。
不可变类的优点
不可变类有两个主要优点,效率和安全。
-
效率
当一个对象是不可变的,那么需要拷贝这个对象的内容时,就不用复制它的本身而只是复制它的地址,复制地址(通常一个指针的大小)只需要很小的内存空间,具有非常高的效率。同时,对于引用该对象的其他变量也不会造成影响。
此外,不变性保证了hashCode 的唯一性,因此可以放心地进行缓存而不必每次重新计算新的哈希码。而哈希码被频繁地使用, 比如在hashMap 等容器中。将hashCode 缓存可以提高以不变类实例为key的容器的性能。
-
线程安全
在多线程情况下,一个可变对象的值很可能被其他进程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况同时省去了同步加锁等过程,因此不可变类是线程安全的。
当然,不可变类也有缺点:不可变类的每一次“改变”都会产生新的对象,因此在使用中不可避免的会产生很多垃圾。